Skip to content

Commit c1daef2

Browse files
aslukclaude
andcommitted
usdGeospatial: shrinkwrap the C++ Hydra plugin into build_usd.py (--usdGeospatial), cross-platform
Make the usdGeospatialSceneIndex example an opt-in, cross-platform build like hdParticleField -- one build_usd.py flag builds PROJ and the plugin, replacing the Linux-only shell scripts. Validated on Linux and Windows (MSVC). Build: - build_usd.py: --usdGeospatial builds PROJ (+ its SQLite3 dependency) as opt-in third-party deps, then passes -DPXR_ENABLE_GEOSPATIAL_SUPPORT to the USD build. - cmake/defaults: PXR_ENABLE_GEOSPATIAL_SUPPORT option + find_package(PROJ). - Register the example under extras/usd/examples (gated on the flag + imaging); fix its CMakeLists: add the missing geospatialAPISchemaAdapter/geospatialSchema sources, define USDGEOSPATIAL_SI_EXPORTS, DISABLE_PRECOMPILED_HEADERS, link PROJ::proj. Parity as a ctest: - run_parity.py + pxr_register_test(testUsdGeospatialParity): CRS engine 9/9, stage resolver 30/30, Hydra scene index 30/30, stage-free auto-insert 30/30 -- all 0.0 mm. dumpHydraXforms built for the runtime_parity figure. - dump_oracle.py auto-converts the committed railway asset so the oracle is the full 30-row set. Cross-platform figure/doc tooling: - fig_runtime_parity.py / render_figures.py: portable dumper + tsv paths (GEO_HYDRA_TSV / GEO_HYDRA_DUMPER, system temp, os.pathsep) -- no /tmp or /usr/share/proj hardcodes. - docs/build_deck.py: read the README as UTF-8 (Windows). - probe_autoinsert.py: drop dead Hd/HdGp imports. Docs (README is the base artifact; deck derives from it): - README build sections -> the cross-platform build + ctest; add a reproducibility slide; regenerate the deck PDF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 576d187 commit c1daef2

14 files changed

Lines changed: 396 additions & 46 deletions

File tree

build_scripts/build_usd.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,75 @@ def InstallZlib(context, force, buildArgs):
789789
RunCMake(context, force, extraArgs + buildArgs)
790790

791791
ZLIB = Dependency("zlib", InstallZlib, "include/zlib.h")
792-
792+
793+
############################################################
794+
# SQLite3 (required by PROJ to build/read its CRS database proj.db)
795+
796+
# SQLite ships as an "amalgamation" (a single sqlite3.c plus headers and the
797+
# shell.c command-line driver) with no build system, so we drop in a minimal
798+
# CMakeLists that produces both a static library AND the sqlite3 command-line
799+
# tool -- PROJ needs the CLI to generate proj.db at build time.
800+
SQLITE3_URL = "https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip"
801+
802+
SQLITE3_CMAKELISTS = """\
803+
cmake_minimum_required(VERSION 3.15)
804+
project(SQLite3 C)
805+
add_library(sqlite3 STATIC sqlite3.c)
806+
set_property(TARGET sqlite3 PROPERTY POSITION_INDEPENDENT_CODE ON)
807+
add_executable(sqlite3_cli shell.c sqlite3.c)
808+
set_target_properties(sqlite3_cli PROPERTIES OUTPUT_NAME sqlite3)
809+
if(NOT WIN32)
810+
find_package(Threads REQUIRED)
811+
target_link_libraries(sqlite3 PUBLIC Threads::Threads ${CMAKE_DL_LIBS})
812+
target_link_libraries(sqlite3_cli PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
813+
endif()
814+
install(TARGETS sqlite3 ARCHIVE DESTINATION lib LIBRARY DESTINATION lib)
815+
install(TARGETS sqlite3_cli RUNTIME DESTINATION bin)
816+
install(FILES sqlite3.h sqlite3ext.h DESTINATION include)
817+
"""
818+
819+
def InstallSQLite3(context, force, buildArgs):
820+
with CurrentWorkingDirectory(DownloadURL(SQLITE3_URL, context, force)):
821+
with open("CMakeLists.txt", "w") as f:
822+
f.write(SQLITE3_CMAKELISTS)
823+
RunCMake(context, force, buildArgs)
824+
825+
SQLITE3 = Dependency("SQLite3", InstallSQLite3, "include/sqlite3.h")
826+
827+
############################################################
828+
# PROJ (coordinate transforms / CRS engine; used by the
829+
# usdGeospatialSceneIndex example when PXR_ENABLE_GEOSPATIAL_SUPPORT is on).
830+
# Depends on SQLite3 (built above) for proj.db.
831+
832+
PROJ_URL = "https://github.com/OSGeo/PROJ/archive/refs/tags/9.4.1.zip"
833+
834+
def InstallPROJ(context, force, buildArgs):
835+
with CurrentWorkingDirectory(DownloadURL(PROJ_URL, context, force)):
836+
sqlite3Exe = os.path.join(context.instDir, "bin",
837+
"sqlite3.exe" if Windows() else "sqlite3")
838+
sqlite3Lib = os.path.join(context.instDir, "lib",
839+
"sqlite3.lib" if Windows() else "libsqlite3.a")
840+
extraArgs = [
841+
'-DENABLE_TIFF=OFF',
842+
'-DENABLE_CURL=OFF',
843+
'-DBUILD_TESTING=OFF',
844+
'-DBUILD_APPS=OFF',
845+
'-DBUILD_SHARED_LIBS=ON',
846+
'-DEXE_SQLITE3="{exe}"'.format(exe=sqlite3Exe),
847+
'-DSQLITE3_INCLUDE_DIR="{inc}"'.format(
848+
inc=os.path.join(context.instDir, "include")),
849+
'-DSQLITE3_LIBRARY="{lib}"'.format(lib=sqlite3Lib),
850+
# For compatibility with CMake 4+
851+
'-DCMAKE_POLICY_VERSION_MINIMUM=3.5',
852+
]
853+
854+
# Add on any user-specified extra arguments.
855+
extraArgs += buildArgs
856+
857+
RunCMake(context, force, extraArgs)
858+
859+
PROJ = Dependency("PROJ", InstallPROJ, "include/proj.h")
860+
793861
############################################################
794862
# boost
795863

@@ -1873,7 +1941,12 @@ def InstallUSD(context, force, buildArgs):
18731941
extraArgs.append('-DPXR_BUILD_USD_VALIDATION=ON')
18741942
else:
18751943
extraArgs.append('-DPXR_BUILD_USD_VALIDATION=OFF')
1876-
1944+
1945+
if context.enableGeospatial:
1946+
extraArgs.append('-DPXR_ENABLE_GEOSPATIAL_SUPPORT=ON')
1947+
else:
1948+
extraArgs.append('-DPXR_ENABLE_GEOSPATIAL_SUPPORT=OFF')
1949+
18771950
if context.buildImaging:
18781951
extraArgs.append('-DPXR_BUILD_IMAGING=ON')
18791952
if context.enablePtex:
@@ -2298,10 +2371,18 @@ def InstallUSD(context, force, buildArgs):
22982371
subgroup.add_argument("--openvdb", dest="enable_openvdb", action="store_true",
22992372
default=False,
23002373
help="Enable OpenVDB support in imaging")
2301-
subgroup.add_argument("--no-openvdb", dest="enable_openvdb",
2374+
subgroup.add_argument("--no-openvdb", dest="enable_openvdb",
23022375
action="store_false",
23032376
help="Disable OpenVDB support in imaging (default)")
23042377
subgroup = group.add_mutually_exclusive_group()
2378+
subgroup.add_argument("--usdGeospatial", dest="enable_geospatial",
2379+
action="store_true", default=False,
2380+
help="Enable geospatial (PROJ) support and build the "
2381+
"usdGeospatialSceneIndex example")
2382+
subgroup.add_argument("--no-usdGeospatial", dest="enable_geospatial",
2383+
action="store_false",
2384+
help="Disable geospatial support (default)")
2385+
subgroup = group.add_mutually_exclusive_group()
23052386
subgroup.add_argument("--usdview", dest="build_usdview",
23062387
action="store_true", default=True,
23072388
help="Build usdview (default)")
@@ -2540,6 +2621,11 @@ def __init__(self, args):
25402621
and args.enable_vulkan
25412622
and not embedded)
25422623

2624+
# - Geospatial (PROJ). Independent of imaging at the flag level, though
2625+
# the usdGeospatialSceneIndex plugin itself links usdImaging, so its
2626+
# CMake registration is additionally gated on PXR_BUILD_USD_IMAGING.
2627+
self.enableGeospatial = (args.enable_geospatial and not embedded)
2628+
25432629
# - USD Imaging
25442630
self.buildUsdImaging = (args.build_imaging == USD_IMAGING and
25452631
not self.targetWasm)
@@ -2629,6 +2715,10 @@ def ForceBuildDependency(self, dep):
26292715
if context.buildMaterialX:
26302716
requiredDependencies += [MATERIALX]
26312717

2718+
# PROJ needs SQLite3 built first (for proj.db), so list SQLITE3 before PROJ.
2719+
if context.enableGeospatial:
2720+
requiredDependencies += [SQLITE3, PROJ]
2721+
26322722
if context.buildImaging:
26332723
if context.enablePtex:
26342724
requiredDependencies += [ZLIB, PTEX]

cmake/defaults/Options.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ option(PXR_ENABLE_HDF5_SUPPORT "Enable HDF5 backend in the Alembic plugin for US
3535
option(PXR_ENABLE_OSL_SUPPORT "Enable OSL (OpenShadingLanguage) based components" OFF)
3636
option(PXR_ENABLE_PTEX_SUPPORT "Enable Ptex support" OFF)
3737
option(PXR_ENABLE_OPENVDB_SUPPORT "Enable OpenVDB support" OFF)
38+
option(PXR_ENABLE_GEOSPATIAL_SUPPORT "Enable geospatial (PROJ) support, used by the usdGeospatialSceneIndex example" OFF)
3839
option(PXR_ENABLE_NAMESPACES "Enable C++ namespaces." ON)
3940
option(PXR_PREFER_SAFETY_OVER_SPEED
4041
"Enable certain checks designed to avoid crashes or out-of-bounds memory reads with malformed input files. These checks may negatively impact performance."

cmake/defaults/Packages.cmake

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,14 @@ if (PXR_ENABLE_MATERIALX_SUPPORT)
319319
add_definitions(-DPXR_MATERIALX_SUPPORT_ENABLED)
320320
endif()
321321

322+
if (PXR_ENABLE_GEOSPATIAL_SUPPORT)
323+
# PROJ ships its own config-mode package (proj-config.cmake) providing the
324+
# PROJ::proj imported target; it is found on CMAKE_PREFIX_PATH (the shared
325+
# install prefix that build_usd.py populates).
326+
find_package(PROJ REQUIRED)
327+
add_definitions(-DPXR_GEOSPATIAL_SUPPORT_ENABLED)
328+
endif()
329+
322330
if(PXR_ENABLE_OSL_SUPPORT)
323331
find_package(OSL REQUIRED)
324332
set(REQUIRES_Imath TRUE)

extras/usd/examples/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ else()
1010
add_subdirectory(usdResolverExample)
1111
add_subdirectory(usdSchemaExamples)
1212
add_subdirectory(usdMakeFileVariantModelAsset)
13+
14+
# The geospatial scene-index example is opt-in (needs PROJ) and links
15+
# usdImaging/hd, so it additionally requires USD imaging to be built.
16+
if (PXR_ENABLE_GEOSPATIAL_SUPPORT AND PXR_BUILD_USD_IMAGING)
17+
add_subdirectory(usdGeospatialSceneIndex)
18+
endif()
1319
endif()

extras/usd/examples/usdGeospatial/README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,14 @@ The takeaway is the schema claim itself: **the schema is the contract; the behav
426426
third runtime (OpenExec, a GPU / cuProj engine, an Omniverse runtime) plugs into the same seam and
427427
is held to the same oracle.
428428

429+
<!-- slide:text eyebrow="Reproducible · cross-platform" title="The second runtime builds like any USD example" body="`build_usd.py --usdGeospatial` fetches and builds PROJ, then the C++ Hydra plugin, behind one opt-in flag — the `hdParticleField` pattern, no bespoke setup. | Parity is a `ctest` (`testUsdGeospatialParity`): CRS engine + stage resolver + Hydra scene index + stage-free auto-insert, all 0.0 mm. | Validated on Linux **and** Windows — the contract builds twice, the same way, on two platforms." -->
430+
431+
And it builds like any other OpenUSD example: `build_usd.py --usdGeospatial --examples` fetches and
432+
builds PROJ and the C++ Hydra plugin behind one opt-in flag — the same shape as `hdParticleField`,
433+
on Linux and Windows alike — and `ctest -R testUsdGeospatialParity` runs the whole parity proof
434+
(CRS engine, stage resolver, Hydra scene index, and the stage-free auto-insert path). The second
435+
runtime isn't a Linux-only lab artifact; it's a normal, opt-in part of the build.
436+
429437
### Where the Python reference runtime sits (no exact precedent — by design)
430438

431439
<!-- slide:text eyebrow="No exact precedent — by design" title="Where the Python reference runtime sits" body="It's the codeless schema's **conformance oracle** — 'given this stage, where does each prim end up?' — pinned to closed-form geodesy, that compliant implementations should match. | NOT the specification: the normative behavior belongs in **prose** (proposed into the Esri proposal); the Python is the oracle that expresses it, not the source of truth. | NOT proposed for USD core, NOT a runtime dependency, NOT Python-in-the-render-loop, NOT the prescribed consumer. | Precedent: AOUSD's core-spec-supplemental — Python sample impls + a compliance framework, spec normative and impl illustrative — is exactly this shape." -->
@@ -474,17 +482,18 @@ Every test is openable and runnable; each has a real negative control or an inde
474482
neutral scene has zero xformOps).
475483
- `render_figures.py` — the coherence figure self-asserts sub-mm co-registration; `fig_runtime_parity`
476484
fails the build if Python-vs-Hydra disagreement exceeds 1 mm.
477-
- `../usdGeospatialSceneIndex/run_parity.sh` — the compiled C++ scene index: CRS engine 9/9, stage
478-
resolver 30/30, Hydra SI via `HdXformSchema` 30/30 — all 0.0 mm; negative control: stock Hydra
479-
puts georef prims at the origin. `testHydraAutoParity` adds the stage-free auto-insert path (30/30).
485+
- `ctest -R testUsdGeospatialParity` (cross-platform; also `run_parity.sh`) — the compiled C++ scene
486+
index: CRS engine 9/9, stage resolver 30/30, Hydra SI via `HdXformSchema` 30/30 — all 0.0 mm;
487+
negative control: stock Hydra puts georef prims at the origin. `testHydraAutoParity` adds the
488+
stage-free auto-insert path (30/30).
480489
- `pxr/usd/usdGeospatial/regen-schema.sh --check` — schema resources are in sync.
481490

482491
### Running — two paths
483492

484-
**The codeless Python path (no external renderer):**
493+
**The codeless Python path (no external renderer)** — any Python with `pxr` (a usd-core wheel or a
494+
USD build) plus `pyproj`, `numpy`, `matplotlib`:
485495

486496
```bash
487-
source <repo>/.venv/bin/activate # usd-core 26.5, pyproj 3.7.1 (PROJ 9.5.1)
488497
cd extras/usd/examples/usdGeospatial
489498
python3 src/reencode_georef.py --stride 40 --out out/earth2_georef.usda
490499
python3 src/verify.py out/earth2_georef.usda
@@ -496,17 +505,21 @@ python3 src/render_figures.py # regenerate the Python-reference figure
496505
```
497506

498507
`render_figures.py` regenerates the nine Python-reference figures; it also produces
499-
`multi_runtime.png` / `runtime_parity.png` **if** the compiled C++ Hydra binary is already built,
500-
otherwise it skips those two with a clear note.
508+
`multi_runtime.png` / `runtime_parity.png` if the compiled C++ Hydra binary is available (built as
509+
below), otherwise it skips those two with a clear note.
501510

502-
**The full two-runtime parity proof (needs a prebuilt USD):**
511+
**The full two-runtime parity proof — an opt-in, cross-platform build.** The C++ Hydra scene index
512+
builds like any other OpenUSD example: one flag makes `build_usd.py` fetch and build PROJ, then the
513+
plugin and its parity tests. Validated on Linux and Windows.
503514

504515
```bash
505-
USD_INST=/path/to/usd/inst ../usdGeospatialSceneIndex/run_parity.sh
516+
python build_scripts/build_usd.py --usdGeospatial --examples --tests <inst> # builds PROJ + plugin + tests
517+
ctest --test-dir <build> -R testUsdGeospatialParity # engine + resolver + Hydra SI + auto-insert, 0.0 mm
506518
```
507519

508-
See `../usdGeospatialSceneIndex/README.md` for the full build environment, and the same directory's
509-
notes for reproducing the auto-insert usdview / usdrecord render.
520+
The plugin installs discoverable via `PXR_PLUGINPATH_NAME`; opening the georef scene in `usdview` /
521+
`usdrecord` then auto-resolves it. `run_parity.py` (invoked by that ctest) is also runnable
522+
standalone. See `../usdGeospatialSceneIndex/README.md` for details and the auto-insert render notes.
510523

511524
## Status, scope, and open questions
512525

extras/usd/examples/usdGeospatial/docs/build_deck.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def caption_after(lines, start):
102102

103103

104104
def build_slides():
105-
lines = open(README).read().splitlines()
105+
lines = open(README, encoding="utf-8").read().splitlines()
106106
marks = [(i, m.group(1), parse_attrs(m.group(2)))
107107
for i, ln in enumerate(lines) for m in [MARKER.search(ln)] if m]
108108
slides = []
@@ -240,7 +240,7 @@ def main():
240240
slides = build_slides()
241241
doc = (f"<!doctype html><html><head><meta charset='utf-8'><style>{CSS}</style></head>"
242242
f"<body>{render(slides)}</body></html>")
243-
with open(OUT, "w") as f:
243+
with open(OUT, "w", encoding="utf-8") as f:
244244
f.write(doc)
245245
print(f"wrote {OUT} ({len(slides)} slides, {len(doc)//1024}KB)")
246246
print("order:", " ".join(s[0].lower() for s in slides))
21.1 KB
Binary file not shown.

extras/usd/examples/usdGeospatial/fig_runtime_parity.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
1919
Output: docs/runtime_parity.png
2020
"""
21-
import os, sys, numpy as np, matplotlib.pyplot as plt
21+
import os, sys, tempfile, numpy as np, matplotlib.pyplot as plt
2222
from PIL import Image
2323

2424
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -30,7 +30,10 @@
3030
ECEF = CRS.from_epsg(4978)
3131

3232
STAGE = os.path.join(HERE, "out", "railway_georef.usda")
33-
HYDRA_TSV = "/tmp/hydra_railway_xforms.tsv"
33+
# Portable: honor GEO_HYDRA_TSV (set by render_figures.py), else the system temp
34+
# dir -- not a hardcoded /tmp (so this works on Windows/macOS too).
35+
HYDRA_TSV = os.environ.get(
36+
"GEO_HYDRA_TSV", os.path.join(tempfile.gettempdir(), "hydra_railway_xforms.tsv"))
3437
OUT = os.path.join(HERE, "docs", "runtime_parity.png")
3538
TEXDIR = os.path.join(HERE, "data", "thirdparty")
3639

extras/usd/examples/usdGeospatial/src/render_figures.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -666,17 +666,25 @@ def fig_runtime_parity(out, stage_path="out/railway_georef.usda"):
666666
plugin lives in ../usdGeospatialSceneIndex/)."""
667667
here = os.path.dirname(os.path.abspath(__file__))
668668
repo_dir = os.path.abspath(os.path.join(here, ".."))
669-
tsv = "/tmp/hydra_railway_xforms.tsv"
670-
dumper = "/tmp/dumpHydraXforms"
669+
import tempfile, shutil
670+
# Portable locations (no hardcoded /tmp): GEO_HYDRA_TSV / GEO_HYDRA_DUMPER
671+
# override; otherwise the system temp dir + PATH lookup. dumpHydraXforms is
672+
# built with the example (installed under <inst>/tests).
673+
tsv = os.environ.get("GEO_HYDRA_TSV",
674+
os.path.join(tempfile.gettempdir(), "hydra_railway_xforms.tsv"))
675+
os.environ["GEO_HYDRA_TSV"] = tsv # fig_runtime_parity.py reads the same file
676+
_exe = "dumpHydraXforms" + (".exe" if os.name == "nt" else "")
677+
dumper = (os.environ.get("GEO_HYDRA_DUMPER") or shutil.which("dumpHydraXforms")
678+
or os.path.join(tempfile.gettempdir(), _exe))
671679
needs = (not os.path.exists(tsv)) or \
672680
(os.path.getmtime(tsv) < os.path.getmtime(os.path.join(repo_dir, stage_path)))
673681
if needs and os.path.exists(dumper):
674-
print("[B] re-running Hydra xform dumper -> /tmp/hydra_railway_xforms.tsv")
682+
print(f"[B] re-running Hydra xform dumper -> {tsv}")
675683
env = dict(os.environ)
676684
env["PXR_PLUGINPATH_NAME"] = (os.path.abspath(os.path.join(
677685
repo_dir, "..", "..", "..", "..", "pxr", "usd", "usdGeospatial",
678-
"resources")) + ":" + env.get("PXR_PLUGINPATH_NAME", ""))
679-
env.setdefault("PROJ_DATA", "/usr/share/proj")
686+
"resources")) + os.pathsep + env.get("PXR_PLUGINPATH_NAME", ""))
687+
# PROJ_DATA is left to the environment (portable); no /usr/share/proj.
680688
import subprocess
681689
with open(tsv, "w") as f:
682690
r = subprocess.run([dumper, os.path.join(repo_dir, stage_path)],

0 commit comments

Comments
 (0)