Skip to content

Relative asset paths with \ separators resolve inside a .usdz but not as loose files #149

Closed

Description

@doubleailes

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 with rel.split(['/', '\\']), so \ is a separator.
  • Filesystem anchors use dir.join(path) (~L591), followed by canonical_identifier
    or lexically_normalize. On Unix, Path treats \ 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

  1. mxpv commented on Oct 10, 2026

    @mxpv
    Owner

    Fixed on main: ar::DefaultResolver now 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.

  2. added a commit that references this issue on Oct 11, 2026
    c2c1afc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions