Source convention: foundation chapters use the earliest project revision that contains the implementation being discussed. A later revision is used only when the relevant feature did not yet exist, and is identified immediately before the corresponding excerpt.

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:

  1. the distinction between USD composition and Hydra rendering;
  2. Rprim, Sprim, Bprim, and instancer responsibilities;
  3. plugin registration and render-delegate factories;
  4. dirty-bit-driven synchronization;
  5. render-pass execution, invalidation, and AOV handoff;
  6. 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"]
  1. USD composes layers, references, variants, and opinions into a stage.
  2. Hydra tracks renderable state and dirtiness without prescribing an algorithm.
  3. 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.

Selecting the displayed AOV in usdview. The viewport switches between renderer outputs without changing the USD scene; the overlay identifies the output currently being resolved by the render delegate. Recorded from an HdRestir working tree based on revision 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.

Selecting authored render-setting variants and opening Hydra's runtime settings dialog. Controls that alter the estimator or camera invalidate the previous accumulation before the next samples are displayed. Recorded from a working tree based on 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.