Skip to content
Tom Cowland edited this page Jan 17, 2023 · 20 revisions

This wiki currently just serves as a staging space for notes and remarks to aid implementation.

Notes on USD

Resolver Resolution

As far as determining which resolver to use, the USD documentation is pretty straitforward.

In the USD implementation, a resolve call will look like this

ArGetResolver().Resolve("assetIdentifier")

ArGetResolver() (on first call), will attempt to provide an ArResolver subclass that satisfies the identifier.

  • First, via checking for URI scheme registrars
  • Then by using PreferredPrimaryResolver if it's been set
  • Then, but using the first custom primary resolver that's registered, sorted by typename
  • Then, the default resolver (which is just a path resolver)

Note

What's really happening behind the scenes here is that the first call the ArGetResolver instantiates all the resolvers that have been registered in the configuration json, including URI and primary resolver.

The ArResolver object that is returned from ArGetResolver() stores these objects via composition, and the Resolve() call itself selects the appropriate one to use, as it has a path to pattern match again at that point.

If any of the custom resolves fail, the resolve immediately falls back to using a default resolver.

URI resolvers, are pretty interesting, they're defined by setting a uriScheme when registering a custom resolver, it might be pleasant to have a custom OpenAssetIO uri scheme, something like this.

{
    "Plugins": [
        {
            "Info": {
                "Types": {
                    "OAIOResolver": {
                        "bases": ["ArResolver"],
                        "uriSchemes": ["openassetio", "oaio"]
                    }
                }
            },
            ...
        }
    ]
}

Then an asset might be referenced in a USD document as

#usda 1.0
(
    defaultPrim = "ACoolAsset"
)

def "ACoolAsset" (
   payload = @openassetio:ACoolAsset
)
{
}

Note

It's a tad ambiguous to me whether the preferred primary resolver, or a valid URI resolver, are given precendence. The docs here seem to indicate that a valid URI resolver will always be chosen first. However, the docs for ArSetPreferredResolver state that it allows users to override USDs plugin discovery entirely. I suspect URI resolvers are still given precedence, but it's something to check.

It may be important to note that whilst the USD documentation asserts that our resolver instance may be constructed at any time, generally the resultant object from ArGetResolver() is cached, so it won't neccesarily be constructed for each Resolve call into it. Seems to me like we'd want to make sure our resolver is stateless, and rely on USD caching as much as we can.

references :

Implementing a Plugin

Note

The best reference here is the example custom resolver plugin that USD provides.

Registration

To register a USD plugin, each plugin must define a pluginfo.json file.

The custom resolver can then be activated by setting the PXR_PLUGINPATH_NAME env variable to point to this file. This will cause all invocations of USD (including provided utilities such as usdview) to consider the custom resolver.

Note the path constants, ie, PLUG_INFO_LIBRARY_PATH. These are set during the USD project build process, see Private.cmake. At some point during the install, (final file in /usr/local/USD/share/usd/examples/plugin/usdResolverExample/resources), these paths are substituted to real paths.

...
"LibraryPath": "../usdResolverExample.so",
"Name": "usdResolverExample",
"ResourcePath": "resources",
"Root": "..",
"Type": "library"
...

Judging by file layout, the paths are relative to Root, and Root is relative to the location of pluginfo.json.

This is something to keep in mind when we're designing the install phase of this project, hopefully we can avoid too much manual setup on the part of the user.

Resolver Contexts

AR2 has the concept of a Resolver Context, which is very similar to our own context object. In fact, their object is intended to wrap other context objects, so hopefully it's an easy plug-in.

The documentation suggests that we may even be able to use our own context objects directly,

Clients may provide this data via context objects of their own (subject to restrictions below).

although i'm not certain how that would work given the signature of methods like BindContext(const ArResolverContext& context, VtValue* bindingData), which seem pretty opinionated about taking an ArResolverContext type.

Once a context is bound, it's retrieved in the resolve function via _GetCurrentContextObject()

When using a context, (ie, when one has defined the implementsContexts flag as true in pluginfo.json), an additional set of Context methods should be implemented.

Note

The docs imply that not all of these methods necessarily need be implemented

If a subclass indicates that it implements any of these functions, its plugin library will be loaded and these functions will be called the corresponding public ArResolver API is called. Otherwise, these functions will not be called.

Interface Methods

To implement the plugin, one derives from ArResolver

What follows is a table of all of the potential interface methods our plugin would need to implement. With a work in progress mapping to the relevant OpenAssetIO concept. It's clear that a working plugin does not necessarily need to implement all these methods, but it's not clear how to determine which ones do or don't need implemented at a glance.

Identifiers

ArResolver Method OpenAssetIO Concept
std::string _CreateIdentifier (const std::string &assetPath, const ArResolvedPath &anchorAssetPath=ArResolvedPath()) const Would call getRelatedReferences with a trait that specifies "alternate version for use" or pass through
std::string _CreateIdentifierForNewAsset (const std::string &assetPath, const ArResolvedPath &anchorAssetPath=ArResolvedPath()) const Would call getRelatedReferences with a trait that specifies "alternate version for use"

Path Resolution Operations

ArResolver Method OpenAssetIO Concept
ArResolvedPath _Resolve (const std::string &assetPath) const finalizedEntityVersion, return entity ref
ArResolvedPath _ResolveForNewAsset (const std::string &assetPath) const finalizedEntityVersion, return entity ref

Scoped Resolution Cache

ArResolver Method OpenAssetIO Concept
void _BeginCacheScope (VtValue *cacheScopeData) Base Implementation Sufficient
void _EndCacheScope (VtValue *cacheScopeData) Base Implementation Sufficient

Asset Operations

ArResolver Method OpenAssetIO Concept
std::string _GetExtension (const std::string &assetPath) const Resolve locatableContent and get path ext
ArAssetInfo _GetAssetInfo (const std::string &assetPath, const ArResolvedPath &resolvedPath) const entityName, entyitVersion, resolve with custom entity info trait
ArTimestamp _GetModificationTimestamp (const std::string &assetPath, const ArResolvedPath &resolvedPath) const resolve a media creation trait if supported via managementPolicy
std::shared_ptr< ArAsset > _OpenAsset (const ArResolvedPath &resolvedPath) const =0 resolve locatableContent trait, construct a ArFilesystemAsset
bool _CanWriteAssetToPath (const ArResolvedPath &resolvedPath, std::string *whyNot) const TBC: permissions API
std::shared_ptr< ArWritableAsset > _OpenAssetForWrite (const ArResolvedPath &resolvedPath, WriteMode writeMode) const =0 preflight + resolve + return ArWritableAsset (write context), Add custom ArtAsset class to handle register in close, TBC: writemode

Context Operations Seems easily (conceptually) mappable to our context/locale traits so can be figured out later.

ArResolver Method OpenAssetIO Concept
void _BindContext (const ArResolverContext &context, VtValue *bindingData) Base Implementation Sufficient
void _UnbindContext (const ArResolverContext &context, VtValue *bindingData) Base Implementation Sufficient
ArResolverContext _CreateDefaultContext () const
ArResolverContext _CreateDefaultContextForAsset (const std::string &assetPath) const This could be a useful OpenAssetIO addition
ArResolverContext _CreateContextFromString (const std::string &contextStr) const
void RefreshContext_ (const ArResolverContext &context)
ArResolverContext _GetCurrentContext () const
bool _IsContextDependentPath (const std::string &assetPath) const This would need to be implemented to ask the manager as we assume entity references are opaque (resolve with custom trait)

Note how these are all protected methods. You'll see in the docs they all, (near enough, some are used to support multiple frontends,) have public doppelgangers. This is due to the Non-virtual interface pattern that USD follows heavily. You can see how in the example, only these implementation methods have been implemented, with the public frontends being provided by the base.

Speaking of the example, it only implements the following methods, which proves we don't need to implement all of this, the base implementation may be just fine for some of it, (BindContext in particular catches my eye by its absense.)

- _CreateIdentifier
- _CreateIdentifierForNewAsset
- _Resolve
- _ResolveForNewAsset
- _CreateDefaultContext
- _CreateDefaultContextForAsset
- _CreateContextFromString
- _IsContextDependentPath
- _RefreshContext
- _GetModificationTimestamp
- _GetAssetInfo
- _OpenAsset
- _OpenAssetForWrite

When is the resolver invoked?

The clearest piece of documentation I've come across on this is from the URI resolver section :

When Ar encounters an asset path or resolved path of the form ":...", it will check if any ArResolver subclasses have been registered for the scheme. If so, it will dispatch the path to that subclass for handling. If not, it will dispatch the path to the primary resolver for handling.

However, this is not very clear. The real question here is, does AR perform a deep resolve when it resolves references to other USD documents, causing a lot of redundant calls into a custom resolver as recursive structures get unfolded.

I get the sense it doesn't, and will bypass even calling through to the resolver redundantly if there's a correctly configured cache. However, this is something we should check up on with our first prototype, with both cached and non-cached formulations.

Performance

USD does some planning to avoid resolving the same reference more than once.

From AR2 docs, referring to ARResolverScopedCache

Resolving asset paths may be expensive and a UsdStage may need to resolve hundreds to thousands of asset paths (or more) depending on the complexity of the scene, many of which may be repeated. A scoped cache helps to minimize that expense. A scoped cache also ensures that resolving a given asset path multiple times returns the same result, which is important for consistency and correctness.

I suspect we can assume most implementations will use this, but needs more research. To implement this, we'll need to perform some scoped caching ourselves in the resolver. AR2 will instruct us of the scope via BeginCacheScope and EndCacheScope. These have perfectly good default implementations, but if we want to get a bit more clever with caching, the option is open.