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 implementation CommitResources() is empty because scene objects
write renderer-owned state during Sync(). This is a fact about this revision,
not a general Hydra rule; a backend that batches uploads can use the same
boundary for deferred resource work.
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 second constructor argument asks Hydra for a multisampled color buffer. That flag describes the AOV buffer requested from Hydra; it does not, by itself, define how HdRestir accumulates Monte Carlo samples. HdRestir’s estimator accumulation is separate renderer-owned state. Depth, albedo, and normal request non-multisampled buffers in this revision. Returning an empty descriptor for an unknown token tells Hydra that the delegate does not provide a default for that AOV.
The recording below shows the viewport side of this contract. Watch the AOV name in the upper-right overlay and the image interpretation while the active output changes. A normal AOV is a geometric diagnostic, not a false-color version of radiance; a depth AOV likewise has its own scalar range and display mapping.
dbca10b, using the Open Chess Set. This later interface
capture demonstrates the Hydra AOV boundary described by the first
commit; it is not presented as a recording of e6fe6bb.
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
}
}
},
"LibraryPath": "@PLUG_INFO_LIBRARY_PATH@",
"Name": "HdRestir",
"Type": "library"
}]
}
The excerpt omits the scene-index plugin and resource/root paths because they do not change renderer discovery. The complete file remains linked in the implementation table. CMake replaces the library placeholder with an installation-relative path; OpenUSD then loads that library and 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.
Renderer settings exposed through usdview
There are two visible setting paths in this capture. The selected USD
RenderSettings prim carries authored variants such as the pipeline used by
the stage, while the Hydra Settings dialog exposes runtime controls supplied
by the render delegate. The latter includes sample budgets, seed, resolution,
path depth, temporal reuse, denoising, camera overrides, physical sky, texture
budget, and diagnostic overlays.
dbca10b with the Open
Chess Set. The dialog belongs to the later renderer interface and is used
here to make the settings boundary concrete; individual controls are
discussed in the chapters that introduce their algorithms.
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.