Skip to content

Conversation

@erikaharrison-adsk
Copy link
Contributor

Description of Change(s)

This is a refactoring of #3567 incorporating the code review feedback, namely:

  1. Removed the HdDirtyBits-based mechanism to check if the filter task cached when the codegen was launched can be reused during material sync. Instead, we always derive the necessary data again, which is a minor overhead relative to the codegen process itself. This can be further optimized in the future using an alternative mechanism, e.g. based on the change tracker as suggested in the original code review.
  2. Removed the new render delegate APIs for starting the early codegen and waiting for its results. Instead, the Storm render delegate injects a Storm-specific scene index which launches the parallel tasks when it receives material primitives. The waiting is exposed via a method of the scene index class.

Link to proposal (if applicable)

  • N/A

Fixes Issue(s)

  • N/A

Checklist

@jesschimein
Copy link
Collaborator

Filed as internal issue #USD-11537

(This is an automated message. See here for more information.)

Comment on lines 1311 to 1328
HdSceneIndexAdapterSceneDelegate::GetMaterialResourceFromSceneIndexPrim(
HdSceneIndexPrim& prim, const TfTokenVector& renderContexts)
{
TRACE_FUNCTION();
HF_MALLOC_TAG_FUNCTION();
HdSceneIndexPrim prim = _GetInputPrim(id);

HdMaterialSchema matSchema = HdMaterialSchema::GetFromParent(
prim.dataSource);
if (!matSchema.IsDefined()) {
return VtValue();
}

// Query for a material network to match the requested render contexts
const TfTokenVector renderContexts =
GetRenderIndex().GetRenderDelegate()->GetMaterialRenderContexts();
HdMaterialNetworkSchema netSchema = matSchema.GetMaterialNetwork(renderContexts);
HdMaterialNetworkSchema netSchema =
matSchema.GetMaterialNetwork(renderContexts);
if (!netSchema.IsDefined()) {
return VtValue();
}

return VtValue(_ToMaterialNetworkMap(netSchema, renderContexts));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted this static function for code reuse between the old synchronous code path and the new scene index which gets scene index prims without any conversions.

list(APPEND optionalPrivateClasses
materialXFilter
materialXShaderGen
materialXSyncSceneIndex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new scene index implementation, which replaces materialXSyncDispatcher in the original PR.

Comment on lines 226 to 234
_hdStMaterialNetwork.ProcessMaterialNetwork(GetId(), hdNetworkMap,
resourceRegistry.get());
fragmentSource = _networkProcessor.GetFragmentCode();
volumeSource = _networkProcessor.GetVolumeCode();
displacementSource = _networkProcessor.GetDisplacementCode();
materialMetadata = _networkProcessor.GetMetadata();
materialTag = _networkProcessor.GetMaterialTag();
params = _networkProcessor.GetMaterialParams();
textureDescriptors = _networkProcessor.GetTextureDescriptors();
fragmentSource = _hdStMaterialNetwork.GetFragmentCode();
volumeSource = _hdStMaterialNetwork.GetVolumeCode();
displacementSource = _hdStMaterialNetwork.GetDisplacementCode();
materialMetadata = _hdStMaterialNetwork.GetMetadata();
materialTag = _hdStMaterialNetwork.GetMaterialTag();
params = _hdStMaterialNetwork.GetMaterialParams();
textureDescriptors = _hdStMaterialNetwork.GetTextureDescriptors();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to rename _networkProcessor to _hdStMaterialNetwork. IMHO "Processor" sounds confusing here because the object _hdStMaterialNetwork is simply initialized with those Process methods from hdNetworkMap, it doesn't process some external objects.

Comment on lines 199 to 210
#ifdef PXR_MATERIALX_SUPPORT_ENABLED
{
HdStRenderDelegate* stormDelegate = static_cast<HdStRenderDelegate*>(
sceneDelegate->GetRenderIndex().GetRenderDelegate());

if (HdSt_MaterialXSyncSceneIndex* sceneIndex =
stormDelegate->GetMaterialXSyncSceneIndex()) {

sceneIndex->Wait();
}
}
#endif

This comment was marked as outdated.

Comment on lines +162 to +163
HdMaterialNode2 const*
HdSt_GetTerminalNode(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now need to call it from outside this source file.

return;
}

ProcessFilterTask(materialId, filterTask, isVolume, resourceRegistry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessMaterialNetwork is now implemented via ProcessFilterTask for code reuse.

Comment on lines +1105 to +1107
auto filterTask = std::make_shared<HdSt_MaterialFilterTask>();
filterTask->hdNetwork =
HdConvertToHdMaterialNetwork2(hdNetworkMap, &isVolume);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state necessary for the codegen process is now encapsulated in the HdSt_MaterialFilterTask class, because it needs to be created earlier than the material sprim and to persist until the parallel codegen task is finished.

HdStMaterialNetwork::ProcessMaterialNetwork is called in the old, synchronous code path, but we create a HdSt_MaterialFilterTask here in order to share code between the two code paths.

Comment on lines +387 to +388
void
HdSt_MaterialFilterTask::AddFallbackDomeLightTextureNode()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and a few other functions are now methods of HdSt_MaterialFilterTask.

const SdfPath domeTexturePath =
terminalNodePath.ReplaceName(_tokens->domeLightFallback);
hdNetwork->nodes.insert({domeTexturePath, hdDomeTextureNode});
hdNetwork.nodes.insert({domeTexturePath, hdDomeTextureNode});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and a few other related pieces of data are now encapsulated as members of HdSt_MaterialFilterTask.

Comment on lines +1346 to +1348
size_t
HdSt_MaterialFilterTask::BuildAnonymizedMaterialNetwork(
HdMaterialNetwork2* anonNetwork)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unified the terminology around "anonymized". The input parameters are replaced by HdSt_MaterialFilterTask's members.

static mx::ShaderPtr
_GenerateMaterialXShader(
HdMaterialNetwork2 const& hdNetwork,
HdSt_MaterialXGeneratorTask::HdSt_MaterialXGeneratorTask(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a different kind of "task" - the necessary data passed to the lambda executing in parallel in TBB and wrapping the MaterialX codegen.

Comment on lines +1530 to +1533
HdSt_MaterialXGeneratorTask generatorTask(
filterTask,
materialPath,
*resourceRegistry->GetHgi());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the generator task on the stack in the case when parallel codegen is off.

Comment on lines +1624 to +1627
return std::make_unique<HdSt_MaterialXGeneratorTask>(
filterTask,
materialPath,
hgi);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the generator task on the heap for parallel codegen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One important change from the original implementation is that the generator task now gets a shared pointer to the filter task and holds on to it. The generation process reads some material network data which is part of the filter task, so it's important to ensure that it's not destroyed before codegen is complete. In the original implementation, the sprim held ownership of the filter task for the duration of codegen.

@@ -0,0 +1,180 @@
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaces pxr/imaging/hdSt/materialXSyncDispatcher.cpp in the original implementation

Comment on lines +105 to +109
if (resourceRegistry->ContainsMaterialXShader(
generatorTask->GetShaderHash())) {
// We already have a shader for this topology.
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First check that the shader hasn't been registered before the early parallel init. This is a new method which, unlike RegisterMaterialXShader, doesn't add a new instance to the registry if an existing one isn't found.

Reusing RegisterMaterialXShader in combination with IsFirstInstance would be problematic because the intended usage pattern, in the case when there's no existing value, seems to be:

  1. Insert a nullptr value and return a registry instance pointing to that value and holding a lock to the whole MaterialX registry.
  2. The client code is supposed to populate the instance with a non-nullptr value while holding the lock. The instance's IsFirstInstance method returns true if the value is nullptr.

So the above pattern can't be used for parallel tasks, when the uniqueness check has to happen before launching the task, the non-nullptr value is known only when the task completes and the lock has to be released ASAP.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

Comment on lines +111 to +115
// Use a separate concurrent container for tracking unique generator tasks.
// The generated shader will be registered in the resource registry when
// the task completes.
auto insertResult =
_generatorTaskSet.insert(generatorTask->GetShaderHash());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another uniqueness check, this time between the different parallel tasks, using a dedicated data structure.

Comment on lines +61 to +64
#ifdef PXR_MATERIALX_SUPPORT_ENABLED
TF_DEFINE_ENV_SETTING(HDST_ENABLE_PARALLEL_MTLX_CODEGEN, false,
"Enable early parallelized MaterialX codegen");
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this setting definition here for better encapsulation.

Comment on lines 623 to 633
void
HdStRenderDelegate::SetTerminalSceneIndex(
const HdSceneIndexBaseRefPtr &terminalSceneIndex)
{
if (!TfGetEnvSetting(HDST_ENABLE_PARALLEL_MTLX_CODEGEN)) {
return;
}

_materialXSyncSceneIndex = HdSt_MaterialXSyncSceneIndexRefPtr(
new HdSt_MaterialXSyncSceneIndex(terminalSceneIndex, *this));
}
Copy link
Contributor

@ppenenko ppenenko Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injecting our scene index as the very last in the chain, and only if the optimization is enabled.

Comment on lines +635 to +640
HdSt_MaterialXSyncSceneIndex*
HdStRenderDelegate::GetMaterialXSyncSceneIndex()
{
return get_pointer(_materialXSyncSceneIndex);
}
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently only used in HdStMaterial::Sync to wait for the parallel codegen, but could be used in the future to reuse some per-material state.

Comment on lines +670 to +677
bool
HdStResourceRegistry::ContainsMaterialXShader(
HdInstance<MaterialX::ShaderPtr>::ID id)
{
bool found = false;
_materialXShaderRegistry.FindInstance(id, &found);
return found;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new method just wraps existing functionality.

@erikaharrison-adsk erikaharrison-adsk marked this pull request as ready for review October 14, 2025 23:59
@ppenenko
Copy link
Contributor

FYI @tcauchois

// Wait for all early parallel codegen tasks to complete and
// retrieve the state cached for this sprim when codegen was
// started
filterTask = sceneIndex->WaitAndExtractFilterTask(GetId());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait for the codegen tasks to complete. Previously, we were waiting by calling a new render delegate API before syncing the materials: https://github.com/PixarAnimationStudios/OpenUSD/pull/3567/files#r1995705092

Now, we wait for the generator task group in each material's sync. In practice, only the first material sprim would ever wait for any measurable duration, and the overhead of waiting for all subsequent sprims is negligible (around 1 microsecond).

This is also where we get the cached per-material state from the scene index. The filter task is removed from the scene index its ownership is transferred to the local variable.

Reusing the filter task allows us to avoid getting the material resource again below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible instead to have the material prim datasource call wait on the scene index, so that Storm doesn't need to deal with the new scene index at all? I'll try to sketch out a proposal in the scene index code.

_hdStMaterialNetwork.ProcessMaterialNetwork(GetId(),
hdNetworkMap, resourceRegistry.get());

processedMaterialNetwork = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the material network has processed the filter task, we don't need the task any more, so we destroy it by letting it go out of scope.

Comment on lines +89 to +101
HD_TRACE_FUNCTION();

for (const HdSceneIndexObserver::DirtiedPrimEntry& entry : entries) {
// If the dirtied prim is a material for which we've cached a filter
// task, remove the task since it's now stale. Any ongoing generator
// tasks will keep their own shared pointers to the respective filter
// tasks, so this is safe. If the element is not in the map, then the
// lookup won't involve locking.
_FilterTaskMap::accessor accessor;
if (_filterTaskMap.find(accessor, entry.primPath)) {
_filterTaskMap.erase(accessor);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting the filter task on dirtying.

{
_FilterTaskMap::accessor accessor;
_filterTaskMap.insert(accessor, id);
accessor->second = generatorTask->GetFilterTask();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching the filter task in the scene index.

@ppenenko
Copy link
Contributor

Description of Change(s)

This is a refactoring of #3567 incorporating the code review feedback, namely:

  1. Removed the HdDirtyBits-based mechanism to check if the filter task cached when the codegen was launched can be reused during material sync. Instead, we always derive the necessary data again, which is a minor overhead relative to the codegen process itself. This can be further optimized in the future using an alternative mechanism, e.g. based on the change tracker as suggested in the original code review.

FYI @tcauchois @klucknav I've pushed another change to this PR reintroducing the reuse of filter tasks in HdStMaterial::Sync, but with invalidation implemented via a simpler mechanism based on scene index dirty notifications instead of dirty flags.

This addresses the other major feedback item from #3567.

To recap, filter tasks can become stale if the material changes after parallel codegen started but before the sprim is synced. Animated materials is the only known case of this, and USD has tests for animated materials.

This should be the last change in this branch.

Copy link
Contributor

@tcauchois tcauchois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Pavlo, this is great progress! Moving the task launching to Hydra 2 PrimsAdded looks like it cut out a bunch of the hd API changes, and it should hopefully be letting you launch the tasks slightly earlier as well!

I feel like if we fold the WaitAndExtractFilterTask call into some kind of datasource access call inside MaterialXSyncSceneIndex we can additionally get rid of some of the new render delegate API in hdSt/renderDelegate.h, and make the scene index more modular/reusable, which I think a bunch of folks would appreciate past just the Storm team!

Happy to expand on my thoughts in the checkin tomorrow or on slack but this is definitely a good direction.

// Wait for all early parallel codegen tasks to complete and
// retrieve the state cached for this sprim when codegen was
// started
filterTask = sceneIndex->WaitAndExtractFilterTask(GetId());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible instead to have the material prim datasource call wait on the scene index, so that Storm doesn't need to deal with the new scene index at all? I'll try to sketch out a proposal in the scene index code.

SdfPath surfTerminalPath;
if (HdMaterialNode2 const* surfTerminal =
_GetTerminalNode(surfaceNetwork, terminalName, &surfTerminalPath)) {
filterTask->terminalNode =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here & material.*, instead of bringing the "filterTask" API into a bunch of files in Storm, you could modularize things better by either:
(1) having the scene index produce a "glsl" material network that can be read by the non-materialx code; or
(2) having the scene index produce a digested material network as code snippets for each stage & params, and pass them through as datasources. If material.cpp finds that, it uses it, and otherwise it falls back to network processing.

This splits the code up a little better, which is nice since the MaterialX code is an external dependency with an evolving API and hidden behind a build flag, so having a big API interface between the two makes me a bit worried.

using HdSt_MaterialFilterTaskSharedPtr =
std::shared_ptr<HdSt_MaterialFilterTask>;

extern HdMaterialNode2 const*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this needs an extern, but if you want it to be publicly visible (which maybe we don't care?) you could throw an HDST_API on it...

/// synchronously, or on the heap, owned by the respective Sprim, if the
/// codegen happens in parallel tasks.
///
struct ARCH_EXPORT_TYPE HdSt_MaterialFilterTask final
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like an implementation detail of the MaterialX code, and I don't think this belongs in hdSt/materialNetwork.h. Also curious why we need ARCH_EXPORT_TYPE here.

HdSt_MaterialXGeneratorTask(
HdSt_MaterialFilterTaskSharedPtr filterTask,
SdfPath const& materialPath,
Hgi const& hgi);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're improving on the status quo here (which is to pass in resource registry and pull hgi out of that), but it would be cleaner to just pass in the relevant info, namely bindless support and shading language.

Comment on lines +105 to +109
if (resourceRegistry->ContainsMaterialXShader(
generatorTask->GetShaderHash())) {
// We already have a shader for this topology.
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

HdSceneIndexPrim
HdSt_MaterialXSyncSceneIndex::GetPrim(const SdfPath &primPath) const
{
// Just forward to input scene index - we don't modify prim data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was envisioning that you call WaitAndExtractFilterTask here, either when you call GetPrim() on a materialx prim, or potentially when you call primDataSource->Get("material"). That way, you don't need all of the complicated logic in renderDelegate.h to pass the scene index pointer through.


#ifdef PXR_MATERIALX_SUPPORT_ENABLED
void
HdStRenderDelegate::SetTerminalSceneIndex(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the scene index, this bit breaks a bunch of encapsulation. We need GetMaterialXSyncSceneIndex so we can call WaitAndExtractFilterTask in material.cpp, but if we do that in the scene index GetPrim() or a datasource instead we don't need this call anymore. Meanwhile, rather than grafting the new scene index onto the terminal scene index like this, the more flexible/preferred way is to register a scene index plugin that inserts it somewhere relative to other scene index filters; it's a small bit of boilerplate you can grab from e.g. the dependencySceneIndexPlugin.* or something.

Then the only issue is your scene index's use of the render delegate, but it shouldn't really be getting a pointer to the render delegate anyway. It does need a pointer to a registry of MaterialX codegen results, but we could either find a lower key way to pass in the resource registry, or just have a local registry on the scene index instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants