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 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.

Record this: Start usdview with the plugin path configured. Open the renderer menu, switch from Storm to HdRestir, then change one light intensity and one mesh transform. Show the viewport restarting accumulation after each edit.
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.