|
| 1 | +# Components and Graphs |
| 2 | + |
| 3 | +# ⚠️ Under Construction - not yet ready for use. ⚠️ |
| 4 | + |
| 5 | +When creating content to remix games, modders often want to make the replacement content react to the state of the game. The Component system has been created as a way to flexibly detect state and apply changes. |
| 6 | + |
| 7 | +Each individual component represents a small, discrete piece of functionality. These components can be connected together, so that data generated by one component can set the input parameters of another component. This forms a **Graph**, with each component being a single node in that graph. These graphs can create complex and reactive behaviors. |
| 8 | + |
| 9 | +Note that the graphs created must be **Directed Acyclic Graphs (DAGs)** - which means: |
| 10 | +- There must be at least one starting component (with no dependencies) |
| 11 | +- There can be no dependency loops (no component can depend on itself, either directly or indirectly) |
| 12 | + |
| 13 | +## Creating Graphs |
| 14 | + |
| 15 | +Graph creation should be done in the Remix Toolkit. The graph editor there is still in development. |
| 16 | + |
| 17 | +## Types of Components |
| 18 | + |
| 19 | +There are three major categories of components: |
| 20 | + |
| 21 | +### Trigger Components |
| 22 | + |
| 23 | +These components generate output values by examining the renderable state or generating values based on time. Examples include: |
| 24 | +* Detecting if a given mesh or material hash is used this frame |
| 25 | +* Detecting if the camera is inside some bounding box |
| 26 | +* Animating a number as time passes |
| 27 | + |
| 28 | +### Transform Components |
| 29 | + |
| 30 | +These components assist with mapping outputs to inputs with different types or ranges. Examples include: |
| 31 | +* Adding two values together |
| 32 | +* Outputting a boolean if some value is greater than another value |
| 33 | +* Combining floats into a vector |
| 34 | +* Converting between different data types |
| 35 | +* Applying mathematical operations |
| 36 | + |
| 37 | +### Override Components |
| 38 | + |
| 39 | +These components alter the renderable state. Examples include: |
| 40 | +* Altering configuration |
| 41 | +* Overriding the state of renderable objects such as Lights or Meshes |
| 42 | +* Swapping which replacements are applied to a given mesh hash |
| 43 | +* Altering the time multiplier (pausing or slowing all animations) |
| 44 | +* Modifying material properties |
| 45 | + |
| 46 | +## Component Data Types |
| 47 | + |
| 48 | +The component system supports the following data types as defined in `rtx_graph_types.h`: |
| 49 | + |
| 50 | +| Type | C++ Type | Description | Example Values | |
| 51 | +|------|----------|-------------|----------------| |
| 52 | +| `Bool` | `uint8_t` | Boolean values (stored as uint8_t for variant compatibility) | `true`, `false` | |
| 53 | +| `Float` | `float` | Single precision floating point | `1.0f`, `-3.14f` | |
| 54 | +| `Float2` | `Vector2` | 2D vector of floats | `Vector2(1.0f, 2.0f)` | |
| 55 | +| `Float3` | `Vector3` | 3D vector of floats | `Vector3(1.0f, 2.0f, 3.0f)` | |
| 56 | +| `Color3` | `Vector3` | RGB color (3D vector) | `Vector3(1.0f, 0.5f, 0.0f)` | |
| 57 | +| `Color4` | `Vector4` | RGBA color (4D vector) | `Vector4(1.0f, 0.5f, 0.0f, 1.0f)` | |
| 58 | +| `Int32` | `int32_t` | 32-bit signed integer | `42`, `-100` | |
| 59 | +| `Uint32` | `uint32_t` | 32-bit unsigned integer | `42`, `1000` | |
| 60 | +| `Uint64` | `uint64_t` | 64-bit unsigned integer | `42ULL`, `1000000ULL` | |
| 61 | +| `Prim` | `uint32_t` | USD prim identifier | `101`, `102` | |
| 62 | + |
| 63 | +## Creating Components |
| 64 | + |
| 65 | +Before creating a new component, check the list of existing components to see if one already exists that matches your needs: |
| 66 | +[Component List](components/index.md) |
| 67 | + |
| 68 | +Components are created in C++, using macros. The [TestComponent](https://github.com/NVIDIAGameWorks/dxvk-remix/blob/main/tests/rtx/unit/graph/test_component.h) is a good example component using every data type. |
| 69 | + |
| 70 | +Defining a component has three mandatory pieces: the parameter lists, the component definition, and the `updateRange` function. |
| 71 | + |
| 72 | +### Defining Parameters |
| 73 | + |
| 74 | +Parameters are how components accept Input, store internal State, and expose Output. Each of these are defined in separate lists using macros. |
| 75 | + |
| 76 | +#### Input Parameters |
| 77 | +Inputs can be set to a constant value or connected to another component's Output. They should be read-only during a component's update function. |
| 78 | + |
| 79 | +```cpp |
| 80 | +#define LIST_INPUTS(X) \ |
| 81 | + X(RtComponentPropertyType::Bool, false, inputBool, "Input Bool", "An example of a boolean input parameter") \ |
| 82 | + X(RtComponentPropertyType::Float, 1.f, inputFloat, "Input Float", "An example of a float input parameter") \ |
| 83 | + X(RtComponentPropertyType::Float3, Vector3(1.f, 2.f, 3.f), inputFloat3, "Input Float3", "An example of a Float3 input parameter") |
| 84 | +``` |
| 85 | +
|
| 86 | +#### State Parameters |
| 87 | +State values are not shared with any other components and can be used however the component needs. They persist between updates. |
| 88 | +
|
| 89 | +```cpp |
| 90 | +#define LIST_STATES(X) \ |
| 91 | + X(RtComponentPropertyType::Bool, false, stateBool, "", "An example of a Bool state parameter") \ |
| 92 | + X(RtComponentPropertyType::Float, 2.f, stateFloat, "", "An example of a Float state parameter") |
| 93 | +``` |
| 94 | + |
| 95 | +#### Output Parameters |
| 96 | +Output values can be read by other components but should only be set by the owning component. |
| 97 | + |
| 98 | +```cpp |
| 99 | +#define LIST_OUTPUTS(X) \ |
| 100 | + X(RtComponentPropertyType::Bool, false, outputBool, "Output Bool", "An example of a Bool output parameter") \ |
| 101 | + X(RtComponentPropertyType::Float, 3.f, outputFloat, "Output Float", "An example of a Float output parameter") |
| 102 | +``` |
| 103 | +
|
| 104 | +#### Parameter Options |
| 105 | +
|
| 106 | +Each parameter can have optional properties set after the docString: |
| 107 | +
|
| 108 | +```cpp |
| 109 | +X(RtComponentPropertyType::Float, 1.f, inputFloat, "Input Float", "test for Float", \ |
| 110 | + property.minValue = 0.0f, property.maxValue = 10.0f, property.optional = true) |
| 111 | +``` |
| 112 | + |
| 113 | +Available options include: |
| 114 | +* `property.minValue` / `property.maxValue` - Range constraints (currently UI hints only) |
| 115 | +* `property.optional` - Whether the component functions without this property being set |
| 116 | +* `property.oldUsdNames` - For backwards compatibility when renaming properties |
| 117 | +* `property.enumValues` - For displaying as an enum in the UI |
| 118 | + |
| 119 | +#### Enum Values Example |
| 120 | + |
| 121 | +The `property.enumValues` option allows you to define a set of named values for a property, which will be displayed as a dropdown in the UI. This is particularly useful for properties that should only accept specific predefined values. |
| 122 | + |
| 123 | +First, define your enum class: |
| 124 | + |
| 125 | +```cpp |
| 126 | +enum class LightType : uint32_t { |
| 127 | + Point = 0, |
| 128 | + Spot = 1, |
| 129 | + Directional = 2, |
| 130 | + Area = 3, |
| 131 | +}; |
| 132 | +``` |
| 133 | +
|
| 134 | +Then use it in your parameter definition: |
| 135 | +
|
| 136 | +```cpp |
| 137 | +#define LIST_INPUTS(X) \ |
| 138 | + X(RtComponentPropertyType::Uint32, 0, lightType, "Light Type", "The type of light to create", \ |
| 139 | + property.enumValues = { \ |
| 140 | + {"Point", {LightType::Point, "Omnidirectional point light"}}, \ |
| 141 | + {"Spot", {LightType::Spot, "Directional spot light with falloff"}}, \ |
| 142 | + {"Directional", {LightType::Directional, "Infinite directional light"}}, \ |
| 143 | + {"Area", {LightType::Area, "Area light with physical size"}} \ |
| 144 | + }) |
| 145 | +``` |
| 146 | + |
| 147 | +The enum values are stored as the underlying type (e.g., `uint32_t` for `LightType`) but displayed with user-friendly names and descriptions in the UI. |
| 148 | + |
| 149 | +### Defining the Component |
| 150 | + |
| 151 | +Invoke the `REMIX_COMPONENT` macro to define your component. Make sure the component name uses UpperCamelCase. |
| 152 | + |
| 153 | +```cpp |
| 154 | +REMIX_COMPONENT( \ |
| 155 | + /* the Component name */ MyComponent, \ |
| 156 | + /* the UI name */ "My Component", \ |
| 157 | + /* the UI categories */ "animation,transform", \ |
| 158 | + /* the doc string */ "A component that does something useful.", \ |
| 159 | + /* the version number */ 1, \ |
| 160 | + LIST_INPUTS, LIST_STATES, LIST_OUTPUTS); |
| 161 | +``` |
| 162 | +
|
| 163 | +### Defining the `updateRange` function |
| 164 | +
|
| 165 | +The `updateRange` function is responsible for updating a batch of components and will usually take the form: |
| 166 | +
|
| 167 | +```cpp |
| 168 | +void MyComponent::updateRange(const Rc<DxvkContext>& context, const size_t start, const size_t end) { |
| 169 | + // Update each component instance in the batch |
| 170 | + for (size_t i = start; i < end; i++) { |
| 171 | + // Read input values: |
| 172 | + if (m_inputBool[i]) { |
| 173 | + // Use state values |
| 174 | + m_stateFloat[i] = m_stateFloat[i] + 1.0f; |
| 175 | + } |
| 176 | + |
| 177 | + // Write output values: |
| 178 | + m_outputFloat[i] = m_stateFloat[i]; |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +**Important Notes:** |
| 184 | +* Graph updates usually happen between rendering frames, so any state being read will be from frame N, but any changes the graph makes will happen on frame N+1 |
| 185 | +* The exception is the first frame the graph exists on - it will be updated once immediately on creation to avoid rendering any default values |
| 186 | +* Always iterate from `start` to `end` to process the correct range of component instances |
| 187 | +* Access properties using the `m_` prefix and array indexing `[i]` |
| 188 | + |
| 189 | +## Component Versioning |
| 190 | + |
| 191 | +Great care needs to be taken when changing components that are already in use to avoid breaking already published data. When updating existing components, follow these guidelines. |
| 192 | + |
| 193 | +### Not a Versioned Change |
| 194 | + |
| 195 | +Making any of these changes does not require incrementing the version number in the Component Definition: |
| 196 | + |
| 197 | +* Change the UI name or tooltip of a property |
| 198 | +* Change the name of a component |
| 199 | + * Make sure to add the old name to the component definition as `spec.oldNames = {"OriginalName"}`. See `test_component.h` for example. |
| 200 | +* Fix logical bugs in the component |
| 201 | +* Make algorithmic changes in the component |
| 202 | + * i.e., change the way the component works, but not the inputs or outputs |
| 203 | + |
| 204 | +### Non-Breaking Change |
| 205 | + |
| 206 | +Making these changes requires bumping the version number and will prevent newly defined copies of this component from working in older runtimes. |
| 207 | + |
| 208 | +* Change a property name |
| 209 | + * Make sure to add the old name to the property definition as `property.oldUsdNames = {"oldName"}` |
| 210 | + * If a Component's USD prim defines multiple versions of the same property (due to layering or incorrect data migration), the strongest version will be used, as defined by: |
| 211 | + * Whichever version is defined on the strongest layer |
| 212 | + * If the strongest layer has multiple versions, then prefer the newest name |
| 213 | + * If the strongest layer does not have the newest name, use the earliest name in `oldUsdNames` |
| 214 | + * That means that if there are multiple `oldUsdNames`, then the oldest name should be on the right: |
| 215 | + ```cpp |
| 216 | + property.oldUsdNames = {"oldName", "olderName", "originalName"} |
| 217 | + ``` |
| 218 | +* Add a new property |
| 219 | +* Change the type or contents of a state property |
| 220 | + |
| 221 | +### Breaking Change |
| 222 | + |
| 223 | +Rather than making these changes to an existing component, a new component should be made (possibly by copying the old component and changing the name). |
| 224 | + |
| 225 | +* Change the type of an input or output property |
| 226 | + * We can't automatically convert a type that is connected to another component |
| 227 | +* Change the contents of an input or output property (i.e., float to unorm) |
| 228 | + * This would need to convert the connected values every frame. Better to just use a different update function. |
| 229 | +* Remove an input or output property |
| 230 | +* Repurpose a component name to a different component type |
| 231 | + * Once a name maps to a bit of functionality, that pairing should be permanent. |
| 232 | + |
| 233 | +**Note:** The UI name for a component can be changed, so while the new copy could use the original UI name, the original component's UI name could be changed to "My Component (deprecated)". |
| 234 | + |
| 235 | +Also note that ideally, the old component and new component should share as much code as possible via helper functions. |
| 236 | + |
| 237 | +## Component Registration |
| 238 | + |
| 239 | +Components are automatically registered when they are defined using the `REMIX_COMPONENT` macro, and the header file is added to `rtx_component_list.h`. The registration system: |
| 240 | + |
| 241 | +1. Generates a unique hash for each component type |
| 242 | +2. Stores the component specification for runtime lookup |
| 243 | +3. Enables automatic generation of documentation and schemas |
| 244 | + |
| 245 | +## Component Schema |
| 246 | + |
| 247 | +Remix Components are using a subset of the OmniGraph system to enable the Toolkit UI and USD encoding. It's important to note that while we expose .ogn schema for Remix Components, they are not functional OmniGraph nodes, and the Remix Runtime does not support non-remix OmniGraph components. |
| 248 | + |
| 249 | +## Graph Execution |
| 250 | + |
| 251 | +Components in a graph are executed in topological order based on their connections. The system: |
| 252 | + |
| 253 | +1. Determines the execution order to ensure all inputs are available before components that depend on them |
| 254 | +2. Updates components in batches for performance |
| 255 | +3. Calls `updateRange` on each component batch with the appropriate start/end indices |
| 256 | +4. Optionally calls `applySceneOverrides` for components that need to modify the scene directly |
| 257 | + |
| 258 | +Note that graphs are batched by a topological hash. This means that large numbers of graphs that have the same component and connections (but different input values) can be updated very performantly. If a graph has a different component or connection, it will be part of a separate batch. |
| 259 | + |
| 260 | +## Best Practices |
| 261 | + |
| 262 | +1. **Keep components focused**: Each component should do one thing well |
| 263 | +2. **Use meaningful names**: Component and property names should clearly indicate their purpose |
| 264 | +3. **Document thoroughly**: Provide clear docStrings for all components and properties |
| 265 | +4. **Test thoroughly**: Components should be tested with various input combinations |
| 266 | +5. **Consider performance**: Batch operations efficiently and avoid unnecessary computations |
| 267 | +6. **Plan for versioning**: Design components with future changes in mind. Consider using Enums instead of booleans |
| 268 | +7. **Use appropriate data types**: Choose the most specific type that fits your needs |
| 269 | +8. **Handle edge cases**: Consider what happens with invalid or missing inputs |
0 commit comments