Skip to content

Conversation

@erikaharrison-adsk
Copy link
Contributor

@erikaharrison-adsk erikaharrison-adsk commented Jun 3, 2025

Description of Change(s)

This change extends the existing MaterialX shader registry (an in-memory cache) with a persistent on-disk cache as a performance optimization. If we've generated a MaterialX shader with a unique ID on a previous run of the application and encounter the same ID on a subsequent run, we avoid redoing the expensive codegen process and instead restore the necessary data from cached files.

The cache directory can be set with the HDST_MTLX_CODEGEN_CACHE_DIR_PATH environment variable or the hdstMtlxCodegenCacheDirPath render setting. It's left up to the application to manage the cache, such as:

  • determine where to place the cache directory;
  • cleaning the directory if the cached files become invalidated, e.g. due to upgrading the versions of USD and/or MaterialX;
  • cleaning up cached files over a certain age or once a storage limit has been reached.

For reference, the optimization reduces the duration of HdRenderIndex::SyncAll for https://github.com/usd-wg/assets/tree/main/full_assets/OpenChessSet from about 550ms to about 250ms in our measurements.

Link to proposal (if applicable)

  • N/A

Fixes Issue(s)

  • N/A

Checklist

Comment on lines +27 to +29
using HdInstanceKey = uint64_t;
using HdInstanceRegistryMutex = std::mutex;
using HdInstanceRegistryLock = std::unique_lock<HdInstanceRegistryMutex>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Previously, these were typedefs in the templated class definition of HdInstance but they never depended on template parameters. They've also been referenced in the HdInstanceRegistry implementation below, where they had to be namespaced with HdInstance::. I'm extending the HdInstanceRegistry implementation in order to optionally support persistence, and these verbose typedefs were hurting the readability in that code, so I'm making them standalone.

explicit HdInstance(HdInstanceKey key,
ValueType const &value,
HdInstanceRegistryLock &&registryLock,
REGISTRY *registry)
Copy link
Contributor

Choose a reason for hiding this comment

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

Previously, it was enough for the instance to hold a pointer to the hash map container owned by the registry, because it would just directly put values in it. But now the registry can implement a custom persistence mechanism, so we need the instance to hold a pointer to the registry and call its methods instead.

/// is used to present a consistent interface to clients in cases
/// where shared resource registration is disabled.
explicit HdInstance(KeyType const &key)
explicit HdInstance(HdInstanceKey key)
Copy link
Contributor

Choose a reason for hiding this comment

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

We know that the key is always just an integer, so it's better to pass it around by value.

Comment on lines +126 to +128
/// The DERIVED template parameter can be used in a Curiously Recurring
/// Template Pattern to extend this class with a persistent cache backing the
/// in-memory dictionary.
Copy link
Contributor

Choose a reason for hiding this comment

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

BTW, this opens up the possibility to use the same pattern for other types of resources.

Comment on lines +148 to +149
/// Copy constructor. Need as HdInstanceRegistryBase is placed in a map
/// and mutex is not copy-constructible, so can't use default
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment doesn't seem to be correct: these objects are never stored by value in the USD codebase, so this constructor shouldn't be necessary and this class should comply with the rule of zero (right now, it doesn't: the copy operator is deleted below but it's copy-constructible).

return it;
}

VALUE value = static_cast<DERIVED*>(this)->LoadFromDisk(key);
Copy link
Contributor

Choose a reason for hiding this comment

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

Loading from disk, if implemented by the derived class.

Comment on lines +633 to +639
pxr_register_test(testHdStMaterialXShaderGen_testCacheSerialization
COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testHdStMaterialXShaderGen --testCacheSerialization"
EXPECTED_RETURN_CODE 0
STDOUT_REDIRECT shadergen_testCacheSerialization.out
DIFF_COMPARE shadergen_testCacheSerialization.out
TESTENV testHdStMaterialXShaderGen
)
Copy link
Contributor

@ppenenko ppenenko Jun 3, 2025

Choose a reason for hiding this comment

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

A new test that tests if metadata is serialized correctly by the new MaterialX codegen cache. It reuses an existing test executable testHdStMaterialXShaderGen with a new command-line argument.

#ifdef PXR_MATERIALX_SUPPORT_ENABLED
if (!isVolume) {
_materialXGfx = HdSt_ApplyMaterialXFilter(&surfaceNetwork, materialId,
_materialXCodegenResult = HdSt_ApplyMaterialXFilter(&surfaceNetwork, materialId,
Copy link
Contributor

Choose a reason for hiding this comment

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

This used to return a shared pointer to a MaterialX::Shader object. Unfortunately, a MaterialX::Shader is not serializable, and source code is only one part of its state. So, to make caching feasible, I extract all the data that the downstream pipeline needs from MaterialX::Shader, encapsulate it in an object called "codegen result" and cache that to disk.

Comment on lines +1111 to +1112
HdSt_MaterialParamVector const& fallbackParams =
mxCodegenResult.GetFallbackParams();
Copy link
Contributor

Choose a reason for hiding this comment

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

Fallback parameters are one necessary part of the codegen result.

// MaterialX parameter Information
const auto* variable = paramsBlock[i];
const auto varType = HdStMaterialXHelpers::GetMxTypeDesc(variable);
for (HdSt_MaterialParam const& fallbackParam : fallbackParams) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The old code was populating the fallback parameters from the MaterialX shader.

In the new code, they are retrieved from MaterialX in the codegen result's constructor if codegen takes place at run time, or read from disk if the codegen happened on a previous run and was cached, and stored in the codegen result object.

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome! :)

// MaterialX glslfxShader.
else {
std::string separator;
const auto varValue = variable->getValue();
Copy link
Contributor

Choose a reason for hiding this comment

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

All of this has moved to the codegen result's constructor.

if (!param.fallbackValue.IsEmpty()) {
materialParams->push_back(std::move(param));
}
for (TfToken textureParamName : mxCodegenResult.GetTextureParams()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, texture parameters are cached in the codegen result.

mx::ShaderPtr mxShader = HdSt_GenMaterialXShader(
mtlxDoc, stdLibraries, searchPaths, mxHdInfo, apiName);

return std::make_shared<HdSt_MaterialXCodegenResult>(*mxShader);
Copy link
Contributor

Choose a reason for hiding this comment

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

Creating a codegen result from the MaterialX Shader, and destroying the latter because we've already extracted everything we will ever need from it.

Comment on lines +1241 to +1258
// TfHashAppend hashes TfTokens not as strings, but as pointers to interned
// strings which are not stable from run to run
void _AppendPersistentHash(
Tf_HashState& hashState, TfToken const& token)
{
TfHashAppend(hashState, token.GetString());
}

// A VtValue may store a TfToken, in which case we need to convert it to a
// string in order to avoid the same pitfall as above
void _AppendPersistentHash(Tf_HashState& hashState, VtValue const& vtValue)
{
if (vtValue.IsHolding<TfToken>()) {
TfHashAppend(hashState, vtValue.Get<TfToken>().GetString());
} else {
TfHashAppend(hashState, vtValue.GetHash());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Without these workarounds, the cache always generated new, random hashes, and could never find an existing file in the cache directory.

Copy link
Contributor

Choose a reason for hiding this comment

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

I want to dig around internally and see if we have a function that does this already. If not, I may try to promote something like this to pxr/base. I think there are definitely some other cases where the TfHash isn't what you want for a fingerprint, though I'm not sure whether you'd hit them in a material network traversal.

Comment on lines +1432 to +1433
HdSt_MaterialXShaderRegistry* const materialXShaderRegistry =
resourceRegistry->GetMaterialXShaderRegistry();
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 specialized instance registry that supports a persistent cache. To minimize include dependencies, I expose it to clients of HdStResourceRegistry by pointer.

Comment on lines +27 to +28
TF_DEFINE_ENV_SETTING(HDST_MTLX_CODEGEN_CACHE_DIR_PATH, "",
"Path to the directory of the persistent MaterialX codegen cache");
Copy link
Contributor

Choose a reason for hiding this comment

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

Environment variable which can be used to set the path to the directory where the cached files are written. It serves as the default value for an HdSt render setting.

Copy link
Contributor

Choose a reason for hiding this comment

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

So as my guiding light for this kind of feature, I'm using the NVidia GL shader disk cache, which I think gets configured by environment variable or the magic nvidia control panel.

From Autodesk's perspective, what kind of control interface would you like for the cache? Env var? Something in C++ so you can put it in a settings panel? Plugin-based so people can configure weird site-specific multilevel caches or something? Curious to hear what the long term plans for something like this would be.

Comment on lines +133 to +135
bool val = false;
issValue >> val;
return VtValue(val);
Copy link
Contributor

Choose a reason for hiding this comment

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

These implementations mostly moved here from materialXFilter.cpp

Comment on lines +492 to +494
{ _tokens->name, JsValue(param.name) },
{ _tokens->type, JsValue(param.fallbackValue.GetTypeName()) },
{ _tokens->value, JsValue(osValue.str()) } };
Copy link
Contributor

Choose a reason for hiding this comment

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

Serializing parameters to the JSON file.

Comment on lines +224 to +227
HdRenderSettingDescriptor{
"Path to the directory of the persistent MaterialX codegen cache",
HdStRenderSettingsTokens->hdstMtlxCodegenCacheDirPath,
VtValue(HdSt_MaterialXShaderRegistry::GetCacheDirPathEnvSetting()) }
Copy link
Contributor

Choose a reason for hiding this comment

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

The cache directory exposed as a Storm render setting. The env var supplies the default value.

_HgiToResourceRegistryMap::GetInstance().GetOrCreateRegistry(
_hgi
#ifdef PXR_MATERIALX_SUPPORT_ENABLED
, cacheDirPath.c_str()
Copy link
Contributor

Choose a reason for hiding this comment

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

Passing the render setting to the registry.

Comment on lines +132 to +133
, _materialXShaderRegistry(
std::make_unique<HdSt_MaterialXShaderRegistry>(mtlxCacheDirPath))
Copy link
Contributor

Choose a reason for hiding this comment

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

The MaterialX shader registry is now owned by unique pointer to simplify include dependencies.

TfToken(name), value);
};

addFallbackParam("BoolParamTrue", VtValue(true));
Copy link
Contributor

Choose a reason for hiding this comment

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

Testing the serialization/deserialization of all supported data types.

std::move(textureParams));

std::stringstream ss;
codegenResult0.SaveMetadata(ss);
Copy link
Contributor

Choose a reason for hiding this comment

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

Serializing the codegen result we've just populated to a string.


JsValue jsMetadata = JsParseStream(ss);

HdSt_MaterialXCodegenResult codegenResult1(
Copy link
Contributor

Choose a reason for hiding this comment

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

Deserializing into a new codegen result object.

((stormMsaaSampleCount, "storm:msaaSampleCount"))

#ifdef PXR_MATERIALX_SUPPORT_ENABLED
#define HDST_MTLX_CODEGEN_CACHE_DIR_PATH_TOKEN (hdstMtlxCodegenCacheDirPath)
Copy link
Contributor

Choose a reason for hiding this comment

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

A token for the render setting's name.

@erikaharrison-adsk erikaharrison-adsk marked this pull request as ready for review June 4, 2025 15:41
@jesschimein
Copy link
Collaborator

Filed as internal issue #USD-11070

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

jstone-lucasfilm pushed a commit to AcademySoftwareFoundation/MaterialX that referenced this pull request Jun 18, 2025
When testing [parallel MaterialX codegen in Storm](PixarAnimationStudios/OpenUSD#3567), and comparing the generated code between test runs using a [persistent MaterialX cache](PixarAnimationStudios/OpenUSD#3661), I noticed that the results were varying around `float` literal formatting. It turned out to be due to a data race on `static` variables controlling the formatting settings.

This is similar to #2378 but limited to multithreaded codegen.
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.

Good stuff! I think the MaterialXCodeGenResult refactor is landable now, but left some comments about the disk-backed instance registry.

public:
friend class HdInstanceRegistryBase<VALUE, HdInstanceRegistry<VALUE>>;

void SaveToDisk(
Copy link
Contributor

Choose a reason for hiding this comment

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

We're anticipating the need for a more sophisticated management layer for the cache, and think that should live outside of hdSt. We have a big requirements list for it (including stuff like location on disk, size limit, eviction policy, security concerns, fingerprinting, hooks for testing); we don't expect you to hit all of that, but we also don't think the instance registry is a good place to park that complexity, since it's just a bit of memoization code.

If the control flow works out (which it looks like it does) I think the idea of the instance registry calling out to the persistent cache seems neat, though! But I wonder if there's a way to get that extensibility without all of the recurrent templates.

// MaterialX parameter Information
const auto* variable = paramsBlock[i];
const auto varType = HdStMaterialXHelpers::GetMxTypeDesc(variable);
for (HdSt_MaterialParam const& fallbackParam : fallbackParams) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome! :)

Comment on lines +1241 to +1258
// TfHashAppend hashes TfTokens not as strings, but as pointers to interned
// strings which are not stable from run to run
void _AppendPersistentHash(
Tf_HashState& hashState, TfToken const& token)
{
TfHashAppend(hashState, token.GetString());
}

// A VtValue may store a TfToken, in which case we need to convert it to a
// string in order to avoid the same pitfall as above
void _AppendPersistentHash(Tf_HashState& hashState, VtValue const& vtValue)
{
if (vtValue.IsHolding<TfToken>()) {
TfHashAppend(hashState, vtValue.Get<TfToken>().GetString());
} else {
TfHashAppend(hashState, vtValue.GetHash());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I want to dig around internally and see if we have a function that does this already. If not, I may try to promote something like this to pxr/base. I think there are definitely some other cases where the TfHash isn't what you want for a fingerprint, though I'm not sure whether you'd hit them in a material network traversal.

Comment on lines +27 to +28
TF_DEFINE_ENV_SETTING(HDST_MTLX_CODEGEN_CACHE_DIR_PATH, "",
"Path to the directory of the persistent MaterialX codegen cache");
Copy link
Contributor

Choose a reason for hiding this comment

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

So as my guiding light for this kind of feature, I'm using the NVidia GL shader disk cache, which I think gets configured by environment variable or the magic nvidia control panel.

From Autodesk's perspective, what kind of control interface would you like for the cache? Env var? Something in C++ so you can put it in a settings panel? Plugin-based so people can configure weird site-specific multilevel caches or something? Curious to hear what the long term plans for something like this would be.

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