Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 50 additions & 34 deletions .github/workflows/build-usd-exchange.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ jobs:
cmake --install /work/_build/cmake --config Release --prefix /work/_build/install

"$PY" - <<'STAGE_EOF'
import glob, importlib.metadata, os, shutil, sys, toml
import glob, importlib.metadata, os, shutil, subprocess, sys, toml

usd ="/work/_build/usd"
usdex = "/work/_build/install"
Expand Down Expand Up @@ -188,9 +188,6 @@ jobs:
else:
sys.exit(f"plugin {plugin} not found")
copy(f"{usd}/plugin/usd/usdShaders.so", f"{libs}/usd/usdShaders.so")
for name in validators:
if name.endswith("Validators"):
copy(f"{usd}/lib/libusd_{name}.so", f"{libs}/libusd_{name}.so")

mtlx = f"{libs}/usd/usdMtlx/resources/libraries"
for src in glob.glob(f"{usd}/libraries/bxdf/*open_pbr_surface.mtlx"):
Expand All @@ -206,6 +203,26 @@ jobs:
if not glob.glob(f"{staging}/pxr/{module}/{lib}*.so"):
sys.exit(f"pxr.{module} has no {lib} binding")

# Stage every native library beside the plugInfo.json files above, under its own SONAME rather than
# auditwheel's content-hash name: usd-exchange 3.0.1's check_wheel_imports.py requires the exact SONAME,
# matching upstream's own staging (tools/repoman/py_package.py's __stage_linux_libraries).
# build_usd.py's own pxr install always uses "lib" (it never calls GNUInstallDirs), but the --onetbb
# dependency it builds does: oneTBB's CMakeLists calls include(GNUInstallDirs) and never overrides
# CMAKE_INSTALL_LIBDIR, so on this Rocky Linux 10 image (64-bit, no /etc/debian_version) GNUInstallDirs'
# own default resolves to "lib64" - libtbb.so.12 lands in {usd}/lib64, not {usd}/lib.
for source_lib in (f"{usd}/lib", f"{usd}/lib64", f"{usdex}/lib"):
for lib in sorted(glob.glob(f"{source_lib}/*.so*")):
if os.path.islink(lib):
continue
with open(lib, "rb") as f:
if f.read(4) != b"\x7fELF":
continue
soname = subprocess.run(["patchelf", "--print-soname", lib], capture_output=True, text=True, check=True).stdout.strip()
if soname:
copy(lib, f"{libs}/{soname}")
with open("/work/_build/staged_sonames.txt", "w") as f:
f.writelines(f"{name}\n" for name in sorted(os.listdir(libs)) if os.path.isfile(f"{libs}/{name}"))

notices = {
"materialx-LICENSE.txt": glob.glob("/work/_build/usd-src/MaterialX-*/LICENSE")[0],
"onetbb-LICENSE.txt": glob.glob("/work/_build/usd-src/oneTBB-*/LICENSE.txt")[0],
Expand Down Expand Up @@ -240,37 +257,36 @@ jobs:
find /work/_build/pyproject /work/_build/usd/lib /work/_build/install/lib -name '*.so*' -type f \
-exec sh -c 'head -c4 "$1" | grep -q ELF && strip "$1"' _ {} \;

# Point every staged binary's RPATH at usd_exchange.libs so the libraries staged above resolve without
# auditwheel re-grafting (and content-hash renaming) them.
find /work/_build/pyproject -name '*.so*' -type f | while read -r binary; do
head -c4 "$binary" | grep -q ELF || continue
relative="$(realpath --relative-to="$(dirname "$binary")" /work/_build/pyproject/usd_exchange.libs)"
if [ "$relative" = "." ]; then
rpath="\$ORIGIN"
else
rpath="\$ORIGIN/${relative}"
fi
patchelf --force-rpath --set-rpath "$rpath" "$binary"
done

"$PY" -m build --wheel --outdir /work/_build/pyproject/dist /work/_build/pyproject
LD_LIBRARY_PATH="/work/_build/install/lib:/work/_build/usd/lib:${LD_LIBRARY_PATH:-}" \
auditwheel repair -w /work/_build/wheelhouse /work/_build/pyproject/dist/*.whl

"$PY" - <<'PLUGINFO_EOF'
import glob, json, os, re, subprocess, sys, tempfile

wheel = glob.glob("/work/_build/wheelhouse/*.whl")[0]
with tempfile.TemporaryDirectory() as tmp:
subprocess.run([sys.executable, "-m", "wheel", "unpack", wheel, "--dest", tmp], check=True)
unpacked = glob.glob(f"{tmp}/*/")[0].rstrip("/")
libs_root = f"{unpacked}/usd_exchange.libs"
hashed = {}
for lib in glob.glob(f"{libs_root}/*.so*"):
match = re.match(r"^(lib.+?)-[0-9a-f]{6,}\.so", os.path.basename(lib))
if match:
hashed[match.group(1)] = os.path.basename(lib)
for plug_info in glob.glob(f"{libs_root}/usd/*/resources/plugInfo.json"):
with open(plug_info) as f:
data = json.loads("".join(line for line in f if not line.lstrip().startswith("#")))
modified = False
for plug in data.get("Plugins", []):
lib = hashed.get(f"libusd_{plug.get('Name')}")
if lib and "LibraryPath" in plug:
plug["LibraryPath"] = f"../../{lib}"
modified = True
if modified:
with open(plug_info, "w") as f:
json.dump(data, f, indent=4)
subprocess.run([sys.executable, "-m", "wheel", "pack", unpacked, "--dest-dir", "/work/dist"], check=True)
PLUGINFO_EOF

# Exclude the libraries staged (and rpath'd) above, so auditwheel leaves their SONAMEs alone instead of
# grafting a second, content-hash-renamed copy; a resolution miss then fails the wheel tests instead.
# This image's statically-linked cp3xx also pulls in libcrypt, which is on no manylinux policy's
# lib_whitelist (any architecture); trust the target's libxcrypt instead of vendoring it, the same way
# other workflows here trust a target's libssl/libcrypto.
exclude_args=()
while read -r soname; do
exclude_args+=(--exclude "$soname")
done < <(cat /work/_build/staged_sonames.txt; find /work/_build/pyproject -name '*.so*' -type f \
-exec sh -c 'head -c4 "$1" | grep -q ELF && readelf -d --wide "$1"' _ {} \; \
| awk '/\(NEEDED\)/ {gsub(/[][]/, "", $NF); print $NF}' | grep '^libcrypt\.so' | sort -u)
LD_LIBRARY_PATH="/work/_build/install/lib:/work/_build/usd/lib:/work/_build/usd/lib64:${LD_LIBRARY_PATH:-}" \
auditwheel repair -w /work/_build/wheelhouse "${exclude_args[@]}" /work/_build/pyproject/dist/*.whl

cp /work/_build/wheelhouse/*.whl /work/dist/
ls -la /work/dist
USDEX_BUILD_EOF

Expand Down
1 change: 1 addition & 0 deletions docs/packages/usd-exchange.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ versions:
- filename: usd_exchange-3.0.0-cp313-cp313-manylinux_2_39_riscv64.whl
sha256: 1237b9c7d3579c59f97a139458c8841d1712583a01966a3265b2f07e03b5ba38
requires-python: <3.14,>=3.10
- version: 3.0.1
121 changes: 121 additions & 0 deletions patches/usd-exchange/3.0.1/0001-Support-riscv64-in-pxr-base-arch.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
From e346d5ee838ae79333ed9e0f6242891c841ed1b8 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 21 Sep 2026 01:53:09 +0000
Subject: [PATCH] Support riscv64 in pxr/base/arch

pxr/base/arch/defines.h recognises only x86 and ARM, and its "Bits" block
ends in #error "Unsupported architecture. x86_64 or ARM64 required." - so
every translation unit of USD fails on riscv64 before anything else is
compiled. Two further sites are reached once that is fixed: arch/math.h
guards ArchSign/ArchFloatToBitPattern/... on
ARCH_CPU_INTEL || ARCH_CPU_ARM || ARCH_OS_WASM_VM and ends in #error Unknown
system architecture (the code it guards is portable IEEE-754 bit twiddling,
not x86 or ARM specific), and nonLockingLinux__execve() in
arch/stackTrace.cpp has hand-written syscall asm for aarch64 and x86_64 with
#error Unknown architecture in the #else.

Add an ARCH_CPU_RISCV define for __riscv, admit __riscv_xlen == 64 to
ARCH_BITS_64, let math.h's gate accept it, and give
nonLockingLinux__execve() a riscv64 branch. The syscall branch mirrors the
aarch64 one exactly: the Linux riscv64 ABI passes arguments in a0-a2, takes
the syscall number in a7, uses the same generic __NR_execve == 221 as
aarch64, and traps with ecall instead of svc. Everything else in arch is
already portable: ARCH_SPIN_PAUSE() has a no-op #else, timing.h only reaches
rdtsc behind PXR_ARCH_PREFER_TSC_TIMING and otherwise uses
std::chrono::steady_clock, and the remaining #error branches in
systemInfo.cpp/fileSystem.cpp/assumptions.cpp select on the OS, not the CPU.

With this, the core (non-imaging) USD libraries and their Python bindings
build and run on riscv64; pxr/base/gf's SIMD paths are already gated on
__SSE2__/__ARM_NEON with scalar fallbacks, and the vendored
double-conversion already lists __riscv.

Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against PixarAnimationStudios/OpenUSD]
---
pxr/base/arch/defines.h | 7 +++++--
pxr/base/arch/math.h | 1 +
pxr/base/arch/overview.dox | 1 +
pxr/base/arch/stackTrace.cpp | 14 ++++++++++++++
4 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/pxr/base/arch/defines.h b/pxr/base/arch/defines.h
index b7ed634..58e860c 100644
--- a/pxr/base/arch/defines.h
+++ b/pxr/base/arch/defines.h
@@ -39,6 +39,8 @@
#elif defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || \
defined(_M_ARM64)
#define ARCH_CPU_ARM
+#elif defined(__riscv)
+#define ARCH_CPU_RISCV
#endif

//
@@ -46,12 +48,13 @@
//

#if defined(__x86_64__) || defined(__aarch64__) || defined(_M_X64) || \
- defined(_M_ARM64) || defined(__wasm64__)
+ defined(_M_ARM64) || defined(__wasm64__) || \
+ (defined(__riscv) && __riscv_xlen == 64)
#define ARCH_BITS_64
#elif defined(__wasm32__)
#define ARCH_BITS_32
#else
-#error "Unsupported architecture. x86_64 or ARM64 required."
+#error "Unsupported architecture. x86_64, ARM64 or RISC-V 64 required."
#endif

//
diff --git a/pxr/base/arch/math.h b/pxr/base/arch/math.h
index ccd53a6..8191023 100644
--- a/pxr/base/arch/math.h
+++ b/pxr/base/arch/math.h
@@ -30,6 +30,7 @@ PXR_NAMESPACE_OPEN_SCOPE
///@{

#if defined (ARCH_CPU_INTEL) || defined (ARCH_CPU_ARM) || \
+ defined (ARCH_CPU_RISCV) || \
defined(ARCH_OS_WASM_VM) || defined (doxygen)

/// This is the smallest value e such that 1+e^2 == 1, using floats.
diff --git a/pxr/base/arch/overview.dox b/pxr/base/arch/overview.dox
index b8eaf9b..045375b 100644
--- a/pxr/base/arch/overview.dox
+++ b/pxr/base/arch/overview.dox
@@ -64,6 +64,7 @@ For processor:
<ul>
<li> ARCH_CPU_INTEL
<li> ARCH_CPU_ARM
+ <li> ARCH_CPU_RISCV
</ul>

For bits:
diff --git a/pxr/base/arch/stackTrace.cpp b/pxr/base/arch/stackTrace.cpp
index 384959e..6a90d38 100644
--- a/pxr/base/arch/stackTrace.cpp
+++ b/pxr/base/arch/stackTrace.cpp
@@ -878,6 +878,20 @@ nonLockingLinux__execve (const char *file,
);
result = __file_result;
}
+#elif defined (ARCH_CPU_RISCV) && defined(ARCH_BITS_64)
+ {
+ long __file_result asm ("a0") = (long)file;
+ char* const* __argv asm ("a1") = argv;
+ char* const* __envp asm ("a2") = envp;
+ long __num_execve asm ("a7") = 221;
+ __asm__ __volatile__ (
+ "ecall"
+ : "=r" (__file_result)
+ : "r"(__num_execve), "r" (__file_result), "r" (__argv), "r" (__envp)
+ : "memory"
+ );
+ result = __file_result;
+ }
#elif defined(ARCH_CPU_INTEL) && defined(ARCH_BITS_64)

/*
--
2.43.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 4 Oct 2026 01:30:19 +0000
Subject: [PATCH] Skip the cache-line size assumption check on riscv64

Arch_ValidateAssumptions() runs on every load of libusd_arch and compares
ARCH_CACHE_LINE_SIZE (pxr/base/arch/align.h: 128 on Apple ARM, 64 everywhere
else) with Arch_ObtainCacheLineSize(), i.e. sysconf(_SC_LEVEL1_DCACHE_LINESIZE)
on Linux. On a mismatch it prints a four-line " ArchWarn: ARCH_CACHE_LINE_SIZE
!= Arch_ObtainCacheLineSize()" block straight to stderr with fprintf
(arch/error.cpp), below the Tf diagnostics layer, so no environment setting or
diagnostic delegate can silence it.

On riscv64 the comparison has nothing to check. The ISA defines no cache-line
size; it varies between implementations. glibc's riscv sysconf reads it from
the AT_L1D_CACHEGEOMETRY auxv entry, which the kernel fills from the
firmware's devicetree/ACPI cache description and sets to 0 when there is none
(arch/riscv/kernel/cacheinfo.c). Whatever value align.h picks for riscv64 is
therefore "wrong" on some machines, and the warning fires on every process
that imports pxr on those machines: every user of the wheel sees it, and
every test that checks a subprocess's stderr exactly fails because of it
(usd-exchange's testDiagnostics.py assertOutputStreams() and
testSettings.py's empty-stderr assertion).

Keep ARCH_CACHE_LINE_SIZE at its 64 default (it only sizes padding and
alignment, so a mismatch costs performance, never correctness) and skip the
comparison when ARCH_CPU_RISCV is defined (added by the previous patch). The
other checks in Arch_ValidateAssumptions(), including the big-endian
ARCH_ERROR, are unchanged.

Upstream-Status: To upstream [not submitted from this automated port run; belongs with the riscv64 arch support of the previous patch in a pull request against PixarAnimationStudios/OpenUSD]
---
pxr/base/arch/assumptions.cpp | 8 ++++++++
1 file changed, 8 insertions(+)

diff --git a/pxr/base/arch/assumptions.cpp b/pxr/base/arch/assumptions.cpp
index 1029b32..f0c63df 100644
--- a/pxr/base/arch/assumptions.cpp
+++ b/pxr/base/arch/assumptions.cpp
@@ -135,10 +135,18 @@ Arch_ValidateAssumptions()
/*
* Make sure that the ARCH_CACHE_LINE_SIZE constant is set as expected
* on the current hardware architecture.
+ *
+ * RISC-V has no architectural cache-line size: it varies between
+ * implementations, and Linux reports whatever the firmware describes
+ * (0 when it describes none), so there is no constant to validate.
*/
+#if !defined(ARCH_CPU_RISCV)
if (ARCH_CACHE_LINE_SIZE != cacheLineSize) {
ARCH_WARNING("ARCH_CACHE_LINE_SIZE != Arch_ObtainCacheLineSize()");
}
+#else
+ (void)cacheLineSize;
+#endif

/*
* Make sure that the machine is little-endian. We do not support
--
2.43.0

Loading