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. 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..0b25bbad --- /dev/null +++ b/containers/moonray/rocky9-cpu/Dockerfile @@ -0,0 +1,90 @@ +# 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 + +# 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 \ + && 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 \ + && 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. +# +# 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. +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..f0f513c3 --- /dev/null +++ b/containers/moonray/rocky9-cpu/README.md @@ -0,0 +1,138 @@ +# 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 . +``` + +### 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: + +* 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. + +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`. + +## 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)` 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 +``` + +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. + +## 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 --cap-add SYS_NICE -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 --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' +``` + +`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) 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..fb0aa8ae --- /dev/null +++ b/containers/moonray/templates/README.md @@ -0,0 +1,192 @@ +# 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, 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. + +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 | +| `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 + +`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 + +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 +``` + +`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 | +|---|---| +| 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 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/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..4735128c --- /dev/null +++ b/containers/moonray/templates/run-render.sh @@ -0,0 +1,263 @@ +#!/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. +# +# 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=... where to find the python / rust openjd + +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 "==> $*"; } + +# ------------------------------------------------------- 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" + 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|path) +render_with() { + 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=() + [ "$KEEP_SESSIONS" = "1" ] && preserve=(--preserve) + + 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/...). + # + # 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" + # 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 \ + -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|path|both) ;; + -h|--help) + sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + die "unknown target '$TARGET' (expected python, rust, path, both, or --fetch-only)" + ;; +esac + +preflight +fetch_scenes + +RESULTS=() + +case "$TARGET" in + python) render_with python ;; + rust) render_with rust ;; + path) render_with path ;; + both) + render_with python + render_with rust + ;; +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))