OpenUSD Hydra render delegates
OpenUSD composes authored scene description into a stage. Hydra converts the render-relevant part of that stage into renderer-facing primitives and tracks which properties have changed. HdRestir implements Hydra’s render-delegate interfaces and translates those primitives into its own scene representation, render pipelines, and image buffers.
This chapter covers:
- the distinction between USD composition and Hydra rendering;
- Rprim, Sprim, Bprim, and instancer responsibilities;
- plugin registration and render-delegate factories;
- dirty-bit-driven synchronization;
- render-pass execution, invalidation, and AOV handoff;
- the source files that implement each boundary.
USD, Hydra, and the render delegate
flowchart LR
USD["USD stage<br/>composed scene"] --> SI["Hydra scene index / adapter<br/>renderer-neutral data"]
SI --> RD["HdRestir render delegate<br/>renderer-owned objects"]
RD --> IMG["AOV render buffers<br/>color, depth, normals…"]
IMG --> UV["usdview viewport"]
- USD composes layers, references, variants, and opinions into a stage.
- Hydra tracks renderable state and dirtiness without prescribing an algorithm.
- The render delegate creates concrete renderer objects, consumes changes, executes work, and exposes outputs.
The OpenUSD introduction describes Hydra as the bridge that allows scene and render delegates to be mixed and matched. The current Hydra getting-started guide explains the scene-index architecture.
USD and Hydra should not be treated as two names for the same data structure. A USD stage supports composition, schemas, layer opinions, and time samples. Hydra presents renderer-oriented data and incremental change information. HdRestir stores a third representation containing the flattened meshes, materials, lights, acceleration structures, and buffers needed by its integrators. The translations between these representations are explicit parts of the renderer.
Hydra’s primitive families
Hydra groups renderer-facing objects by role:
| Family | Meaning | HdRestir examples |
|---|---|---|
| Rprim | Renderable primitive | meshes |
| Sprim | Scene/state primitive | materials, lights, cameras |
| Bprim | Buffer-backed primitive | render buffers, render settings |
| Instancer | Repeated transformed geometry | USD point/native instances |
Each object has a stable USD path and dirty bits. When a mesh transform changes,
Hydra can request only that data during Sync(); the delegate need not rebuild
unrelated materials.
Implementation files
All implementation excerpts in this chapter come from the first commit,
e6fe6bb.
Later Hydra changes are introduced only in their corresponding historical
chapters.
| File | Hydra responsibility | HdRestir responsibility |
|---|---|---|
plugInfo.json.in |
Plugin metadata and type discovery | Names the plugin library and renderer type |
renderer_plugin.cpp |
HdRendererPlugin factory interface |
Creates and destroys HdRestirRenderDelegate |
hd_restir_render_delegate.cpp |
Supported prim types and object factories | Creates HdRestir meshes, materials, lights, and buffers |
hd_restir_mesh.cpp |
Mesh Sync() and dirty bits |
Updates HdRestir mesh state and scene membership |
hd_restir_render_pass.cpp |
HdRenderPass::_Execute() and AOV bindings |
Restarts rendering when scene, camera, resolution, or outputs change |
hd_restir_render_buffer.cpp |
HdRenderBuffer allocation and resolve |
Transfers renderer output to Hydra-visible storage |
The frame lifecycle
sequenceDiagram
participant U as usdview / HdEngine
participant H as Hydra render index
participant D as HdRestir delegate
participant P as HdRestir render pass
participant R as Renderer
U->>H: stage changes
H->>D: Sync dirty prims
D->>D: update scene + invalidate state
H->>D: CommitResources
U->>P: Execute(renderPassState)
P->>R: render camera + AOV bindings
R-->>P: accumulated frame
P-->>U: render buffers
Sync() is about state transfer. Execute() is about producing pixels.
CommitResources() is the boundary after synchronization and before tasks;
OpenUSD’s
HdRenderDelegate reference
documents that lifecycle.
The Metal tutorials’
per-frame rendering loop
shows the lower-level command-buffer and command-encoder sequence. An
HdRenderPass is not a Metal render-pass descriptor: Hydra invokes a
renderer-facing operation, and HdRestir may satisfy it with several CPU or GPU
passes before resolving the requested AOVs.
In the first HdRestir implementation CommitResources() is empty. This is
valid because scene objects update the renderer-owned scene during their
Sync() calls, while render-thread coordination occurs through the render
parameter and render pass. An empty implementation should therefore be read as
an architectural choice at this revision, not as a statement that the Hydra
method is never important. A backend that batches GPU uploads could use this
boundary differently.
AOVs are the contract with the viewport
An arbitrary output variable (AOV) is a named image such as color, depth,
normal, or prim ID. Hydra gives the render pass bindings to HdRenderBuffer
objects. HdRestir renders into its own internal buffers, resolves the requested
outputs, and copies or maps them into those Hydra buffers.
This separation matters:
- the path tracer can accumulate high-precision spectral data internally;
- usdview can request a display-ready color buffer;
- tests can request an auxiliary result without changing the integrator;
- later, GPU-resident work can stay on the device until one explicit readback.
The delegate advertises concrete formats and clear values:
HdAovDescriptor
HdRestirRenderDelegate::GetDefaultAovDescriptor(TfToken const& name) const
{
if (name == HdAovTokens->color) {
return HdAovDescriptor(HdFormatFloat32Vec4, true,
VtValue(GfVec4f(0.0f)));
} else if (name == HdAovTokens->depth) {
return HdAovDescriptor(HdFormatFloat32, false, VtValue(1.0f));
} else if (name == HdRestirAovTokens->albedo) {
return HdAovDescriptor(HdFormatFloat32Vec3, false, VtValue(GfVec3f(0.0f)));
} else if (name == HdRestirAovTokens->normal) {
return HdAovDescriptor(HdFormatFloat32Vec3, false, VtValue(GfVec3f(0.0f)));
}
return HdAovDescriptor();
}
The color descriptor is multi-sampled because the renderer accumulates repeated estimator samples. Depth and albedo describe the current surface rather than an average of independent radiance estimates, so they use different semantics. Returning an empty descriptor for an unknown token tells Hydra that the delegate does not provide a default for that AOV.
Plugin discovery
plugInfo.json registers the render delegate type with OpenUSD’s plugin
system. The small renderer plugin class advertises the display name and creates
HdRestirRenderDelegate. The delegate, in turn, declares the prim types and
AOV descriptors it supports.
The renderer is therefore not a fork of usdview. It is a dynamically discovered participant in a published interface.
The template installed by CMake contains the type relationship and the library location:
{
"Plugins": [
{
"Info": {
"Types": {
"HdRestirRendererPlugin": {
"bases": [
"HdRendererPlugin"
],
"displayName": "Restir",
"priority": 99
},
"HdRestir_ImplicitSurfaceSceneIndexPlugin" : {
"bases": ["HdSceneIndexPlugin"],
"loadWithRenderer" : "Restir",
"priority": 0,
"displayName": "Scene Index to turn implicit surfaces into prims suitable for HdRestir"
}
}
},
"LibraryPath": "@PLUG_INFO_LIBRARY_PATH@",
"Name": "HdRestir",
"ResourcePath": "@PLUG_INFO_RESOURCE_PATH@",
"Root": "@PLUG_INFO_ROOT@",
"Type": "library"
}
]
}
The build replaces the @...@ placeholders with installation-relative paths.
OpenUSD reads this metadata, loads the shared library, and then reaches the C++
type registration:
TF_REGISTRY_FUNCTION(TfType)
{
HdRendererPluginRegistry::Define<HdRestirRendererPlugin>();
}
// ...
HdRenderDelegate*
HdRestirRendererPlugin::CreateRenderDelegate()
{
return new HdRestirRenderDelegate();
}
// ...
void
HdRestirRendererPlugin::DeleteRenderDelegate(HdRenderDelegate *renderDelegate)
{
delete renderDelegate;
}
The raw pointer is imposed by HdRendererPlugin’s virtual interface. Ownership
is still explicit: the registry calls CreateRenderDelegate, and Hydra later
returns the object to DeleteRenderDelegate.
Render-delegate factories
The delegate declares the primitive types it accepts:
const TfTokenVector HdRestirRenderDelegate::SUPPORTED_RPRIM_TYPES =
{
HdPrimTypeTokens->mesh,
};
const TfTokenVector HdRestirRenderDelegate::SUPPORTED_SPRIM_TYPES =
{
HdPrimTypeTokens->camera,
HdPrimTypeTokens->extComputation,
HdPrimTypeTokens->material,
HdPrimTypeTokens->distantLight,
HdPrimTypeTokens->sphereLight,
HdPrimTypeTokens->domeLight,
HdPrimTypeTokens->rectLight,
};
const TfTokenVector HdRestirRenderDelegate::SUPPORTED_BPRIM_TYPES =
{
HdPrimTypeTokens->renderBuffer,
};
These lists are capabilities, not a request to instantiate every type. Hydra uses the associated factory only when the render index needs a particular primitive. For example:
HdRprim *
HdRestirRenderDelegate::CreateRprim(TfToken const& typeId,
SdfPath const& rprimId)
{
HdRestir_LOG << "[Restir] CreateRprim: " << typeId.GetText() << " " << rprimId.GetText() << std::endl;
if (typeId == HdPrimTypeTokens->mesh) {
auto rPrim{std::make_unique<HdRestirMesh>(rprimId)};
return rPrim.release();
}
return nullptr;
}
The SdfPath is retained as stable identity. Later scene updates, material
bindings, instancing, and object IDs depend on that identity rather than on the
address of a temporary USD object.
Mesh synchronization
HdRestirMesh::Sync receives a scene delegate, the render parameter shared by
HdRestir prims, and a dirty-bit mask. The first implementation acquires the
renderer scene explicitly:
HdRestirMesh::Sync(HdSceneDelegate* sceneDelegate,
HdRenderParam* renderParam,
HdDirtyBits* dirtyBits,
TfToken const &reprToken)
{
const SdfPath& id = GetId();
auto* restirRenderParam{static_cast<HdRestirRenderParam*>(renderParam)};
std::lock_guard<std::recursive_mutex> lock{restirRenderParam->GetSceneLock()};
restirRenderParam->AcquireSceneForEdit();
_instancerId = sceneDelegate->GetInstancerId(id);
if (HdChangeTracker::IsVisibilityDirty(*dirtyBits, id)) {
_visible = sceneDelegate->GetVisible(id);
}
if (HdChangeTracker::IsTransformDirty(*dirtyBits, id)) {
_transform = GfMatrix4f(sceneDelegate->GetTransform(id));
}
// ...
}
The lock excludes concurrent access while AcquireSceneForEdit() marks the
scene as being modified. Transform and visibility are updated independently
according to their dirty bits. Later refactors place this protocol behind an
EditScene callback; that later spelling is not used here.
Points, normals, UVs, colors, computed primvars, topology, subsets, material
bindings, and instancer identity have their own checks in the same file.
Clearing AllSceneDirtyBits at the end asserts that the object has consumed
every scene-related change it advertises. Failing to read a property before
clearing its bit would leave the renderer with stale state and no future
notification for that change.
Render-pass invalidation
The render pass compares versioned or value-based state before starting more samples. In the first commit, a scene-version change only requests a restart:
int currentSceneVersion{_sceneVersion->load()};
if (_lastSceneVersion != currentSceneVersion) {
needStartRender = true;
_lastSceneVersion = currentSceneVersion;
}
This is weaker than the final implementation: the block itself does not stop the worker or clear accumulated state. Camera, data-window, and AOV changes do perform their own stop and reconfiguration. The historical chapter on this commit treats invalidation as an explicit limitation rather than attributing later behavior to the first revision.
After translating the AOV bindings, the pass resolves the currently available result:
const auto rendererAovBindings{MakeRendererAovBindings(aovBindings)};
const auto requestedOutputNames{Restir::Renderer::CollectRequestedOutputNames(rendererAovBindings)};
// ...
_renderer->ResolveTargets(rendererAovBindings);
if (needStartRender) {
// Start (or restart) rendering only when state changes occurred.
_renderer->MarkAovBuffersUnconverged(rendererAovBindings);
_renderThread->StartRender();
}
Resolution and restart are distinct. ResolveTargets lets usdview display the
latest complete data while the render thread is stopped or being
reconfigured. StartRender occurs only after all changed state has been
applied.
Caption to keep: Hydra synchronizes changed scene state into the render delegate; the renderer invalidates stale accumulation and produces new AOVs for the same viewport.
Ownership boundaries
The ownership model used in the following chapters is:
| Layer | Owns | Does not own |
|---|---|---|
| USD | Authored and composed scene description | HdRestir acceleration structures or accumulation |
| Hydra | Render-index primitives, dirty-state propagation, task execution, AOV bindings | The numerical path-tracing algorithm |
| HdRestir | Renderer scene representation, integrators, persistent estimator state, CPU/GPU buffers | USD layer composition |
The first implementation commit can be read as the code that establishes these three boundaries and connects them to a complete render loop.