Authoring rules#

usd-validation-nvidia is rule-based: each check is a class that inspects a Usd.Stage (or its layers, prims, or package contents) and reports Issue objects. This page shows how to write, register, and distribute your own rules.

The rule interface#

A rule subclasses BaseRuleChecker and overrides one or more Check* callbacks. The engine invokes them as it walks an asset, so a rule only implements the callbacks it needs:

Callback

Invoked with

CheckStage(usdStage)

The composed Usd.Stage (whole-stage checks).

CheckPrim(prim)

Each Usd.Prim on the stage.

CheckLayer(layer)

Each Sdf.Layer used by the asset.

CheckDependencies(usdStage, layerDeps, assetDeps)

The stage together with its layer and asset dependencies.

CheckUnresolvedPaths(unresolvedPaths)

Asset paths that did not resolve.

CheckDiagnostics(diagnostics)

Diagnostics emitted while opening/composing the stage.

CheckZipFile(zipFile, packagePath)

Each Usd.ZipFile for .usdz packages.

CheckFormatDependency(dependency)

Each format-specific dependency (a FormatDependency).

ResetCaches()

Hook to clear any per-asset state the rule caches between runs.

Report findings from inside a callback with the helper methods, in increasing severity:

  • self._AddInfo(message, ...) - informational only.

  • self._AddWarning(message, ...) - a non-blocking concern.

  • self._AddFailedCheck(message, ...) - a normative failure.

  • self._AddError(message, ...) - the rule itself could not run.

Each accepts an at= location (a prim, property, layer, or other Identifier), an optional code, and an optional requirement. _AddFailedCheck, _AddError, and _AddWarning also take one or more suggestions used for auto-fixing (see below); _AddInfo does not.

Registering a rule#

Decorate the class with register_rule() and a category name. Registered rules become part of every default ValidationEngine:

from pxr import UsdGeom
from usd_validation_nvidia import BaseRuleChecker, register_rule


@register_rule("MyStudio")
class VisibilityAttributeChecker(BaseRuleChecker):
    """Meshes should author a 'visibility' attribute."""

    def CheckPrim(self, prim):
        if not prim.IsA(UsdGeom.Mesh):
            return
        if not prim.GetAttribute("visibility").IsValid():
            self._AddWarning(
                message=f"Mesh <{prim.GetPath()}> does not author 'visibility'.",
                at=prim,
            )

Run it like any other rule; an engine created with no arguments discovers all registered rules:

from usd_validation_nvidia import ValidationEngine

engine = ValidationEngine()
results = engine.validate("asset.usda")

register_rule also accepts skip= (register conditionally, for example in tests) and overwrite= (replace an existing rule in a category). Categories group related rules: the CLI’s --category flag and register_rule use them. To turn individual rules on or off on an engine, use enable_rule() / disable_rule().

Providing an auto-fix#

Attach a Suggestion to an issue to make it fixable. IssueFixer applies the suggestion to the stage and writes the result back:

from usd_validation_nvidia import IssueFixer

fixer = IssueFixer("asset.usda")
fixer.fix(results.issues())
fixer.save()

See Python API for the Suggestion, IssueFixer, and Issue signatures.

Wrapping a native USD validator#

OpenUSD ships its own UsdValidation validators. Rather than reimplement one, subclass UsdValidatorAdapter and name the validator to expose it as a rule:

from usd_validation_nvidia import register_rule, UsdValidatorAdapter


@register_rule("Basic", skip="usdUtilsValidators:UsdzPackageValidator" not in UsdValidatorAdapter)
class UsdzPackageValidator(UsdValidatorAdapter):
    @classmethod
    def validator_name(cls):
        return "usdUtilsValidators:UsdzPackageValidator"

Running a callback in a process pool#

By default every rule callback runs in the calling process. Decorate a callback with multiprocess_safe() to declare it eligible for the process pool that --process COUNT enables:

from usd_validation_nvidia import BaseRuleChecker, multiprocess_safe, register_rule


@register_rule("Basic")
class MyLayerRule(BaseRuleChecker):
    @multiprocess_safe
    def CheckLayer(self, layer):
        ...

The decorator is a declaration, not a request: marked callbacks run in the pool only when process execution is enabled, and behave exactly as before when it is not. This feature is experimental.

A callback is only safe to mark if it holds to both of these:

  • Its arguments and results survive pickling. Work crosses a process boundary, so anything the callback receives or reports must be picklable. Validation identifiers and layer compliance events already are.

  • It shares no mutable state with the parent process. Accumulating results on self across calls does not work — a worker mutates its own copy, and the parent never sees it. Report findings through the usual issue helpers instead.

CheckStage and CheckPrim callbacks that traverse a live stage are generally not safe to mark, because the stage does not cross the process boundary.

Distributing rules as a plugin#

Rules register when their module is imported. To ship rules in your own package so they load automatically, expose a plugin through the usd_validation_nvidia entry-point group; the plugin manager discovers it at startup:

# pyproject.toml of your rule package
[project.entry-points."usd_validation_nvidia"]
my_studio_rules = "my_studio_rules:Plugin"

The referenced object implements PluginProtocol: an on_startup() hook, where you import or register your rule modules, and an on_shutdown() hook. DefaultPlugin is the built-in reference implementation. The legacy omni.asset_validator group is also scanned for backward compatibility, but new plugins should use usd_validation_nvidia.

Loading authored specs and loose rules#

For applications that load authored profile/specification content at runtime, use usd_profiles_nvidia.SpecificationsLoader to parse the content and register_content() to register its requirement definitions, capabilities, features, and profiles. Loose Python validator modules have a separate lifecycle token from register_validation_modules():

from usd_profiles_nvidia import SpecificationsLoader
from usd_validation_nvidia import (
    register_content,
    register_validation_modules,
    unregister_content,
    unregister_validation_modules,
)

content = SpecificationsLoader(
    root_dir="specs",
    reverse_domain="com.example.assets",
).load()

registered_modules = None
try:
    register_content(content)
    registered_modules = register_validation_modules(["specs/capabilities/geometry/validation.py"])
    ...
finally:
    if registered_modules is not None:
        unregister_validation_modules(registered_modules)
    unregister_content(content)

The loaded content object is the lifecycle token for documentation/specification content. The registered_modules token owns imported rule modules, so reload and shutdown code can unregister exactly what it previously loaded.