run-validators — Validate and auto-fix a USD asset
Runs the usd-validation-nvidia engine over a USD stage through
tools/validators/run.sh (POSIX) or run.bat (Windows). The wrapper
registers the UsdOptimize* performance rules and then delegates to the
official usd_validation_nvidia CLI entry point, so one run covers both
the Usd Optimize rules and the base usd_validation_nvidia rules.
The validators are reference-documented in
docs/performance-validators.rst —
that is the source of truth for the rule list and what each rule checks. Many
rules ship a fix that usd-validation-nvidia's IssueFixer can apply
automatically; pass --fix to enable it.
What this skill covers
- Usage — flags and positional args.
- Step 1 — validate the input path.
- Step 2 — invoke the driver, including
--processfor parallel validation on large scenes. - Step 3 — summarize results and hand off.
- Errors to handle — failure modes.
- Auto-fix model — how
IssueFixer/--fixapplies fixes, where it writes, and what it cannot. - Programmatic invocation —
register_all()+ValidationEngine. - CLI invocation —
nvidia_usd_validatesetup, the wheel/entry-point requirement, and thelibusdalignment gotcha. - Adding a new validator — pointer to the
new-validatorskill.
Companion skills: interpret-validators (read the report and decide what to do
about issues that could not be auto-fixed), run-operations (run
operations / preset configs when a manual fix is needed).
Usage
One positional argument (the asset) plus optional flags:
| Flag | Meaning |
|---|---|
ASSET |
Required. .usd / .usda / .usdc / .usdz, or a folder of them (read-only runs only — see Limitations). |
-f / --fix |
Opt-in. Run IssueFixer on every fixable issue. Non-destructive by default: writes fixes to a new file (<stem>.fixed<ext>, or --fix-output); the source is untouched unless --fix-in-place (or USD_OPTIMIZE_FIX_IN_PLACE=1) is set. |
--fix-output <PATH> |
Where --fix writes the fixed stage (default <stem>.fixed<ext> beside the source). |
--fix-in-place |
Make --fix overwrite the source (destructive — confirm first). Env: USD_OPTIMIZE_FIX_IN_PLACE=1. |
--csv-output <CSV> |
Write a per-issue CSV (all rules). |
--json-output <JSON> |
Write the full validation result as JSON (rules[].issues[]). Carries the same issues as the CSV; summarize_csv.py reads either. |
-r RULE |
Enable only a specific rule (repeatable). |
-D RULE |
Disable a specific rule (repeatable). |
-c CATEGORY |
Enable only a specific category (repeatable). |
--parameter NAME=VALUE |
Override a rule parameter (repeatable). |
-v / --verbose |
Upstream's verbosity flag. Raises upstream's log level and makes count-only rules emit one issue per failing prim (see below). Repeatable — -vv for DEBUG logging. |
-p PREDICATE |
Filter output by severity: IsError, IsFailure, IsWarning. |
--group-by requirement|rule_name |
Group output by requirement or rule. |
Run tools/validators/run.sh --help for the full flag list. -v / --verbose
is upstream's own flag: the wrapper reads it without consuming it, so one flag
raises upstream's log level and turns on per-prim reporting.
Verbose per-prim reporting
By default several rules report a single aggregate issue with a count (e.g.
"Found 2 nonManifold meshes to fix") anchored at the stage root, so the per-prim
paths never reach the CSV Location column. Verbose mode makes those rules also
emit one issue per failing prim (full path in Location), which is what you want
for CLI/batch debugging. The aggregate summary is still emitted.
Three ways to enable it, all read at validation time so they work no
matter how the rules are driven (our wrapper, the upstream CLI, or a direct
ValidationEngine):
tools/validators/run.{sh,bat} asset.usd -v(or--verbose) — this is upstream's verbosity flag, so it also raises upstream's log level to INFO (-vvfor DEBUG). The other two forms change reporting only.USD_OPTIMIZE_VALIDATOR_VERBOSE=1in the environment--parameter VERBOSE=true(or--parameter <RuleName>.VERBOSE=true), advertised through the standard usd-validation-nvidia parameter system; or programmaticallyusd_optimize.validators.set_verbose(True).
Rules that already emit per-prim issues (e.g. SmallMeshChecker,
InvisiblePrimsChecker, WindingsChecker) are unaffected by the flag.
If no path is provided, ask:
"Which USD file should I validate? Provide the full path."
Step 1 — Validate the input
Reject a path that does not exist or is not a .usd* file before doing
anything else.
Step 2 — Invoke the driver
Use the wrapper for the host OS. It sets LD_LIBRARY_PATH / PATH and
PYTHONPATH for the build's bundled Python, registers the Usd Optimize
rules, and delegates to the usd_validation_nvidia CLI. Always go through
the wrapper — invoking the bundled interpreter directly inherits the user's
PYTHONPATH and fails with USD-version mismatches.
Validation is read-only by default — add --fix only when the user asks for
repairs. --fix is non-destructive: it writes fixes to a new file
(<stem>.fixed<ext>, or --fix-output) and leaves the source untouched. Pass
--csv-output to save a per-issue CSV for later analysis with
interpret-validators.
Get the artifact directory from resolve_artifacts.py — don't invent one.
The driver has no default --csv-output, so artifacts land wherever you put
them. interpret-validators looks them up at a path derived from the asset, so
writing anywhere else makes its replay lookup report missing and every asset
re-validates from scratch. Pass --logs-dir to both scripts if the user wants a
specific location.
POSIX (validate read-only, then opt into --fix to repair into a new file):
ASSET="<path/to/asset.usd>"
ARTIFACT_DIR=$(python3 tools/validators/resolve_artifacts.py "$ASSET" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["artifact_dir"])')
CSV="$ARTIFACT_DIR/issues.csv"
mkdir -p "$ARTIFACT_DIR"
# validate (read-only)
tools/validators/run.sh "$ASSET" --csv-output "$CSV" > "$ARTIFACT_DIR/run.log" 2>&1
# to repair: --fix writes fixes to a new file (<stem>.fixed<ext>); $ASSET is kept
tools/validators/run.sh "$ASSET" --fix --csv-output "$CSV" >> "$ARTIFACT_DIR/run.log" 2>&1
Windows (PowerShell):
$Asset = "<path\to\asset.usd>"
$ArtifactDir = (& py -3 tools\validators\resolve_artifacts.py $Asset | ConvertFrom-Json).artifact_dir
$Csv = Join-Path $ArtifactDir "issues.csv"
New-Item -ItemType Directory -Force -Path $ArtifactDir | Out-Null
# validate (read-only)
& tools\validators\run.bat $Asset --csv-output $Csv *> (Join-Path $ArtifactDir "run.log")
# to repair: --fix writes fixes to a new file (<stem>.fixed<ext>); $Asset is kept
& tools\validators\run.bat $Asset --fix --csv-output $Csv *>> (Join-Path $ArtifactDir "run.log")
Redirect to a log file rather than piping through tail (pipes buffer until
the process exits).
Output location: --fix writes to a new file by default — --fix-output <path>, or <stem>.fixed<ext> beside the source — so the original is preserved.
--fix-in-place (or USD_OPTIMIZE_FIX_IN_PLACE=1) overwrites the source
instead; it's destructive, so confirm with the user before using it.
Long-running execution
Validation on real assets takes minutes (occlusion and overlap checks are the slowest). Launch the driver as a long-running command and return control:
- Claude Code: set
run_in_background: trueon the Bash call. - Generic shells:
nohup ... &(POSIX) orStart-Process ... -NoNewWindow(PowerShell), then read bounded snapshots ofrun.log.
Tell the user it is running and that you will report results when done.
For a status snapshot: tail -n 80 "<artifact_dir>/run.log".
Parallel validation on large scenes (--process)
usd-validation-nvidia runs rules across a process pool when given
--process COUNT; the driver passes unknown flags straight through, so it
works with no extra tooling:
tools/validators/run.sh "$ASSET" --process 4 \
--no-defaultRules -c Usd:Performance -c Omni:Geometry --csv-output "$CSV"
Start at 4 and only raise it if you have headroom to spare. Each worker opens its own copy of the stage, so memory — not core count — is the binding constraint, and upstream caps workers with a flat count that knows nothing about available RAM. Worker count saturates early, partly because the engine is already internally threaded, so past ~4 the extra copies rarely pay for themselves.
Only rules marked @multiprocess_safe are pooled — every Usd Optimize rule is,
via BaseUsdOptimizeChecker.CheckStage. Unmarked rules still run inline, so
coverage is unchanged; the scope flags above are what make the pooling pay off.
Expect a modest win, and always pair --process with a scope flag. The
speedup is set by how much of the enabled rule set is poolable: the Usd Optimize
rules are all marked, but almost none of the base rules are, so a default run
leaves the base rules — which dominate the wall clock — inline and gains close to
nothing. It also varies by stage, since one long-pole rule cannot be subdivided
across workers. Findings are identical either way.
--processis upstream-experimental and defaults to0(disabled). Combining it with--fixis untested —IssueFixermutates a single in-memory stage, so use the serial path when you need a repaired asset.On Linux the pool forks — upstream omits
mp_context, so the platform default applies (fork on Linux, spawn on Windows). If the parent process has already initialized CUDA, a forked worker inherits a dead context and GPU-backed rules fail withCUDA error: initialization error, surfacing as FAILURE issues instead of findings.isCudaAvailable()caches behindstd::call_once, so the worker returns the parent's stale verdict and the CPU fallback never fires. Verified onfindOverlappingMeshes; rules with the same guard (e.g.deduplicateGeometry) are exposed the same way. A plain one-shot CLI run does not trip it, because the parent never touches CUDA before forking — but any process that does GPU work and then validates with--processwill.Spawned workers re-import the rule modules and see them as written, so rule tuning applied by assigning to a class attribute in the driving process never reaches them (verified: a threshold lowered in-process gives 4 issues inline, 1 pooled).
--verboseworks that way — do not rely on it under--process.
Step 3 — Summarize and hand off
When the run finishes, show the last ~40 lines of run.log (the per-rule issue
counts and the severity summary) and append:
Validation complete.
Artifacts: <artifact_dir>/
issues.csv — every issue, one row per rule hit
run.log — full driver output
<N> issues found. <M> were auto-fixed (--fix).
Where those two numbers come from. The driver prints no aggregate fix
line, and the Summary per Severity: block at the end of the log is the
pre-fix count — byte-identical whether or not --fix ran. So take <N>
from that summary, and get <M> by counting the per-issue statuses the fixer
logs individually as it works (<message>........FixStatus.SUCCESS):
grep -c 'FixStatus.SUCCESS' "<artifact_dir>/run.log"
Most issues are fixed automatically. What remains needs a decision (which prims, how aggressive, an accepted trade-off). For those, hand off:
For the remaining issues, run
/interpret-validators <asset>— they typically need a decision before an operation or preset config can address them.
If --fix was used, re-validate the file the fixes landed in — the
--fix-output path (<stem>.fixed<ext> by default), or the source itself for
--fix-in-place — to confirm the targeted rules dropped:
tools/validators/run.sh "<the file you fixed>" --csv-output "$CSV"
Don't interpret the remaining issues here — that's interpret-validators.
Errors to handle
| Symptom | Cause | What to tell the user |
|---|---|---|
Build not found at _build/... |
The repo isn't built | Point at the build skill; build first. |
Driver exits 1 and the log ends with a Summary per Severity: block |
Normal — the run completed and found at least one issue. Warnings alone are enough; only a genuinely clean asset exits 0. | Not a failure. The CSV is complete — parse it as usual. |
| Driver exits 2 | A wrapper flag guard rejected the command (e.g. --fix with no usable asset) |
The error: line on stderr names the problem; fix the command and re-run. |
| Driver exits non-zero with no severity summary in the log | USD open error or plugin import error | Surface the last lines of run.log; don't parse a partial CSV. |
usd-validation-nvidia install fails in the wrapper |
First-run pip install behind a proxy | Set HTTPS_PROXY / HTTP_PROXY and re-run. |
| 0 Usd Optimize issues but base-rule issues present | The asset has no UsdGeomMesh content (references-only / materials-library / layout stage), so mesh rules find nothing |
Expected — not a registration failure. Confirm with inspect-asset if unsure. |
If the repo isn't built, don't build it yourself — point at the build
skill and stop.
Auto-fix model
Each rule reports issues; many also attach a suggestion describing the
operation that would fix the issue. usd-validation-nvidia's IssueFixer
applies those suggestions when the user opts into --fix. When opted in, most
issues are resolved without any manual step.
What IssueFixer cannot resolve automatically is any issue whose fix needs
a decision — which prims to act on, how aggressive to be, or a trade-off the
user must accept (e.g. decimation amount, whether to merge instanced meshes).
Those remain after --fix and are the input to interpret-validators.
Programmatic invocation
from usd_optimize.validators import register_all
from usd_validation_nvidia import ValidationEngine, IssueFixer
from pxr import Usd
register_all() # register every Usd Optimize rule
stage = Usd.Stage.Open("asset.usd")
results = ValidationEngine().validate(stage)
# example of printing the issues
for issue in results.issues():
print(issue.severity, issue.rule.__name__, issue.message)
# Apply the auto-fixable suggestions
fix_results = IssueFixer(stage).fix(results.issues())
stage.Export("asset.fixed.usdc")
register_all() is idempotent.
CLI invocation (nvidia_usd_validate)
The upstream nvidia_usd_validate CLI can also drive the rules, but it
discovers plugins via importlib.metadata entry points, so it only sees Usd
Optimize once the usd-optimize wheel is pip-installed (a source-tree
PYTHONPATH registers no entry-point metadata). There is no allow-list env
var — once the wheel is installed the registrant entry point auto-loads.
./repo.sh build && ./repo.sh py_package # produces _build/packages/usd_optimize-*.whl
python3.12 -m pip install _build/packages/usd_optimize-*.whl
nvidia_usd_validate asset.usd
nvidia_usd_validate --help | grep UsdOptimize # verify rules are discovered
libusd alignment gotcha. pxr (often from usd-exchange) and Usd
Optimize's C++ core must bind the same libusd image, or UsdUtilsStageCache
splits and the validator errors with missing stage IDs. The run.sh wrapper
aligns PYTHONPATH / LD_LIBRARY_PATH automatically; the raw CLI does not. If
you hit stage-id / cache errors on a dev-tree CLI run, export the build's USD
first:
export PYTHONPATH=_build/$platform/release/python:_build/target-deps/usd/release/lib/python:$PYTHONPATH
export LD_LIBRARY_PATH=_build/$platform/release/lib:_build/$platform/release/extraLibs:$LD_LIBRARY_PATH
Note: the entry-point plugin registers the default rule set; to run a rule that
is not in the default set, register it programmatically with register_all()
first.
Adding a new validator
See new-validator for the full recipe.
Prerequisites
- A built repo (
./repo.sh build/repo.bat build) so the wrappers and bundled Python exist. - A USD asset (
.usd/.usda/.usdc/.usdz). - Network access on first run (the wrapper pip-installs
usd-validation-nvidiainto the bundled Python).
Limitations
--fixis opt-in and writes to a new file by default (--fix-in-placeto overwrite the source); it does not decide how to resolve issues that have no automatic fix — that'sinterpret-validators.--fixneeds a single file, not a folder. Upstream accepts a folder asASSETand read-only validation of one works, but--fixhas to copy the source to protect it, so a folder exits 2 with--fix requested but no .usd/.usda/.usdc/.usdz asset was found to protect. Pass files individually, or use--fix-in-placeon the folder.- Occlusion / overlap rules can take tens of minutes to potential hours on large stages.
- The raw
nvidia_usd_validateCLI needs the wheel installed and may need thelibusdalignment exports above.