Skip to content

Commit 52baaf9

Browse files
Merge branch 'REMIX-4240-component-versioning' into 'main'
REMIX-4240 Add handling for renaming components and their properties. Closes REMIX-4240 See merge request lightspeedrtx/dxvk-remix-nv!1671
2 parents b239af0 + eaa871c commit 52baaf9

File tree

8 files changed

+673
-31
lines changed

8 files changed

+673
-31
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

src/dxvk/rtx_render/graph/rtx_graph_component_macros.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@
8282
{ \
8383
RtComponentPropertySpec& property = s_spec.properties[index]; \
8484
__VA_ARGS__; \
85+
if (!property.oldUsdNames.empty()) { \
86+
std::string prefix = "inputs:"; \
87+
if (property.ioType == RtComponentPropertyIOType::Output) { \
88+
prefix = "outputs:"; \
89+
} \
90+
for (std::string& oldName : property.oldUsdNames) { \
91+
oldName = prefix + oldName; \
92+
} \
93+
} \
8594
index++; \
8695
} \
8796

@@ -93,7 +102,7 @@
93102

94103
#define REMIX_COMPONENT_WRITE_STATIC_SPEC(componentClass, uiName, categories, docString, versionNumber, X_INPUTS, X_STATES, X_OUTPUTS, ...) \
95104
static std::once_flag s_once_flag; \
96-
static const std::string s_fullName = "lightspeed.trex.components." #componentClass; \
105+
static const std::string s_fullName = RtComponentPropertySpec::kUsdNamePrefix + #componentClass; \
97106
static RtComponentSpec s_spec = { \
98107
{ \
99108
X_INPUTS(REMIX_COMPONENT_WRITE_PROPERTY_SPEC_INPUT) \

src/dxvk/rtx_render/graph/rtx_graph_types.cpp

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,25 @@ void registerComponentSpec(const RtComponentSpec* spec) {
143143

144144
std::lock_guard<std::mutex> lock(getComponentSpecMapMutex());
145145

146-
// Check for duplicate registration
147-
auto existing = getComponentSpecMap().find(spec->componentType);
148-
if (existing != getComponentSpecMap().end()) {
149-
Logger::err(str::format("Component spec for type ", spec->name, " already registered. Conflicting component spec: ", existing->second->name));
146+
// Check for duplicate registration and insert
147+
auto [iterator, wasInserted] = getComponentSpecMap().insert({spec->componentType, spec});
148+
if (!wasInserted) {
149+
Logger::err(str::format("Component spec for type ", spec->name, " already registered. Conflicting component spec: ", iterator->second->name));
150150
assert(false && "Multiple component specs mapped to a single ComponentType.");
151151
}
152152

153-
getComponentSpecMap()[spec->componentType] = spec;
153+
if (!spec->oldNames.empty()) {
154+
for (const auto& oldName : spec->oldNames) {
155+
std::string fullOldName = RtComponentPropertySpec::kUsdNamePrefix + oldName;
156+
RtComponentType oldType = XXH3_64bits(fullOldName.c_str(), fullOldName.size());
157+
auto [oldIterator, oldWasInserted] = getComponentSpecMap().insert({oldType, spec});
158+
if (!oldWasInserted) {
159+
Logger::err(str::format("Component spec for legacy type name ", fullOldName, " already registered. Conflicting component spec: ", oldIterator->second->name));
160+
assert(false && "Multiple component specs mapped to a single ComponentType.");
161+
}
162+
}
163+
}
164+
154165
}
155166

156167
const RtComponentSpec* getComponentSpec(const RtComponentType& componentType) {

src/dxvk/rtx_render/graph/rtx_graph_types.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ using RtComponentType = XXH64_hash_t;
152152
static const RtComponentType kInvalidComponentType = kEmptyHash;
153153

154154
struct RtComponentPropertySpec {
155+
static inline const std::string kUsdNamePrefix = "lightspeed.trex.components.";
156+
155157
RtComponentPropertyType type;
156158
RtComponentPropertyValue defaultValue;
157159
RtComponentPropertyIOType ioType;
@@ -165,11 +167,19 @@ struct RtComponentPropertySpec {
165167
// To set optional values when using the macros, write them as a comma separated list after the docString.
166168
// `property.<name> = <value>`, i.e. `property.minValue = 0.0f, property.maxValue = 1.0f`
167169

170+
// If this property has been renamed, list the old `usdPropertyName`s here for backwards compatibility.
171+
// If multiple definitions for the same property exist, the property on the strongest USD layer will be used.
172+
// If multiple definitions for the same property exist on a single layer, `name` will be used first,
173+
// followed by the earliest name in `oldUsdNames`. So the ideal order should be:
174+
// property.oldUsdNames = { "thirdName", "secondName", "originalName" }
175+
std::vector<std::string> oldUsdNames;
176+
168177
// NOTE: These are currently unenforced on the c++ side, but should be used for OGN generation.
169178
// TODO: consider enforcing these on the c++ side (between component batch updates?)
170179
RtComponentPropertyValue minValue = false;
171180
RtComponentPropertyValue maxValue = false;
172181

182+
173183
// Whether the component will function without this property being set.
174184
// Runtime side all properties have a default value, so this is mostly a UI hint.
175185
bool optional = false;
@@ -212,6 +222,8 @@ struct RtComponentSpec {
212222
// Optional functions for component batches. Set these by adding a lambda to the end of the component definition macro:
213223
// `spec.applySceneOverrides = [](...) { ... }`
214224

225+
// If this component has been renamed, list the old `name`s here for backwards compatibility.
226+
std::vector<std::string> oldNames;
215227

216228
// Optional function intended for applying values in the graph to renderable objects. This is called near the top of SceneManager::prepareSceneData.
217229
std::function<void(const Rc<DxvkContext>& context, RtComponentBatch& batch, const size_t start, const size_t end)> applySceneOverrides;

0 commit comments

Comments
 (0)