From 1eee9a1e946030fc5c14598c42e09f2ab7c7f6ad Mon Sep 17 00:00:00 2001 From: David Leong <116610336+leongdl@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:44:33 -0700 Subject: [PATCH 1/6] feat: add a moonray sample, build from source Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com> --- containers/moonray/.gitignore | 4 ++ containers/moonray/README.md | 26 ++++++++ containers/moonray/rocky9-cpu/Dockerfile | 69 ++++++++++++++++++++ containers/moonray/rocky9-cpu/README.md | 83 ++++++++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 containers/moonray/.gitignore create mode 100644 containers/moonray/README.md create mode 100644 containers/moonray/rocky9-cpu/Dockerfile create mode 100644 containers/moonray/rocky9-cpu/README.md diff --git a/containers/moonray/.gitignore b/containers/moonray/.gitignore new file mode 100644 index 00000000..15c67ea9 --- /dev/null +++ b/containers/moonray/.gitignore @@ -0,0 +1,4 @@ +# Downloaded example scenes (~700 MB) and render output stay local. +scenes/ +output/ +example_scenes.zip diff --git a/containers/moonray/README.md b/containers/moonray/README.md new file mode 100644 index 00000000..a23fd61e --- /dev/null +++ b/containers/moonray/README.md @@ -0,0 +1,26 @@ +# MoonRay containers + +Container samples for running [MoonRay](https://openmoonray.org/), DreamWorks' open-source +production path tracer, on the CPU. + +| Sample | What it demonstrates | Start here when | +|---|---|---| +| [Rocky Linux 9 CPU image](rocky9-cpu/) | Compiling MoonRay from source per the [official container build docs](https://docs.openmoonray.org/getting-started/installation/building-moonray/rocky9_container_build/) | You want an unprivileged CPU image, or need to build a specific MoonRay revision | + +The source build takes hours to compile but produces a self-contained image that runs as a normal +unprivileged container, with no systemd, or `--privileged` requirement. + +## Where to get the sample scenes + +No scene files ship with this sample. MoonRay publishes the example scenes itself, and the sample's +`.gitignore` keeps any local copy out of git — the archive unpacks to roughly 700 MB. + +* **Bundled test scenes** — small `.rdla` and `.usd` files already inside the image at + `/source/testdata`, from the [MoonRay source tree](https://github.com/dreamworksanimation/openmoonray). + Use these to confirm the build works. +* **Official example scenes** — download + [example_scenes.zip](https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip) from the + [MoonRay test scenes page](https://docs.openmoonray.org/getting-started/test-scenes/), then mount + the unpacked directory into the container. + +See [rocky9-cpu/README.md](rocky9-cpu/README.md) for the exact download and render commands. diff --git a/containers/moonray/rocky9-cpu/Dockerfile b/containers/moonray/rocky9-cpu/Dockerfile new file mode 100644 index 00000000..12f02289 --- /dev/null +++ b/containers/moonray/rocky9-cpu/Dockerfile @@ -0,0 +1,69 @@ +# Builds MoonRay from source on Rocky Linux 9, following the official +# container build procedure: +# https://docs.openmoonray.org/getting-started/installation/building-moonray/rocky9_container_build/ +# +# The docs run these steps interactively in a rockylinux:9 container and +# `docker commit` the result; this Dockerfile encodes the same steps so the +# image is reproducible with a single command: +# +# docker build --platform linux/amd64 -t openmoonray-rocky9 . +# +# The build compiles MoonRay's third-party dependencies (OpenEXR, USD, ...) +# and then MoonRay itself — expect it to take on the order of hours and to +# need 8+ GB of RAM available to the Docker VM. +# +# CUDA/GPU (XPU) support is omitted (--nocuda / MOONRAY_USE_OPTIX=NO), per +# the same docs, since GPUs are typically not available in containers. + +FROM rockylinux:9 + +SHELL ["/bin/bash", "-c"] + +# The openmoonray repository uses submodules and Git LFS for test data. +RUN dnf install -y git git-lfs \ + && git lfs install + +ARG OPENMOONRAY_REPO=https://github.com/dreamworksanimation/openmoonray.git +ARG OPENMOONRAY_REF=main + +# The moonray/materialx_shaders submodule points at a repository that is not +# public (404), so deactivate it and fetch the rest of the submodules. +RUN git clone --branch "${OPENMOONRAY_REF}" "${OPENMOONRAY_REPO}" /source \ + && cd /source \ + && git config submodule."moonray/materialx_shaders".update none \ + && git submodule update --init --recursive + +# Step 1: install the system packages MoonRay's build needs. +RUN source /source/building/Rocky9/install_packages.sh --nocuda \ + && dnf clean all + +# The EPEL cmake links the system libuv and crashes in its SIGCHLD handler +# when the image is built under x86_64 emulation (e.g. on Apple Silicon). +# The pip wheel is the same cmake 3.x with a bundled libuv and works +# everywhere; it installs to /usr/local/bin, which shadows /usr/bin. +RUN pip3 install "cmake<4" + +# Step 2: build the remaining dependencies from source (installs under /installs). +RUN mkdir /build \ + && cd /build \ + && cmake /source/building/Rocky9 \ + && cmake --build . -- -j "$(nproc)" + +# Step 3: build and install MoonRay itself. +RUN cd /build \ + && rm -rf ./* \ + && cmake /source \ + -DPYTHON_EXECUTABLE=python3 \ + -DBOOST_PYTHON_COMPONENT_NAME=python39 \ + -DABI_VERSION=0 \ + -DMOONRAY_USE_OPTIX=NO \ + && cmake --build . -j "$(nproc)" \ + && mkdir -p /installs/openmoonray \ + && cmake --install /build --prefix /installs/openmoonray \ + && rm -rf /build + +# setup.sh puts moonray, moonray_gui, and hd_render on PATH. +RUN echo 'source /installs/openmoonray/scripts/setup.sh' > /etc/profile.d/openmoonray.sh + +ENTRYPOINT ["/bin/bash", "-lc"] +CMD ["moonray"] diff --git a/containers/moonray/rocky9-cpu/README.md b/containers/moonray/rocky9-cpu/README.md new file mode 100644 index 00000000..0811fe94 --- /dev/null +++ b/containers/moonray/rocky9-cpu/README.md @@ -0,0 +1,83 @@ +# MoonRay on Rocky Linux 9 (CPU, source build) + +This sample builds [MoonRay](https://openmoonray.org/) from source in a +[Rocky Linux 9](https://hub.docker.com/_/rockylinux) container, following the official +[Rocky 9 container build procedure](https://docs.openmoonray.org/getting-started/installation/building-moonray/rocky9_container_build/). +Where the official docs run the steps interactively and `docker commit` the result, this +`Dockerfile` encodes the same steps so the image is reproducible with one command. + +The resulting image needs no snapd, no systemd, and no `--privileged` — MoonRay is compiled into +`/installs/openmoonray` and runs in a plain unprivileged container. + +## Prerequisites + +* Docker (or a compatible CLI such as `finch`; substitute `finch` for `docker` below) +* Time and resources: the build compiles MoonRay's dependency stack (OpenEXR, OpenVDB, USD, ...) + and then MoonRay itself. On a native x86_64 machine expect roughly 1–2 hours and 8+ GB of RAM; + under emulation on Apple Silicon it can take many hours. +* ~20 GB of free disk for intermediate layers + +## Build + +```console +docker build --platform linux/amd64 -t openmoonray-rocky9 . +``` + +The MoonRay git ref defaults to the `main` branch; override it with +`--build-arg OPENMOONRAY_REF=`. + +Two deviations from the official docs, both encoded in the `Dockerfile` with comments: + +* The `moonray/materialx_shaders` submodule points at a repository that is not public, so it is + deactivated before `git submodule update`. +* cmake is installed from pip (`pip3 install "cmake<4"`): the EPEL cmake crashes in libuv's + signal handling when the build runs under x86_64 emulation on an arm64 host. + +## Where to get the scenes + +No scene files ship with this sample. There are two sources: + +* **Bundled test scenes** — small `.rdla` and `.usd` files already present in the image at + `/source/testdata`, carried in from the + [MoonRay source tree](https://github.com/dreamworksanimation/openmoonray). Nothing to download. +* **Official example scenes** — the larger `pbrt_scenes` set, published by MoonRay as + [example_scenes.zip](https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip) on the + [test scenes page](https://docs.openmoonray.org/getting-started/test-scenes/). Download it + yourself; it unpacks to roughly 700 MB, and the sample's `.gitignore` keeps `scenes/`, + `output/`, and the zip out of git. + +## Render the bundled test scene + +The source tree (kept at `/source` in the image) includes small test scenes: + +```console +mkdir -p output +docker run --rm -v "$(pwd)/output:/output" openmoonray-rocky9 \ + 'moonray -in /source/testdata/rectangle.rdla -out /output/rectangle.exr' +``` + +## Render the example scenes + +Download and unpack the example scenes, then mount them into the container: + +```console +curl -LO https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip +mkdir -p scenes output +unzip example_scenes.zip -d scenes/ +docker run --rm -v "$(pwd)/scenes:/scenes" -v "$(pwd)/output:/output" \ + -w /scenes/example_scenes/pbrt_scenes/veach-mis openmoonray-rocky9 \ + 'moonray -in scene.rdla -in scene.rdlb -exec_mode scalar -out /output/veach-mis.exr' +``` + +`hd_render` (the USD Hydra delegate CLI) is also on `PATH`: + +```console +docker run --rm -v "$(pwd)/output:/output" openmoonray-rocky9 \ + 'hd_render -in /source/testdata/sphere.usd -out /output/sphere.exr' +``` + +## Links + +* [Official Rocky 9 container build docs](https://docs.openmoonray.org/getting-started/installation/building-moonray/rocky9_container_build/) +* [MoonRay test scenes (example_scenes.zip)](https://docs.openmoonray.org/getting-started/test-scenes/) +* [OpenMoonRay source](https://github.com/dreamworksanimation/openmoonray) From 6241edfcbbe265e6ad3eafbd36dbfbcf1c8cd415 Mon Sep 17 00:00:00 2001 From: David Leong Date: Fri, 31 Jul 2026 00:05:41 +0000 Subject: [PATCH 2/6] fix: correct the moonray rocky9 source build and document mbind The MoonRay build step configured cmake by hand and never pointed at the dependencies built by the previous step, so the build failed after about 50 minutes with: CMake Error ... Could NOT find JsonCpp (missing: JsonCpp_LIBRARIES) building/Rocky9/CMakeLists.txt installs the dependency stack to /opt/MoonRay/installs (its InstallRoot default), not to /installs. The official docs configure MoonRay through the rocky9-release preset in CMakeLinuxPresets.json, which supplies CMAKE_PREFIX_PATH and the per-dependency *_ROOT variables pointing there, along with the /build binary dir, the /installs/openmoonray install prefix, and PYTHON_EXECUTABLE / BOOST_PYTHON_COMPONENT_NAME / ABI_VERSION. Use the preset. Its build preset runs the install target, so the separate cmake --install is gone. The image then built but could not render: moonray defaults to -auto_affinity on, which calls mbind(2), and Docker's default seccomp profile only permits that syscall with CAP_SYS_NICE, so a plain docker run aborted at render startup with numaNodeMBInd() sysCallMBind() failed. numaNodeId:0 size:33554432 The upstream docs pass --security-opt seccomp=unconfined. The README now documents two narrower options and uses --cap-add SYS_NICE in the docker run examples; -auto_affinity off also works with no capability change. hd_render needs neither. Also pin OPENMOONRAY_REF to the released tag v2026.29.1 instead of main so a rebuild reproduces the same MoonRay, and record the requested ref and resolved commit in the image at /openmoonray-ref.txt. Verified on x86_64: image builds (7.43 GB, v2026.29.1 d96c6e30), moonray renders testdata/rectangle.rdla in 3.9s and hd_render renders testdata/sphere.usd, both to valid EXRs. --- containers/moonray/rocky9-cpu/Dockerfile | 45 +++++++++++++----- containers/moonray/rocky9-cpu/README.md | 60 ++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/containers/moonray/rocky9-cpu/Dockerfile b/containers/moonray/rocky9-cpu/Dockerfile index 12f02289..0b25bbad 100644 --- a/containers/moonray/rocky9-cpu/Dockerfile +++ b/containers/moonray/rocky9-cpu/Dockerfile @@ -24,14 +24,28 @@ RUN dnf install -y git git-lfs \ && git lfs install ARG OPENMOONRAY_REPO=https://github.com/dreamworksanimation/openmoonray.git -ARG OPENMOONRAY_REF=main + +# Which MoonRay version to build. Defaults to a released tag rather than a +# branch so that rebuilding this Dockerfile later produces the same MoonRay. +# Pick another version from the tag list: +# https://github.com/dreamworksanimation/openmoonray/tags +# Upstream publishes two tag series: the older openmoonray-..0.0 +# scheme (up to openmoonray-3.6.0.1) and the current date-based v.. +# scheme. Override with, for example: +# docker build --platform linux/amd64 \ +# --build-arg OPENMOONRAY_REF=openmoonray-3.6.0.1 \ +# -t openmoonray-rocky9 . +# This is passed to `git clone --branch`, so it accepts a tag or a branch name +# ("main" for the tip of development) but not a bare commit sha. +ARG OPENMOONRAY_REF=v2026.29.1 # The moonray/materialx_shaders submodule points at a repository that is not # public (404), so deactivate it and fetch the rest of the submodules. RUN git clone --branch "${OPENMOONRAY_REF}" "${OPENMOONRAY_REPO}" /source \ && cd /source \ && git config submodule."moonray/materialx_shaders".update none \ - && git submodule update --init --recursive + && git submodule update --init --recursive \ + && echo "${OPENMOONRAY_REF} $(git rev-parse HEAD)" > /openmoonray-ref.txt # Step 1: install the system packages MoonRay's build needs. RUN source /source/building/Rocky9/install_packages.sh --nocuda \ @@ -50,16 +64,23 @@ RUN mkdir /build \ && cmake --build . -- -j "$(nproc)" # Step 3: build and install MoonRay itself. -RUN cd /build \ - && rm -rf ./* \ - && cmake /source \ - -DPYTHON_EXECUTABLE=python3 \ - -DBOOST_PYTHON_COMPONENT_NAME=python39 \ - -DABI_VERSION=0 \ - -DMOONRAY_USE_OPTIX=NO \ - && cmake --build . -j "$(nproc)" \ - && mkdir -p /installs/openmoonray \ - && cmake --install /build --prefix /installs/openmoonray \ +# +# This goes through the rocky9-release CMake preset (CMakeLinuxPresets.json), +# as the official docs do, rather than a hand-written cmake invocation. The +# preset is what connects this step to step 2: the dependencies install to +# /opt/MoonRay/installs (the InstallRoot default in +# building/Rocky9/CMakeLists.txt), and the preset sets CMAKE_PREFIX_PATH and +# the per-dependency *_ROOT variables that point there. Configuring without +# it fails immediately at the first dependency lookup: +# CMake Error ... Could NOT find JsonCpp (missing: JsonCpp_LIBRARIES) +# The preset also sets the build directory to /build, the install prefix to +# /installs/openmoonray, and PYTHON_EXECUTABLE / BOOST_PYTHON_COMPONENT_NAME / +# ABI_VERSION; its build preset runs the `install` target, so there is no +# separate `cmake --install`. +RUN rm -rf /build/* \ + && cd /source \ + && cmake --preset rocky9-release -DMOONRAY_USE_OPTIX=NO \ + && cmake --build --preset rocky9-release -- -j "$(nproc)" \ && rm -rf /build # setup.sh puts moonray, moonray_gui, and hd_render on PATH. diff --git a/containers/moonray/rocky9-cpu/README.md b/containers/moonray/rocky9-cpu/README.md index 0811fe94..4740c575 100644 --- a/containers/moonray/rocky9-cpu/README.md +++ b/containers/moonray/rocky9-cpu/README.md @@ -23,8 +23,35 @@ The resulting image needs no snapd, no systemd, and no `--privileged` — MoonRa docker build --platform linux/amd64 -t openmoonray-rocky9 . ``` -The MoonRay git ref defaults to the `main` branch; override it with -`--build-arg OPENMOONRAY_REF=`. +### Choosing a MoonRay version + +`OPENMOONRAY_REF` selects which MoonRay to build. It defaults to the released tag +`v2026.29.1` rather than a branch, so rebuilding this `Dockerfile` months from now produces the +same MoonRay instead of whatever `main` happens to be that day. + +Available versions are listed on the +[openmoonray tags page](https://github.com/dreamworksanimation/openmoonray/tags) (also on the +[releases page](https://github.com/dreamworksanimation/openmoonray/releases)). Upstream has used +two tag series: the older `openmoonray-..0.0` scheme, which ends at +`openmoonray-3.6.0.1`, and the current date-based `v..` scheme. + +```console +docker build --platform linux/amd64 \ + --build-arg OPENMOONRAY_REF=openmoonray-3.6.0.1 \ + -t openmoonray-rocky9:3.6.0.1 . +``` + +The value goes to `git clone --branch`, so a tag or a branch name works — pass `main` to build the +tip of development — but a bare commit sha does not. Older tags are not tested by this sample and +may need different system packages than `building/Rocky9/install_packages.sh` installs at the +pinned version. + +The version that was actually built is recorded in the image at `/openmoonray-ref.txt`, as the +requested ref plus the commit it resolved to: + +```console +docker run --rm openmoonray-rocky9 'cat /openmoonray-ref.txt' +``` Two deviations from the official docs, both encoded in the `Dockerfile` with comments: @@ -33,6 +60,31 @@ Two deviations from the official docs, both encoded in the `Dockerfile` with com * cmake is installed from pip (`pip3 install "cmake<4"`): the EPEL cmake crashes in libuv's signal handling when the build runs under x86_64 emulation on an arm64 host. +MoonRay itself is configured through upstream's `rocky9-release` CMake preset, as the official +docs do. The preset is what links step 3 to step 2: the dependencies install to +`/opt/MoonRay/installs`, and the preset supplies the `CMAKE_PREFIX_PATH` and per-dependency +`*_ROOT` variables that point there. Configuring by hand without it stops at +`Could NOT find JsonCpp`. + +## Memory affinity and `mbind` + +`moonray` sets memory affinity by default (`-auto_affinity on`), which calls `mbind(2)`. Docker's +default seccomp profile only permits that syscall with `CAP_SYS_NICE`, so a plain `docker run` +aborts during render startup: + +``` +what(): numaNodeMBInd() sysCallMBind() failed. numaNodeId:0 size:33554432 +``` + +The official docs work around this with `--security-opt seccomp=unconfined`. Two narrower options +work as well, and the `docker run` commands below use the first: + +* `--cap-add SYS_NICE` — keeps the default seccomp profile and leaves affinity control enabled. +* `-auto_affinity off` on the `moonray` command line — no added capability or relaxed sandbox, at + the cost of NUMA-aware allocation. Sensible on a single-socket machine. + +`hd_render` is unaffected and needs neither. + ## Where to get the scenes No scene files ship with this sample. There are two sources: @@ -52,7 +104,7 @@ The source tree (kept at `/source` in the image) includes small test scenes: ```console mkdir -p output -docker run --rm -v "$(pwd)/output:/output" openmoonray-rocky9 \ +docker run --rm --cap-add SYS_NICE -v "$(pwd)/output:/output" openmoonray-rocky9 \ 'moonray -in /source/testdata/rectangle.rdla -out /output/rectangle.exr' ``` @@ -64,7 +116,7 @@ Download and unpack the example scenes, then mount them into the container: curl -LO https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip mkdir -p scenes output unzip example_scenes.zip -d scenes/ -docker run --rm -v "$(pwd)/scenes:/scenes" -v "$(pwd)/output:/output" \ +docker run --rm --cap-add SYS_NICE -v "$(pwd)/scenes:/scenes" -v "$(pwd)/output:/output" \ -w /scenes/example_scenes/pbrt_scenes/veach-mis openmoonray-rocky9 \ 'moonray -in scene.rdla -in scene.rdlb -exec_mode scalar -out /output/veach-mis.exr' ``` From e8ee84235446310b1599587e92ecd01167ddcc4a Mon Sep 17 00:00:00 2001 From: David Leong Date: Fri, 31 Jul 2026 00:07:30 +0000 Subject: [PATCH 3/6] docs: clarify that CAP_SYS_NICE is a moonray run-time requirement The mbind section sat directly after the Build section, which read as a build requirement. It is not: docker build needs no added capability or seccomp change. Move the section down next to the render commands, retitle it, and say so outright. --- containers/moonray/rocky9-cpu/README.md | 37 +++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/containers/moonray/rocky9-cpu/README.md b/containers/moonray/rocky9-cpu/README.md index 4740c575..f0f513c3 100644 --- a/containers/moonray/rocky9-cpu/README.md +++ b/containers/moonray/rocky9-cpu/README.md @@ -66,11 +66,27 @@ docs do. The preset is what links step 3 to step 2: the dependencies install to `*_ROOT` variables that point there. Configuring by hand without it stops at `Could NOT find JsonCpp`. -## Memory affinity and `mbind` +## Where to get the scenes + +No scene files ship with this sample. There are two sources: + +* **Bundled test scenes** — small `.rdla` and `.usd` files already present in the image at + `/source/testdata`, carried in from the + [MoonRay source tree](https://github.com/dreamworksanimation/openmoonray). Nothing to download. +* **Official example scenes** — the larger `pbrt_scenes` set, published by MoonRay as + [example_scenes.zip](https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip) on the + [test scenes page](https://docs.openmoonray.org/getting-started/test-scenes/). Download it + yourself; it unpacks to roughly 700 MB, and the sample's `.gitignore` keeps `scenes/`, + `output/`, and the zip out of git. + +## Running moonray needs `CAP_SYS_NICE` + +This is a run-time requirement only. `docker build` needs nothing beyond the flags shown above. -`moonray` sets memory affinity by default (`-auto_affinity on`), which calls `mbind(2)`. Docker's -default seccomp profile only permits that syscall with `CAP_SYS_NICE`, so a plain `docker run` -aborts during render startup: +`moonray` sets memory affinity by default (`-auto_affinity on`), which calls `mbind(2)` to bind +memory to a NUMA node. Docker's default seccomp profile permits that syscall only when the +container has `CAP_SYS_NICE`, so under a plain `docker run` the container starts and the scene +loads, then the render thread aborts as it initializes: ``` what(): numaNodeMBInd() sysCallMBind() failed. numaNodeId:0 size:33554432 @@ -85,19 +101,6 @@ work as well, and the `docker run` commands below use the first: `hd_render` is unaffected and needs neither. -## Where to get the scenes - -No scene files ship with this sample. There are two sources: - -* **Bundled test scenes** — small `.rdla` and `.usd` files already present in the image at - `/source/testdata`, carried in from the - [MoonRay source tree](https://github.com/dreamworksanimation/openmoonray). Nothing to download. -* **Official example scenes** — the larger `pbrt_scenes` set, published by MoonRay as - [example_scenes.zip](https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip) on the - [test scenes page](https://docs.openmoonray.org/getting-started/test-scenes/). Download it - yourself; it unpacks to roughly 700 MB, and the sample's `.gitignore` keeps `scenes/`, - `output/`, and the zip out of git. - ## Render the bundled test scene The source tree (kept at `/source` in the image) includes small test scenes: From cd1653877091a02bb9c7c6b2c27f6bbcf70715da Mon Sep 17 00:00:00 2001 From: David Leong Date: Fri, 31 Jul 2026 00:08:28 +0000 Subject: [PATCH 4/6] docs: list the moonray sample in the containers index The containers/ sample index still listed only AL2023 and Blender, and described itself as covering "both" samples, so the moonray sample added on this branch was unreachable from it. --- containers/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/containers/README.md b/containers/README.md index dc13b01c..82ffa51e 100644 --- a/containers/README.md +++ b/containers/README.md @@ -4,11 +4,12 @@ These samples provide Dockerfiles and related resources for building container i ## Sample index -This table covers both user-selectable container samples below `containers/`. Supporting scripts and image assets remain with their sample. +This table covers the user-selectable container samples below `containers/`. Supporting scripts and image assets remain with their sample. | Sample | What it demonstrates | Start here when | |---|---|---| | [AL2023 worker-equivalent image](al2023-deadline/) | Reproducing a point-in-time service-managed fleet package set on Amazon Linux 2023 | You need to test packages or software against worker-compatible system libraries | | [Blender application container](blender/blender-aswf-ci-base/) | Packaging Blender, the Deadline Cloud adaptor, and GPU support in an application image | You want to render Blender workloads from a purpose-built container | +| [MoonRay application container](moonray/) | Compiling MoonRay from source for CPU rendering on Rocky Linux 9 | You want to render with MoonRay, or need a specific MoonRay revision | -The worker-equivalent image is useful for local compatibility work and package builds. The Blender image is an application-container example and includes its own deployment resources and instructions. +The worker-equivalent image is useful for local compatibility work and package builds. The Blender and MoonRay images are application-container examples; Blender includes its own deployment resources and instructions, and MoonRay builds the renderer from source. From e72c22f6430081779ee021c8eed7fa971589228c Mon Sep 17 00:00:00 2001 From: David Leong Date: Fri, 31 Jul 2026 00:33:05 +0000 Subject: [PATCH 5/6] feat: add an OpenJD sample that renders moonray in a container Adds containers/moonray/templates/, an Open Job Description job that renders a MoonRay example scene inside the rocky9-cpu image using the WRAP_ACTIONS and EXPR extensions. The container concern is kept out of the job template. moonray-render-job.yaml declares only EXPR and describes the render as a plain `moonray -in ... -out ...` invocation, so it also runs unchanged on a host with moonray on PATH. local-docker-wrap-env.yaml supplies the three WRAP_ACTIONS hooks that re-run each action via `docker run`, forwarding the wrapped command with repr_sh() as the single argument the image's `bash -lc` ENTRYPOINT expects. On a farm the same hooks would arrive as a queue environment instead. run-render.sh downloads the official example scenes, points TMPDIR at ./sessions so session working directories land beside the templates rather than in /tmp, bind mounts that directory into the container, and runs the job with the Python CLI, the Rust CLI, or both, verifying the .exr artifact rather than trusting the exit code. The hooks pass --cap-add SYS_NICE: MoonRay binds NUMA memory with mbind(2), which docker's default seccomp profile blocks, and without the capability every render aborts in initTLS. Verified with the Rust openjd CLI on a 16-core x86_64 host: veach-mis renders in 149s to a valid 1280x720 RGBA OpenEXR, within 0.5% of the reference scene.exr shipped with the scene. Both templates pass `openjd check` on the Python and Rust implementations. --- containers/moonray/templates/.gitignore | 7 + containers/moonray/templates/README.md | 175 +++++++++++++++ .../templates/local-docker-wrap-env.yaml | 166 +++++++++++++++ .../moonray/templates/moonray-render-job.yaml | 95 +++++++++ containers/moonray/templates/run-render.sh | 201 ++++++++++++++++++ 5 files changed, 644 insertions(+) create mode 100644 containers/moonray/templates/.gitignore create mode 100644 containers/moonray/templates/README.md create mode 100644 containers/moonray/templates/local-docker-wrap-env.yaml create mode 100644 containers/moonray/templates/moonray-render-job.yaml create mode 100755 containers/moonray/templates/run-render.sh diff --git a/containers/moonray/templates/.gitignore b/containers/moonray/templates/.gitignore new file mode 100644 index 00000000..ee642ea6 --- /dev/null +++ b/containers/moonray/templates/.gitignore @@ -0,0 +1,7 @@ +# The session root: session working directories, unpacked scenes, rendered +# output and run logs. All generated, none of it belongs in git. +sessions/ + +# The example scenes archive (~700 MB) is downloaded on demand by run-render.sh. +example_scenes.zip +example_scenes.zip.part diff --git a/containers/moonray/templates/README.md b/containers/moonray/templates/README.md new file mode 100644 index 00000000..b02f4def --- /dev/null +++ b/containers/moonray/templates/README.md @@ -0,0 +1,175 @@ +# MoonRay in a container, as an Open Job Description job + +An [Open Job Description](https://github.com/OpenJobDescription) job that renders a MoonRay example +scene inside the [`rocky9-cpu`](../rocky9-cpu/) image, using the `WRAP_ACTIONS` and `EXPR` +extensions to put the render in a container without the job template knowing anything about +containers. + +The job template describes a render. The wrap environment describes a container. They are separate +files on purpose: the same job runs unchanged on a host with `moonray` on `PATH`, or on a render +farm where the container comes from a queue environment. + +## Files + +| File | Purpose | +|---|---| +| `moonray-render-job.yaml` | The job. `EXPR` only — no mention of docker. | +| `local-docker-wrap-env.yaml` | Environment template with the `WRAP_ACTIONS` hooks that run each action in a container on this workstation. | +| `run-render.sh` | Downloads the scenes, then runs the job with the Python CLI, the Rust CLI, or both. | +| `.gitignore` | Keeps `sessions/` and the scene archive out of git. | + +## Prerequisites + +* Docker, with the `openmoonray-rocky9` image built from the sibling sample: + + ```console + cd ../rocky9-cpu + docker build --platform linux/amd64 -t openmoonray-rocky9 . + ``` + + That build compiles MoonRay from source and takes hours. See its + [README](../rocky9-cpu/README.md). + +* An `openjd` CLI on `PATH` — either + [openjd-cli](https://github.com/OpenJobDescription/openjd-cli) (Python) or + [openjd-rs](https://github.com/OpenJobDescription/openjd-rs) (Rust). Both are supported and + produce identical container invocations. + +* `curl` and `unzip`, for fetching the example scenes. + +## Quick start + +```console +./run-render.sh rust # render with the Rust openjd CLI +./run-render.sh python # render with the Python openjd-cli +./run-render.sh # both, one after the other +./run-render.sh --fetch-only # just download and unpack the scenes +``` + +On the first run this downloads +[example_scenes.zip](https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip) +(379 MB, unpacking to ~705 MB) into `sessions/scenes/`. Later runs reuse it. + +Output lands in `sessions/output/-.exr`, prefixed per implementation so the two runs +do not overwrite each other. + +Overrides, as environment variables: + +| Variable | Default | Meaning | +|---|---|---| +| `IMAGE` | `openmoonray-rocky9` | Image to run | +| `SCENE` | `veach-mis` | Scene under `pbrt_scenes/` | +| `EXEC_MODE` | `scalar` | `scalar` or `vectorized` | +| `DOCKER_USER` | invoking user | `docker --user` value | +| `KEEP_SESSIONS` | `1` | Pass `--preserve` to keep session dirs | + +## Running it by hand + +`run-render.sh` is a convenience wrapper. The underlying invocation is: + +```console +openjd run moonray-render-job.yaml \ + --environment local-docker-wrap-env.yaml \ + --step Render \ + -p SessionsDir="$PWD/sessions" \ + -p ContainerMount=/mnt/session \ + -p OutputPrefix=rust +``` + +## How it works + +`WRAP_ACTIONS` lets one environment replace the lifecycle actions of everything inside it. Each of +the three hooks — `onWrapEnvEnter`, `onWrapTaskRun`, `onWrapEnvExit`, which must all be defined +together — receives the action it replaced through `WrappedAction.*` variables, and here re-runs it +with `docker run`. + +So the job's `onRun`: + +```yaml +command: moonray +args: [-in, "{{ scene_dir }}/scene.rdla", ..., -out, "{{ out_exr }}"] +``` + +becomes, at run time: + +```console +docker run --rm --cap-add SYS_NICE --platform linux/amd64 --user : \ + -e HOME=/tmp -v /sessions:/mnt/session openmoonray-rocky9 \ + 'env moonray -in /mnt/session/scenes/.../veach-mis/scene.rdla \ + -in /mnt/session/scenes/.../veach-mis/scene.rdlb \ + -exec_mode scalar -out /mnt/session/output/rust-veach-mis.exr' +``` + +Three details in that command are load-bearing: + +* **One argument after the image name.** The image's `ENTRYPOINT` is `bash -lc`, which takes the + whole command as a single string. The hook composes one string rather than separate argv entries. +* **`repr_sh()`**, from `EXPR`, shell-quotes the forwarded command and args so metacharacters, + spaces and quotes reach the process verbatim instead of being interpreted by that login shell. +* **`env` prefix** applies any session-defined variables (`WrappedAction.Environment`) inside the + container. With none defined the list is empty and `env` is a passthrough. + +`EXPR` also supplies the job's `let` bindings, which build the in-container paths once from +`Task.Param.Scene`. + +The wrap environment's own `onEnter`/`onExit` run on the **host**, not in a container — a wrap +environment's own lifecycle is never intercepted by its own hooks. `onEnter` uses that to check the +image exists up front, so a missing image fails once with a clear message instead of once per task. + +## Notes and gotchas + +**`--cap-add SYS_NICE` is required.** MoonRay binds NUMA memory with `mbind(2)`, which Docker's +default seccomp profile blocks unless the container has `CAP_SYS_NICE`. Without it every render +aborts during thread-local setup: + +``` +terminate called after throwing an instance of 'scene_rdl2::except::RuntimeError' + what(): numaNodeMBInd() sysCallMBind() failed. numaNodeId:0 size:33554432 +``` + +This affects plain `docker run` too, not just this job — including the render command in the +`rocky9-cpu` README. + +**Two harmless log lines when running non-root.** `docker --user` with a uid that has no entry in +the image's `/etc/passwd` produces `id: cannot find name for user ID `, and MoonRay logs +`ERROR: boost::filesystem::create_directory: Permission denied: +"/installs/openmoonray/shader_json/"`. Neither stops the render. Set `DOCKER_USER=0:0` to run as +root and silence both, at the cost of root-owned files in `sessions/output/`. + +**Session directories.** Neither CLI exposes a session-directory flag; both derive the session root +from the system temp dir on POSIX. `run-render.sh` therefore sets `TMPDIR` to `sessions/`, so +session working directories appear as `sessions/OpenJD/…` rather than in `/tmp`. The whole +`sessions/` directory is what gets bind mounted, which is how the container sees the scenes and how +output gets back to the host. + +**Parameters shared across the two templates.** `ContainerMount` is declared in both files with the +same type and default. That is legal — the CLI merges job and environment template parameter +definitions into one parameter space — and it means one `-p ContainerMount=…` moves the mount target +and the job's paths together instead of letting them drift. It is also why `-p SessionsDir=…` +works at all: `SessionsDir` is declared by the *environment* template, not the job. + +**GPU.** The image is built `--nocuda`, so `EXEC_MODE=xpu` is not available. + +## Verified run + +`./run-render.sh rust` on a 16-core x86_64 Linux host, Rust `openjd` CLI, docker 25.0: + +``` +IMPL RESULT SECONDS EXR_BYTES LOG +rust PASS 149 5826348 sessions/logs/render-rust.log +``` + +Produced `sessions/output/rust-veach-mis.exr`: + +| Property | Value | +|---|---| +| Format | OpenEXR, version 2 | +| Resolution | 1280 × 720 | +| Channels | A, B, G, R | +| Compression | ZIP | +| Size | 5,826,348 bytes | + +MoonRay reported `Render time = 00:02:26.97` inside the container, against 149 s of wall time for +the whole session — so container startup, scene load and session setup account for roughly two +seconds. For reference, the `scene.exr` that ships alongside `veach-mis` is 5,798,529 bytes, within +0.5% of what this render produced. diff --git a/containers/moonray/templates/local-docker-wrap-env.yaml b/containers/moonray/templates/local-docker-wrap-env.yaml new file mode 100644 index 00000000..1277d47f --- /dev/null +++ b/containers/moonray/templates/local-docker-wrap-env.yaml @@ -0,0 +1,166 @@ +# Container wrap environment for LOCAL runs — WRAP_ACTIONS + EXPR. +# +# "local" as in this workstation: it shells out to the docker CLI on the +# machine running openjd, and bind mounts a host directory. It is applied ad +# hoc with `openjd run --environment ...` (see run-render.sh). On a render farm +# the same WRAP_ACTIONS hooks would normally arrive as a queue environment +# managed by the farm rather than from a file next to the job. +# +# This is the piece that puts the job inside a container. WRAP_ACTIONS lets an +# environment replace the lifecycle actions of everything inside it: each hook +# below runs *instead of* the action it wraps, receiving the original action +# through the WrappedAction.* variables. Here every hook re-runs the original +# command inside openmoonray-rocky9, with the host session directory bind +# mounted so the scenes are visible and the rendered .exr lands back on the +# host. +# +# All three hooks must be defined together — WRAP_ACTIONS is all-or-nothing — +# and only one environment in a session may define them. +specificationVersion: environment-2023-09 + +extensions: +- WRAP_ACTIONS +- EXPR + +parameterDefinitions: +# The host directory to bind mount. PATH (not STRING) so that a relative +# default resolves against this template's directory, giving ./sessions +# without anyone hardcoding an absolute path. run-render.sh passes an absolute +# path explicitly rather than relying on that resolution. +- name: SessionsDir + type: PATH + default: sessions + description: Host directory exposed to the container; holds scenes/, output/ and the session working dirs. + +# Also declared in moonray-render-job.yaml with the same type and default, so +# one -p sets the mount target and the job's in-container paths together. +- name: ContainerMount + type: STRING + default: /mnt/session + description: Mount point for SessionsDir inside the container. + +- name: Image + type: STRING + default: openmoonray-rocky9 + description: Container image holding the MoonRay build. + +- name: Platform + type: STRING + default: linux/amd64 + description: The MoonRay image is x86_64 only. + +- name: DockerUser + type: STRING + default: "0:0" + description: >- + Passed to docker --user, as UID:GID. run-render.sh defaults this to the + invoking user so rendered output is not left root-owned on the host. + Running non-root logs a harmless "create_directory: Permission denied: + /installs/openmoonray/shader_json/" — MoonRay renders anyway. Set 0:0 if a + scene needs to write inside the image. + +environment: + name: MoonRayDocker + script: + let: + - mount_spec = string(Param.SessionsDir) + ':' + Param.ContainerMount + + actions: + # onEnter and onExit belong to the wrapping environment itself, and a + # wrap environment's own lifecycle is never intercepted by its own + # hooks — so these two run on the HOST, not in the container. onEnter is + # a fail-fast check: without it, a missing image surfaces once per task + # as a confusing docker error instead of once, up front. + onEnter: + command: bash + args: + - -c + - >- + if docker image inspect {{ repr_sh(Param.Image) }} >/dev/null 2>&1; + then echo "wrap-env: image {{ Param.Image }} present, mounting {{ mount_spec }}"; + else echo "wrap-env: image {{ Param.Image }} not found - build the sibling rocky9-cpu sample first" >&2; exit 1; + fi + + # Replaces the onEnter of every environment inside this one. + onWrapEnvEnter: + command: docker + args: + - run + - --rm + # MoonRay binds NUMA memory with mbind(2), which Docker's default + # seccomp profile blocks unless the container has CAP_SYS_NICE. Without + # this the render aborts in initTLS with + # "numaNodeMBInd() sysCallMBind() failed". + - --cap-add + - SYS_NICE + - --platform + - "{{ Param.Platform }}" + - --user + - "{{ Param.DockerUser }}" + - -e + - HOME=/tmp + - -v + - "{{ mount_spec }}" + - "{{ Param.Image }}" + # The image's ENTRYPOINT is `bash -lc`, which takes the whole command + # as ONE argument — hence a single composed string here rather than + # separate argv entries. repr_sh() shell-quotes the forwarded command + # and args so metacharacters, spaces and quotes reach the process + # verbatim instead of being interpreted by that login shell. The `env` + # prefix applies any session-defined variables inside the container; + # with none defined the list is empty and `env` is a no-op passthrough. + - "env {{ repr_sh(WrappedAction.Environment) }} {{ repr_sh(WrappedAction.Command) }} {{ repr_sh(WrappedAction.Args) }}" + + # Replaces the onRun of every task — this is the hook that renders. + onWrapTaskRun: + command: docker + args: + - run + - --rm + # MoonRay binds NUMA memory with mbind(2), which Docker's default + # seccomp profile blocks unless the container has CAP_SYS_NICE. Without + # this the render aborts in initTLS with + # "numaNodeMBInd() sysCallMBind() failed". + - --cap-add + - SYS_NICE + - --platform + - "{{ Param.Platform }}" + - --user + - "{{ Param.DockerUser }}" + - -e + - HOME=/tmp + - -v + - "{{ mount_spec }}" + - "{{ Param.Image }}" + - "env {{ repr_sh(WrappedAction.Environment) }} {{ repr_sh(WrappedAction.Command) }} {{ repr_sh(WrappedAction.Args) }}" + + # Replaces the onExit of every environment inside this one. + onWrapEnvExit: + command: docker + args: + - run + - --rm + # MoonRay binds NUMA memory with mbind(2), which Docker's default + # seccomp profile blocks unless the container has CAP_SYS_NICE. Without + # this the render aborts in initTLS with + # "numaNodeMBInd() sysCallMBind() failed". + - --cap-add + - SYS_NICE + - --platform + - "{{ Param.Platform }}" + - --user + - "{{ Param.DockerUser }}" + - -e + - HOME=/tmp + - -v + - "{{ mount_spec }}" + - "{{ Param.Image }}" + - "env {{ repr_sh(WrappedAction.Environment) }} {{ repr_sh(WrappedAction.Command) }} {{ repr_sh(WrappedAction.Args) }}" + + onExit: + command: bash + args: + - -c + # Quoted as a YAML string: an unquoted scalar containing ": " would be + # parsed as a mapping, not a command. + - 'echo "wrap-env: done"' diff --git a/containers/moonray/templates/moonray-render-job.yaml b/containers/moonray/templates/moonray-render-job.yaml new file mode 100644 index 00000000..1fcba3cf --- /dev/null +++ b/containers/moonray/templates/moonray-render-job.yaml @@ -0,0 +1,95 @@ +# MoonRay render job — Open Job Description 2023-09, EXPR extension. +# +# Nothing in this template mentions docker. The commands below are written as +# if they already run inside the openmoonray-rocky9 container, and the +# WRAP_ACTIONS environment in local-docker-wrap-env.yaml is what actually puts them +# there: it intercepts this step's onRun and re-executes it via `docker run`, +# forwarding command and args verbatim with EXPR's repr_sh(). +# +# Run it with run-render.sh, which supplies the parameters below and applies +# the wrap environment. Manual equivalent: +# +# openjd run moonray-render-job.yaml \ +# --environment local-docker-wrap-env.yaml \ +# -p SessionsDir= \ +# -p ContainerMount=/mnt/session \ +# -p OutputPrefix=python +# +specificationVersion: jobtemplate-2023-09 + +# EXPR provides the `let` bindings used below. WRAP_ACTIONS is declared by the +# environment template, not here — this job is deliberately unaware of it. +extensions: +- EXPR + +name: MoonRayRender +description: Renders a MoonRay example scene inside a container via WRAP_ACTIONS. + +parameterDefinitions: +# ContainerMount is declared in BOTH this template and local-docker-wrap-env.yaml, +# with the same type and default. That is legal: the CLI merges job and +# environment template parameter definitions into one parameter space, and +# definitions of the same name must agree on type (the last default wins). One +# `-p ContainerMount=...` therefore moves the bind-mount target and the paths +# below together, so the two templates cannot drift apart. +- name: ContainerMount + type: STRING + default: /mnt/session + description: Where the host session directory is mounted inside the container. + +# In-container paths are STRING, not PATH, on purpose. PATH parameters are +# resolved and validated against the *host* filesystem (and are subject to +# path mapping), which would rewrite or reject a container-only path. +- name: SceneSubdir + type: STRING + default: scenes/example_scenes/pbrt_scenes + description: Location of the unpacked example scenes, relative to ContainerMount. + +- name: OutputSubdir + type: STRING + default: output + description: Where .exr output is written, relative to ContainerMount. + +- name: ExecMode + type: STRING + default: scalar + allowedValues: + - scalar + - vectorized + description: MoonRay execution mode. The container is built --nocuda, so xpu is unavailable. + +- name: OutputPrefix + type: STRING + default: render + description: Filename prefix, so the python and rust runs do not overwrite each other. + +steps: +- name: Render + parameterSpace: + taskParameterDefinitions: + - name: Scene + type: STRING + range: + - veach-mis + script: + # EXPR let bindings. These live at script level rather than step level + # because they reference Task.Param.*, which step-level `let` cannot see. + # Binding names must be lowercase per the EXPR spec. + let: + - scene_dir = Param.ContainerMount + '/' + Param.SceneSubdir + '/' + Task.Param.Scene + - out_exr = Param.ContainerMount + '/' + Param.OutputSubdir + '/' + Param.OutputPrefix + '-' + Task.Param.Scene + '.exr' + actions: + onRun: + # This is the in-container invocation. The wrap hook receives it as + # WrappedAction.Command / WrappedAction.Args and forwards it verbatim. + # veach-mis ships as a .rdla plus a .rdlb, so both are passed. + command: moonray + args: + - -in + - "{{ scene_dir }}/scene.rdla" + - -in + - "{{ scene_dir }}/scene.rdlb" + - -exec_mode + - "{{ Param.ExecMode }}" + - -out + - "{{ out_exr }}" diff --git a/containers/moonray/templates/run-render.sh b/containers/moonray/templates/run-render.sh new file mode 100755 index 00000000..fa3e8e36 --- /dev/null +++ b/containers/moonray/templates/run-render.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# +# Render a MoonRay example scene through Open Job Description, once with the +# Python openjd-cli and once with the Rust openjd CLI. +# +# The job template carries no docker knowledge; local-docker-wrap-env.yaml supplies +# WRAP_ACTIONS hooks that re-run each action inside the openmoonray-rocky9 +# image with ./sessions bind mounted, so the scenes are visible inside the +# container and the .exr comes back out on the host. +# +# Usage: +# ./run-render.sh # both implementations +# ./run-render.sh python # Python openjd-cli only +# ./run-render.sh rust # Rust openjd only +# ./run-render.sh --fetch-only # download and unpack scenes, render nothing +# +# Environment overrides: +# IMAGE=openmoonray-rocky9 container image to run +# SCENE=veach-mis example scene to render +# EXEC_MODE=scalar scalar | vectorized +# DOCKER_USER=: defaults to the invoking user +# KEEP_SESSIONS=1 pass --preserve so session dirs survive (default 1) +# VENV=... RUST_BIN=... locations of the two implementations + +set -u -o pipefail + +TEMPLATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +JOB_TEMPLATE="$TEMPLATE_DIR/moonray-render-job.yaml" +WRAP_ENV="$TEMPLATE_DIR/local-docker-wrap-env.yaml" + +# Session root. Everything mutable lives under here, and this whole directory +# is what gets bind mounted into the container. Not checked into git. +SESSIONS_DIR="$TEMPLATE_DIR/sessions" +SCENES_DIR="$SESSIONS_DIR/scenes" +OUTPUT_DIR="$SESSIONS_DIR/output" +LOG_DIR="$SESSIONS_DIR/logs" + +# The zip is downloaded beside the templates; it unpacks into the session dir. +SCENES_URL="https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip" +SCENES_ZIP="$TEMPLATE_DIR/example_scenes.zip" + +IMAGE="${IMAGE:-openmoonray-rocky9}" +CONTAINER_MOUNT="${CONTAINER_MOUNT:-/mnt/session}" +SCENE="${SCENE:-veach-mis}" +EXEC_MODE="${EXEC_MODE:-scalar}" +DOCKER_USER="${DOCKER_USER:-$(id -u):$(id -g)}" +KEEP_SESSIONS="${KEEP_SESSIONS:-1}" + +VENV="${VENV:-$HOME/work/openjd/.venv}" +RUST_BIN="${RUST_BIN:-$HOME/work/openjd/openjd-rs/target/release}" + +die() { echo "error: $*" >&2; exit 1; } +note() { echo "==> $*"; } + +# ---------------------------------------------------------------- preflight -- +preflight() { + command -v docker >/dev/null 2>&1 || die "docker not found on PATH" + docker info >/dev/null 2>&1 || die "the docker daemon is not reachable" + command -v curl >/dev/null 2>&1 || die "curl not found on PATH" + command -v unzip >/dev/null 2>&1 || die "unzip not found on PATH" + + docker image inspect "$IMAGE" >/dev/null 2>&1 \ + || die "image '$IMAGE' not found. Build it first, from the sibling sample: + cd $(cd "$TEMPLATE_DIR/.." && pwd)/rocky9-cpu + docker build --platform linux/amd64 -t $IMAGE ." + + [ -f "$JOB_TEMPLATE" ] || die "missing job template: $JOB_TEMPLATE" + [ -f "$WRAP_ENV" ] || die "missing wrap environment: $WRAP_ENV" +} + +# ------------------------------------------------------------------- scenes -- +# Download the official example scenes and unpack them into the session dir, +# which is the directory the container sees. Both steps are skipped if already +# done, so re-running the script is cheap. +fetch_scenes() { + mkdir -p "$SCENES_DIR" "$OUTPUT_DIR" "$LOG_DIR" + + local scene_root="$SCENES_DIR/example_scenes/pbrt_scenes" + + if [ -d "$scene_root/$SCENE" ]; then + note "scenes already unpacked at $scene_root" + return 0 + fi + + if [ ! -f "$SCENES_ZIP" ]; then + note "downloading example scenes (~700 MB) to $SCENES_ZIP" + curl -fL --retry 3 -o "$SCENES_ZIP.part" "$SCENES_URL" \ + || die "download failed: $SCENES_URL" + mv "$SCENES_ZIP.part" "$SCENES_ZIP" + else + note "reusing existing $SCENES_ZIP" + fi + + note "unpacking into $SCENES_DIR" + unzip -q -o "$SCENES_ZIP" -d "$SCENES_DIR" || die "unzip failed" + + [ -d "$scene_root/$SCENE" ] \ + || die "scene '$SCENE' not found under $scene_root after unpacking" +} + +# -------------------------------------------------------------------- render -- +# $1 = impl label (python|rust), $2 = directory holding the openjd executable +render_with() { + local impl="$1" bindir="$2" + local log="$LOG_DIR/render-$impl.log" + local expected="$OUTPUT_DIR/$impl-$SCENE.exr" + + # Note: bash 4.2 (this host) treats "${arr[@]}" on an empty array as an + # unbound variable under `set -u`, hence the ${arr[@]+...} guards below. + local preserve=() + [ "$KEEP_SESSIONS" = "1" ] && preserve=(--preserve) + + rm -f "$expected" + + note "$impl: rendering $SCENE -> $(basename "$expected") (log: $log)" + + # Neither CLI exposes a session-directory flag; both derive the session + # root from the system temp dir on POSIX, so TMPDIR is what puts the + # session working directories under ./sessions (as ./sessions/OpenJD/...). + # + # The venv's bin stays on PATH for the Rust run too, so a bare `python` + # resolves identically in both. + local start end rc + start=$(date +%s) + ( + export TMPDIR="$SESSIONS_DIR" + export PATH="$bindir:$VENV/bin:$PATH" + openjd run "$JOB_TEMPLATE" \ + --environment "$WRAP_ENV" \ + --step Render \ + -p "SessionsDir=$SESSIONS_DIR" \ + -p "ContainerMount=$CONTAINER_MOUNT" \ + -p "Image=$IMAGE" \ + -p "DockerUser=$DOCKER_USER" \ + -p "ExecMode=$EXEC_MODE" \ + -p "OutputPrefix=$impl" \ + ${preserve[@]+"${preserve[@]}"} \ + --verbose + ) > "$log" 2>&1 + rc=$? + end=$(date +%s) + + echo "EXIT:$rc SECONDS:$((end - start))" >> "$log" + + # A zero exit code is not proof the render happened — check the artifact. + local size=0 + [ -f "$expected" ] && size=$(stat -c %s "$expected" 2>/dev/null || echo 0) + + if [ "$rc" -eq 0 ] && [ "$size" -gt 0 ]; then + note "$impl: OK $((end - start))s $(numfmt --to=iec "$size" 2>/dev/null || echo "$size B") $expected" + RESULTS+=("$impl|PASS|$((end - start))|$size|$log") + else + note "$impl: FAILED rc=$rc $((end - start))s exr_bytes=$size see $log" + RESULTS+=("$impl|FAIL|$((end - start))|$size|$log") + fi +} + +# ---------------------------------------------------------------------- main -- +TARGET="${1:-both}" + +case "$TARGET" in + --fetch-only) + fetch_scenes + note "fetch complete; nothing rendered" + exit 0 + ;; + python|rust|both) ;; + -h|--help) + sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + die "unknown target '$TARGET' (expected python, rust, both, or --fetch-only)" + ;; +esac + +preflight +fetch_scenes + +RESULTS=() + +case "$TARGET" in + python) render_with python "$VENV/bin" ;; + rust) render_with rust "$RUST_BIN" ;; + both) + render_with python "$VENV/bin" + render_with rust "$RUST_BIN" + ;; +esac + +echo +printf '%-8s %-6s %8s %14s %s\n' IMPL RESULT SECONDS EXR_BYTES LOG +failures=0 +for row in ${RESULTS[@]+"${RESULTS[@]}"}; do + IFS='|' read -r impl status secs bytes log <<< "$row" + printf '%-8s %-6s %8s %14s %s\n' "$impl" "$status" "$secs" "$bytes" "$log" + [ "$status" = PASS ] || failures=$((failures + 1)) +done + +exit $((failures > 0)) From 12e69089cfc0f6666fb14c287fd4a78e08f48516 Mon Sep 17 00:00:00 2001 From: David Leong Date: Fri, 31 Jul 2026 00:43:49 +0000 Subject: [PATCH 6/6] feat: make the openjd implementation an explicit toggle in run-render.sh The script already ran either CLI, but nothing recorded which one, so a log or an .exr could only be traced back to an implementation by recognising the log format. Each run now resolves its openjd up front and writes the resolved path and version into the top of its log, and echoes both while running. Adds a third choice, `path`, for a normally installed CLI rather than the two source-checkout locations that `python` and `rust` assume, and validates the selection before starting: a missing or mislocated binary now fails immediately naming the variable to set, instead of surfacing as `openjd: command not found` from inside a session. openjd-cli implements --version; the Rust CLI does not, so the version line says so rather than reporting nothing. Records both implementations in the README's verified run. python and rust render veach-mis to outputs that differ by exactly 3 bytes, all inside the EXR capDate header attribute - the capture timestamp. Every pixel is identical, so the wrap environment behaves the same under both. --- containers/moonray/templates/README.md | 35 +++++--- containers/moonray/templates/run-render.sh | 94 ++++++++++++++++++---- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/containers/moonray/templates/README.md b/containers/moonray/templates/README.md index b02f4def..fb0aa8ae 100644 --- a/containers/moonray/templates/README.md +++ b/containers/moonray/templates/README.md @@ -40,12 +40,19 @@ farm where the container comes from a queue environment. ## Quick start ```console -./run-render.sh rust # render with the Rust openjd CLI -./run-render.sh python # render with the Python openjd-cli -./run-render.sh # both, one after the other +./run-render.sh rust # render with the Rust openjd CLI, from $RUST_BIN +./run-render.sh python # render with the Python openjd-cli, from $VENV/bin +./run-render.sh path # use whichever openjd is already on PATH +./run-render.sh # python then rust, one after the other ./run-render.sh --fetch-only # just download and unpack the scenes ``` +`python` and `rust` look in the source-checkout locations in the table below, which suits a machine +with both built from source; `path` is for a normally installed CLI. Each run prints and logs the +`openjd` it resolved and its version, so an `.exr` can always be traced back to the CLI that made +it. A missing or mislocated CLI fails immediately with the variable to set, rather than surfacing as +`openjd: command not found` from inside a session. + On the first run this downloads [example_scenes.zip](https://docs.openmoonray.org/assets/test-scenes/example_scenes.zip) (379 MB, unpacking to ~705 MB) into `sessions/scenes/`. Later runs reuse it. @@ -62,6 +69,8 @@ Overrides, as environment variables: | `EXEC_MODE` | `scalar` | `scalar` or `vectorized` | | `DOCKER_USER` | invoking user | `docker --user` value | | `KEEP_SESSIONS` | `1` | Pass `--preserve` to keep session dirs | +| `VENV` | `~/work/openjd/.venv` | Venv holding the Python `openjd-cli` | +| `RUST_BIN` | `~/work/openjd/openjd-rs/target/release` | Directory holding the Rust `openjd` | ## Running it by hand @@ -152,14 +161,20 @@ works at all: `SessionsDir` is declared by the *environment* template, not the j ## Verified run -`./run-render.sh rust` on a 16-core x86_64 Linux host, Rust `openjd` CLI, docker 25.0: +Both implementations, on a 16-core x86_64 Linux host with docker 25.0: ``` IMPL RESULT SECONDS EXR_BYTES LOG +python PASS 153 5826348 sessions/logs/render-python.log rust PASS 149 5826348 sessions/logs/render-rust.log ``` -Produced `sessions/output/rust-veach-mis.exr`: +`openjd-cli 0.7.5.post21+g4e9a38421` (Python) and the Rust `openjd` built from `openjd-rs` +`af7e3c2`. The two outputs differ by exactly **3 bytes**, all inside the EXR `capDate` header +attribute — the capture timestamp. Every pixel is byte-identical, so the wrap environment behaves +the same under both implementations. + +Each render produced a `sessions/output/-veach-mis.exr`: | Property | Value | |---|---| @@ -169,7 +184,9 @@ Produced `sessions/output/rust-veach-mis.exr`: | Compression | ZIP | | Size | 5,826,348 bytes | -MoonRay reported `Render time = 00:02:26.97` inside the container, against 149 s of wall time for -the whole session — so container startup, scene load and session setup account for roughly two -seconds. For reference, the `scene.exr` that ships alongside `veach-mis` is 5,798,529 bytes, within -0.5% of what this render produced. +MoonRay reported `Render time = 00:02:26.97` inside the container on the Rust run, against 149 s of +wall time for the whole session — so container startup, scene load and session setup account for +roughly two seconds. The 4 s spread between the two implementations is render noise, not a +meaningful difference: the CLI does nothing but start one container per task. For reference, the +`scene.exr` that ships alongside `veach-mis` is 5,798,529 bytes, within 0.5% of what these renders +produced. diff --git a/containers/moonray/templates/run-render.sh b/containers/moonray/templates/run-render.sh index fa3e8e36..4735128c 100755 --- a/containers/moonray/templates/run-render.sh +++ b/containers/moonray/templates/run-render.sh @@ -8,19 +8,25 @@ # image with ./sessions bind mounted, so the scenes are visible inside the # container and the .exr comes back out on the host. # -# Usage: -# ./run-render.sh # both implementations -# ./run-render.sh python # Python openjd-cli only -# ./run-render.sh rust # Rust openjd only +# Which implementation runs is the first argument: +# ./run-render.sh # both, python first then rust +# ./run-render.sh python # openjd-cli (Python) from $VENV/bin +# ./run-render.sh rust # openjd (Rust) from $RUST_BIN +# ./run-render.sh path # whichever openjd is already on PATH # ./run-render.sh --fetch-only # download and unpack scenes, render nothing # +# python and rust look in the checkout locations below, which suit a machine +# with both built from source. Use `path` if you installed one of them normally. +# Each run records the openjd it resolved, and its version, at the top of its +# log — so which implementation produced a given .exr is never a guess. +# # Environment overrides: # IMAGE=openmoonray-rocky9 container image to run # SCENE=veach-mis example scene to render # EXEC_MODE=scalar scalar | vectorized # DOCKER_USER=: defaults to the invoking user # KEEP_SESSIONS=1 pass --preserve so session dirs survive (default 1) -# VENV=... RUST_BIN=... locations of the two implementations +# VENV=... RUST_BIN=... where to find the python / rust openjd set -u -o pipefail @@ -53,6 +59,40 @@ RUST_BIN="${RUST_BIN:-$HOME/work/openjd/openjd-rs/target/release}" die() { echo "error: $*" >&2; exit 1; } note() { echo "==> $*"; } +# ------------------------------------------------------- implementation pick -- +# Resolve the openjd executable for one implementation, failing early and +# specifically rather than letting `openjd: command not found` surface from +# inside a session. Sets OPENJD_DIR (empty means "leave PATH alone"), +# OPENJD_EXE and OPENJD_VERSION. +resolve_openjd() { + local impl="$1" dir="" hint="" + + case "$impl" in + python) dir="$VENV/bin" ; hint="set VENV=" ;; + rust) dir="$RUST_BIN" ; hint="set RUST_BIN=/target/release, or build it with: cargo build --release" ;; + path) dir="" ; hint="install openjd-cli, or put the openjd-rs binary on PATH" ;; + *) die "resolve_openjd: unknown implementation '$impl'" ;; + esac + + if [ -n "$dir" ]; then + OPENJD_EXE="$dir/openjd" + [ -x "$OPENJD_EXE" ] || die "no openjd executable at $OPENJD_EXE + $hint + or run ./run-render.sh path to use whichever openjd is on PATH" + else + OPENJD_EXE="$(command -v openjd 2>/dev/null || true)" + [ -n "$OPENJD_EXE" ] || die "no openjd found on PATH + $hint" + fi + + OPENJD_DIR="$dir" + + # openjd-cli implements --version; the Rust CLI currently does not, so fall + # back to naming the binary rather than reporting nothing. + OPENJD_VERSION="$("$OPENJD_EXE" --version 2>/dev/null | head -1)" + [ -n "$OPENJD_VERSION" ] || OPENJD_VERSION="(no --version flag; openjd-rs does not implement one)" +} + # ---------------------------------------------------------------- preflight -- preflight() { command -v docker >/dev/null 2>&1 || die "docker not found on PATH" @@ -100,12 +140,15 @@ fetch_scenes() { } # -------------------------------------------------------------------- render -- -# $1 = impl label (python|rust), $2 = directory holding the openjd executable +# $1 = impl label (python|rust|path) render_with() { - local impl="$1" bindir="$2" + local impl="$1" local log="$LOG_DIR/render-$impl.log" local expected="$OUTPUT_DIR/$impl-$SCENE.exr" + resolve_openjd "$impl" + local bindir="$OPENJD_DIR" + # Note: bash 4.2 (this host) treats "${arr[@]}" on an empty array as an # unbound variable under `set -u`, hence the ${arr[@]+...} guards below. local preserve=() @@ -113,8 +156,21 @@ render_with() { rm -f "$expected" + note "$impl: using $OPENJD_EXE" + note "$impl: version $OPENJD_VERSION" note "$impl: rendering $SCENE -> $(basename "$expected") (log: $log)" + # Record the resolved implementation at the top of the log, so a log or an + # .exr can always be traced back to the CLI that produced it. + { + echo "implementation: $impl" + echo "openjd: $OPENJD_EXE" + echo "version: $OPENJD_VERSION" + echo "scene: $SCENE exec_mode: $EXEC_MODE image: $IMAGE" + echo "started: $(date -Is)" + echo "----------------------------------------------------------------" + } > "$log" + # Neither CLI exposes a session-directory flag; both derive the session # root from the system temp dir on POSIX, so TMPDIR is what puts the # session working directories under ./sessions (as ./sessions/OpenJD/...). @@ -125,7 +181,12 @@ render_with() { start=$(date +%s) ( export TMPDIR="$SESSIONS_DIR" - export PATH="$bindir:$VENV/bin:$PATH" + # Put the chosen implementation first. For `path` there is nothing to + # prepend. The venv's bin follows when it exists, so the bare `python` + # some job fixtures spawn resolves the same way for every choice. + [ -n "$bindir" ] && PATH="$bindir:$PATH" + [ -d "$VENV/bin" ] && PATH="$PATH:$VENV/bin" + export PATH openjd run "$JOB_TEMPLATE" \ --environment "$WRAP_ENV" \ --step Render \ @@ -137,7 +198,7 @@ render_with() { -p "OutputPrefix=$impl" \ ${preserve[@]+"${preserve[@]}"} \ --verbose - ) > "$log" 2>&1 + ) >> "$log" 2>&1 rc=$? end=$(date +%s) @@ -165,13 +226,13 @@ case "$TARGET" in note "fetch complete; nothing rendered" exit 0 ;; - python|rust|both) ;; + python|rust|path|both) ;; -h|--help) - sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 ;; *) - die "unknown target '$TARGET' (expected python, rust, both, or --fetch-only)" + die "unknown target '$TARGET' (expected python, rust, path, both, or --fetch-only)" ;; esac @@ -181,11 +242,12 @@ fetch_scenes RESULTS=() case "$TARGET" in - python) render_with python "$VENV/bin" ;; - rust) render_with rust "$RUST_BIN" ;; + python) render_with python ;; + rust) render_with rust ;; + path) render_with path ;; both) - render_with python "$VENV/bin" - render_with rust "$RUST_BIN" + render_with python + render_with rust ;; esac