Configuration Contract#
The resolver supports both standalone OpenUSD usage and embedded application usage. Configuration is explicit so that multiple stages, jobs, or tests can run without accidentally sharing unsafe global state.
Runtime Properties#
Local USD files work without ovstorage.
Remote USD identifiers require ovstorage and fail clearly when ovstorage is missing or misconfigured.
The product API must not require a process-global raw library handle.
Host applications can configure resolver behavior before opening remote USD.
Diagnostics can be run before the first remote OpenUSD operation.
Materialized-file lifetime is owned by the storage bridge; the resolver keeps only the bridge-supplied lease and drops it when the caller is done.
Configuration Inputs#
Hosts configure the resolver by building a ResolverOptions value (declared in
include/ovstorage_usd_resolver/configuration.hpp) and passing it to
createResolverRuntime(). Its fields are:
remoteUriSchemes— the URI schemes this runtime treats as remote (must be a subset of what the build advertises viaOVS_USD_RESOLVER_URI_SCHEMES; narrowing is enforced by identifier creation, resolution, metadata, open, and write paths; widening is rejected at validation time). Disabled schemes fail closed before the runtime calls its storage bridge.isolationKey— a per-stage/per-job label hosts may use for their own bookkeeping. Not a security boundary and not used to partition any resolver-owned state.storageBridgeFactory— host-provided factory invoked once when the runtime builds its storage bridge. The default (unavailableStorageBridgeFactory) returns a bridge that reports every operation as unavailable, so a missing or mis-wired factory produces a resolver-owned diagnostic instead of a crash or hidden fallback.traceSink— optional callback invoked at selected resolver/runtime operation boundaries and raw-input points. Before invocation, identifiers lose URI userinfo and sensitive query, fragment, and SDF-argument values; URI-shaped tokens in status/detail messages receive the same treatment. The emitted event families arecreate_identifier.*,resolve.*,resolve_for_new_asset.*,open_asset.*(including aopen_asset.raw_inputfor the pre-normalization identifier handed by OpenUSD),stat.*, andwrite.*, plusmodification_timestamp.raw_inputandasset_info.raw_inputon the metadata paths. Compatibility probing additionally emitsresolve_existence_cache.{hit,miss,wait,coalesced,invalidate}. Hosts own sink synchronization; the resolver may invoke it from multiple threads.resolveVerifiesExistence- when true,_Resolve()stats a claimed remote object through the bound runtime before returning it.Successreturns the normalized path, authoritativeNotFoundreturns empty, and all other failures preserve the path so later IO can report the real auth, routing, or backend error. One exception: an identifier whose path names a UDIM tile set (<UDIM>,<UVTILE0>or<UVTILE1>, each delimiter raw or percent-encoded) is a set of objects, not one, so it resolves to itself without a stat, whether or not any tile exists; the consumer that expands the tiles checks them. A nonemptyResolve()therefore does not prove that a tile set has tiles. Concrete tiles, and tags outside the path (SDF format arguments, a package’s inner path, the URI’s query or fragment), keep the check. See D-0010. The generic default is false. Legacy-replacement hosts enable it when their clients use a nonemptyResolve()result as an existence signal.resolveExistencePositiveTtlandresolveExistenceNegativeTtl- retention periods for successful and authoritative missing results. Defaults are five seconds and one second. Zero disables retention for that result class; negative durations fail option validation.resolveExistenceCacheMaxEntries- hard per-runtime metadata-entry bound, default 4096. Least-recently-used entries are evicted at the bound. It must be nonzero when existence verification is enabled.
Provider selection, credentials, endpoint configuration, and read-policy knobs
(range-read strategy, chunk sizes, retries) live on the ovstorage side — either
in the host-built LayerHandle handed to OvStorageBridgeOptions::layerHandle,
or in the stack configuration file pointed at by
OvStorageBridgeOptions::configPath. The resolver has no view into them and no
duplicate resolver-side knobs.
The declarative construction path creates an ovstorage Registry, asks
StackConfig to parse and build the graph, and does not inspect provider kinds
or URI schemes. Ovstorage seeds that registry with only the built-in file
provider. If the TOML names plugin-provided kinds, the host sets
OvStorageBridgeOptions::pluginDirectory to an explicitly trusted directory
of native provider libraries: libovstorage_plugin_*.so on Linux,
ovstorage_plugin_*.dll on Windows, or libovstorage_plugin_*.dylib on macOS. The bridge registers every matching
plugin and keeps it alive until after the built LayerHandle is destroyed.
Standalone CLI commands map OVSTORAGE_PLUGIN_DIR to this field. Hosts that
need an exact plugin allowlist build their own registry and LayerHandle, then
use the host-handle path.
configPath and loadDefaultConfig are mutually exclusive. The latter uses
ovstorage’s documented search order (./ovstorage.toml, then the XDG user
config path); the CLI selects it when --stack-config is omitted.
Hugging Face read-only example#
The shipping scheme list includes hf, but registration is not provider
configuration. The host still supplies an ovstorage connection for each Hub
repository it wants to expose. This minimal StackConfig mounts one immutable
dataset revision through ovstorage’s Hugging Face plugin:
[ovstorage]
root = "huggingface"
[ovstorage.layers.huggingface]
[[ovstorage.connections]]
backend_kind = "huggingface"
config = { repo_id = "namespace/repository", repo_type = "dataset", revision = "<commit-sha>" }
With the trusted ovstorage plugin directory configured, OpenUSD consumers use
logical addresses such as
hf://datasets/namespace/repository/path/to/root.usd. The resolver neither
turns these addresses into HTTP URLs nor implements Hugging Face behavior;
ovstorage owns Hub authentication, routing, and CDN redirects. The shipped
provider is read-only. MDL, texture, and other non-USD consumers in a host need
their own hf:// integration because this resolver handles USD layers and
packages only.
There is no resolver-owned materialization cache: no cache root, no cache
budget, no LRU. Every materialize call returns a MaterializedAsset lease
over an on-disk file the concrete storage bridge placed (and, for the shipped
ovstorage bridge, the ovstorage runtime GCs). Hosts that want a global
materialization-size budget configure it on the ovstorage layer.
The optional Resolve-existence cache is separate metadata-only state. It is
owned by one ResolverRuntime, coalesces concurrent first probes for the same
provider key, and stores only success and authoritative NotFound. A
successful resolver write invalidates the affected key, including suppression
of an older probe that was still in flight. External changes become visible
after the configured TTL. Package inner paths and SDF format arguments share
the outer provider-object key.
createResolverRuntime() is the core runtime hook. It validates
ResolverOptions, constructs the configured bridge, and returns an explicit
runtime handle. Multiple runtime handles can coexist in one process.
OpenUSD callers bind explicit runtime handles through ArResolverContext with
makeOpenUsdResolverContext(). The resolver’s _OpenAsset path requires that
bound context for remote reads and fails closed when no resolver context is
bound. The bound runtime also determines the active scheme subset, so different
contexts in one process can enable different portions of the compiled list and
can disagree about the existence of the same URI without cache leakage.
Standalone Usage#
Source consumers follow the build guide for the supported host SDK layout and ovstorage 0.3.x. No resolver wheels are published for this release. An optional locally built Python package provides explicit registration and runtime binding for the pinned SDK. For a source build:
point
PXR_PLUGINPATH_NAMEat the build tree’splugInfo.json;run
doctorto validate CLI setup and ovstorage bridge availability;open a remote USD through OpenUSD;
run
materializeortracefor debugging.
No renderer or OV application is required for this path.
Embedded Usage#
Embedded applications configure the resolver before the first OpenUSD resolver
selection. In particular, they do not select or replace
the resolver after OpenUSD has already initialized ArGetResolver().
For remote asset reads, embedded applications:
build a
ResolverOptionsvalue;enable
resolveVerifiesExistencewhen replacing a legacy resolver contract that treats nonemptyResolve()as proof of existence;call
createResolverRuntime();create an OpenUSD resolver context with
makeOpenUsdResolverContext();pass that context to
UsdStage::Open(path, context)when opening a stage;bind that context with OpenUSD’s thread-local
ArResolverContextBinderon each thread that performs lower-level direct resolver or asset operations.
Passing the context into UsdStage::Open is the preferred stage-composition
contract. It keeps the ovstorage runtime available for sublayers, references,
payloads, and package-internal assets that OpenUSD resolves after the root
layer starts loading.
setDefaultResolverRuntime registers a process-wide runtime choice used when
creating a default context; it does not bind that context across threads.
ScopedDefaultContext wraps the same thread-local binder, so create one around
OpenUSD work on every worker thread or pass the context explicitly.
Ownership is divided as follows:
the host owns the ovstorage context or configuration;
the host provides the bridge factory before remote USD use;
the resolver owns OpenUSD identifier and asset behavior;
ovstorage owns provider-specific IO and owns the on-disk materialized bytes (the resolver holds a bridge-supplied
LocalLeaseHandle, not a byte cache);the host owns application-specific non-USD asset IO.
Bridge Contract#
The resolver calls ovstorage through the narrow, fake-testable
StorageBridge interface. It exposes:
whole-object and range reads;
stat and object metadata;
bridge-owned local materialization leases;
whole-object writes with explicit destination preconditions;
aggregate and per-address capability checks;
per-address owning-target lookup for adapter diagnostics; and
normalized storage status and error codes.
The bridge contains no:
provider SDK calls;
hidden omniclient calls;
private endpoint assumptions;
process-global raw handles as the product configuration API.
Materialization directory permissions#
The concrete bridge creates lease files atomically in the system temporary directory and removes them when the lease ends. POSIX files have mode 0600. Windows files inherit the temporary directory’s DACL; CRT read/write flags do not create an owner-only ACL. Windows hosts must select a per-user temporary directory with access limited to the application identity and trusted administrators before starting the process. A shared writable/readable TEMP directory does not provide confidentiality for materialized assets.