Repository navigation
Relative asset paths with \ separators resolve inside a .usdz but not as loose files #149
Description
Summary
On Linux, DefaultResolver keeps \ as part of a file name when it anchors a loose
relative asset path. A package-relative path is split on both / and \. The same
Windows-authored layer therefore resolves when it is packaged as a .usdz and fails
when it sits beside its files, for asset attributes and for composition arcs alike.
Reproduction
Checked against main at 933aa8f, on Linux. tex/stone.png and sub/layer.usda
(which defines /S with int x = 7) sit beside each layer.
| authored in a loose layer | result |
|---|---|
asset f = @tex/stone.png@ |
resolved_path() == Some(…/tex/stone.png) |
asset f = @tex\stone.png@ |
resolved_path() == None |
subLayers = [@sub/layer.usda@] |
/S.x == 7 |
subLayers = [@sub\layer.usda@] |
/S is absent; Stage::open succeeds |
references = @sub/layer.usda@</S> |
/R.x == 7 |
references = @sub\layer.usda@</S> |
/R.x is absent; Stage::open succeeds |
The packaged case is the one that works: a .usdz whose root layer authors
asset f = @tex\stone.png@, with an entry tex/stone.png, resolves to
pkg.usdz[tex/stone.png].
Probe
use openusd::{sdf, usd::Stage, usdz::ArchiveWriter};
use std::fs;
fn main() {
let d = std::path::Path::new("run");
fs::create_dir_all(d.join("tex")).unwrap();
fs::write(d.join("tex/stone.png"), b"x").unwrap();
fs::write(d.join("back.usda"), "#usda 1.0\ndef \"Q\"\n{\n asset f = @tex\\stone.png@\n}\n").unwrap();
let s = Stage::open(d.join("back.usda").to_str().unwrap()).unwrap();
let a = s.attribute("/Q.f").unwrap().get::<sdf::AssetPath>().unwrap().unwrap();
println!("loose: {:?}", a.resolved_path()); // None
let mut w = ArchiveWriter::create(d.join("pkg.usdz")).unwrap();
w.add_layer("root.usda", b"#usda 1.0\ndef \"Q\"\n{\n asset f = @tex\\stone.png@\n}\n").unwrap();
w.add_layer("tex/stone.png", b"x").unwrap();
w.finish().unwrap();
let s = Stage::open(d.join("pkg.usdz").to_str().unwrap()).unwrap();
let a = s.attribute("/Q.f").unwrap().get::<sdf::AssetPath>().unwrap().unwrap();
println!("usdz: {:?}", a.resolved_path()); // Some("…/pkg.usdz[tex/stone.png]")
}Where it comes from
In crates/openusd/src/ar.rs, DefaultResolver::create_identifier takes two routes:
- Package-relative anchors go through
join_packaged_path(~L1017). It splits the
reference withrel.split(['/', '\\']), so\is a separator. - Filesystem anchors use
dir.join(path)(~L591), followed bycanonical_identifier
orlexically_normalize. On Unix,Pathtreats\as an ordinary byte.
What C++ USD does
C++ normalizes the full path only on Windows. _AnchorRelativePath in
pxr/usd/ar/defaultResolver.cpp replaces \ with / in the anchor alone. The asset
path is then passed through TfNormPath → ArchNormPath. In pxr/base/arch/fileSystem.cpp
the ARCH_OS_WINDOWS version of ArchNormPath begins with
std::replace(path.begin(), path.end(), '\\', '/'). The POSIX version does not.
C++ on Linux therefore fails the same way openusd does, and openusd's loose-file
behaviour is at parity. A layer authored on Windows resolves for its author in C++,
which is why such files exist in the wild.
Where it shows up
Intel's New Sponza USD export, written by "USD for Autodesk 3ds Max", authors all 98
texture inputs as @textures\<name>.png@. Opened with openusd on Linux, none of them
resolves.
Proposal
Treat \ as a separator when anchoring a relative filesystem path, as
join_packaged_path already does. Equivalently, apply on every platform what
ArchNormPath does on Windows.
- Scope: relative paths only, and only after the authored spelling has failed. If a
file whose name really contains\exists, it still wins, so no path that resolves
today changes meaning. A cheaper alternative with the same practical effect is to
normalize unconditionally; a Unix file name containing\is rare. - Drive-letter and UNC paths (
C:\…,\\server\…):Path::is_relative()reports
these as relative on Unix, so today they get joined under the anchor directory. They
cannot resolve on Unix either way. It may be worth leaving them alone, or treating
them as absolute, rather than producing…/dir/C:/…. - Coverage: because this lives in
create_identifier, it covers asset attributes,
sublayers, references and payloads together.
link to doubleailes/crust-render#263
Activity
mxpv commentedon Oct 10, 2026
Fixed on
main:ar::DefaultResolvernow reads\as a separator in a relative asset path on every platform, so@textures\stone.png@, sublayers, references and payloads authored on Windows resolve on Linux and macOS as they do inside a.usdz. Two side effects: a Unix file whose name contains a literal\can no longer be named by a relative asset path, and drive or UNC paths (C:..., \server..., any X: prefix) are treated as absolute and left unresolved on Unix.Read `\` as a separator in relative asset paths