Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2824559
better EGL check handling
m-reuter Mar 6, 2026
027c8b7
better error log messages and exits
m-reuter Mar 6, 2026
714cfcf
improve detection of DISPLAY set but not working
m-reuter Mar 6, 2026
1d63aff
improve detection of DISPLAY set but not working
m-reuter Mar 6, 2026
a3185d6
improve detection of DISPLAY set but not working
m-reuter Mar 6, 2026
8800687
linux headless detect egl first
m-reuter Mar 6, 2026
ab7388a
remove duplicate log output for saving
m-reuter Mar 6, 2026
a0246c1
update README and DOCKER docs
m-reuter Mar 6, 2026
8f9eaa9
disable mesa warning output if home is not writeable (e.g. inside doc…
m-reuter Mar 6, 2026
84b8c7e
attempt for egl in docker
m-reuter Mar 6, 2026
184c88d
update docker offscreen test chain
m-reuter Mar 6, 2026
6c65622
further modification
m-reuter Mar 6, 2026
ee0c992
output EGL device and update docs
m-reuter Mar 6, 2026
3ac638e
allow --gpus all
m-reuter Mar 6, 2026
19db611
fix --gpus all path
m-reuter Mar 6, 2026
66cbf2f
add logging output to debug container GPU issue
m-reuter Mar 6, 2026
27d5119
revert nvidia stuff
m-reuter Mar 6, 2026
7b837e4
more fixing
m-reuter Mar 6, 2026
34b1467
more fixing
m-reuter Mar 6, 2026
45d145b
update log messages and documentation
m-reuter Mar 6, 2026
d5a083d
try to remove EGL warnings in singularity
m-reuter Mar 6, 2026
effabdc
try to remove EGL warnings in singularity
m-reuter Mar 6, 2026
cb5203e
fix typo
m-reuter Mar 6, 2026
9c764dc
update docstrings and guard mesa
m-reuter Mar 6, 2026
8ad4781
simplify docker (no osmesa etc needed)
m-reuter Mar 7, 2026
b8d370a
update Doc
m-reuter Mar 7, 2026
91a47e8
update dockerfile for GPUs and documentation
m-reuter Mar 8, 2026
3405175
fix typo update logger messages
m-reuter Mar 8, 2026
5677465
improve egl context detection
m-reuter Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 79 additions & 19 deletions DOCKER.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Docker Guide
# Docker / Singularity Guide

The Docker image provides a fully headless rendering environment. By default
it uses [OSMesa](https://docs.mesa3d.org/osmesa.html) (Mesa's CPU software
renderer) — no display server, `xvfb`, or GPU required. If a GPU render
device is available, pass `--device /dev/dri/renderD128` to enable EGL GPU
rendering instead; `libegl1` is already included in the image.
The Docker image provides a fully headless rendering environment using
**EGL** — no display server or `xvfb` required.

- **CPU rendering (default):** EGL falls back to Mesa's llvmpipe software
renderer automatically. No GPU or special flags needed.
- **GPU rendering (NVIDIA):**
- For **Docker**, pass `--gpus all` and EGL selects the
GPU via the NVIDIA Container Toolkit.
- For **Singularity/Apptainer**, pass `--nv` (NVIDIA) enables GPU
rendering via EGL automatically.

The default entry point is `whippersnap4` (four-view batch rendering).
`whippersnap1` (single-view snapshot and rotation video) can be invoked by
Expand Down Expand Up @@ -42,6 +47,26 @@ docker run --rm --init \
-o /output/snap4.png
```

### With NVIDIA GPU (faster rendering)

Pass `--gpus all` to let EGL use the GPU via the NVIDIA Container Toolkit:

```bash
docker run --rm --init \
--gpus all \
-v /path/to/subject:/subject \
-v /path/to/output:/output \
--user $(id -u):$(id -g) \
whippersnappy \
-lh /subject/surf/lh.thickness \
-rh /subject/surf/rh.thickness \
-sd /subject \
-o /output/snap4.png
```

> **Note:** Requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)
> installed on the host (`nvidia-ctk --version` to verify).

### With an annotation file instead of an overlay

```bash
Expand Down Expand Up @@ -181,6 +206,37 @@ parent directory to retrieve them on the host.

---

## Singularity / Apptainer

The same image can be used with Singularity or Apptainer.

**CPU rendering** (default — no GPU needed):
```bash
singularity exec \
-B /path/to/subject:/subject \
-B /path/to/output:/output \
whippersnappy.sif \
whippersnap4 \
-lh /subject/surf/lh.thickness \
-rh /subject/surf/rh.thickness \
-sd /subject -o /output/snap4.png
```

**GPU rendering** — pass `--nv` (NVIDIA) or `--rocm` (AMD); EGL selects
the GPU automatically:
```bash
singularity exec --nv \
-B /path/to/subject:/subject \
-B /path/to/output:/output \
whippersnappy.sif \
whippersnap4 \
-lh /subject/surf/lh.thickness \
-rh /subject/surf/rh.thickness \
-sd /subject -o /output/snap4.png
```

---

## Notes

- The `--init` flag is recommended so that signals (e.g. `Ctrl-C`) are handled
Expand All @@ -189,18 +245,22 @@ parent directory to retrieve them on the host.
not root.
- The interactive GUI (`whippersnap`) is **not** available in the Docker image —
it requires a display server and PyQt6, which are not installed.
- **Default rendering** uses **OSMesa** (Mesa's CPU software renderer, provided
by the `libosmesa6` system package). No GPU or `/dev/dri/` device needed.
- **GPU rendering via EGL** works out of the box — `libegl1` is included in the
image. Pass the render device into the container and WhipperSnapPy will
automatically prefer EGL over OSMesa when `/dev/dri/renderD*` is accessible:
```bash
docker run --rm --init \
--device /dev/dri/renderD128 \
-v /path/to/subject:/subject \
-v /path/to/output:/output \
whippersnappy \
-lh /subject/surf/lh.thickness -rh /subject/surf/rh.thickness \
-sd /subject -o /output/snap4.png
- **Docker CPU rendering** (default — no GPU needed): EGL uses Mesa's llvmpipe
software renderer. The log will show:
```
EGL context active — CPU software rendering (llvmpipe (...), ...)
```
- **Docker GPU rendering** (`--gpus all`, NVIDIA only): EGL uses the NVIDIA GPU
driver injected by the NVIDIA Container Toolkit. The log will show:
```
EGL context active — GPU rendering (...)
```
- **Singularity GPU rendering** with `--nv` uses EGL with the NVIDIA GPU
driver injected by Singularity. The log will show:
```
EGL context active — GPU rendering (...)
```




27 changes: 18 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
FROM python:3.11-slim

# libosmesa6 — OSMesa CPU software renderer (default headless path, no GPU needed)
# libegl1 — EGL dispatch library; enables GPU rendering when /dev/dri/renderD*
# is passed via --device (e.g. docker run --device /dev/dri/renderD128)
# libgl1 — base OpenGL dispatch library required by PyOpenGL
# libglib2.0-0, libfontconfig1, libdbus-1-3 — runtime deps for Pillow / font rendering
# Suppress Mesa's shader-cache warning ("Failed to create //.cache …") that
# appears when running as a non-standard user inside Docker where $HOME is
# unset or points to a non-writable directory.
ENV MESA_SHADER_CACHE_DISABLE=1

# In order to find NVIDIA GPUs (--gpus all)
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=all

# libegl1 — GLVND EGL dispatch library (routes to GPU or Mesa llvmpipe)
# libgl1 — base OpenGL dispatch library required by PyOpenGL
# libfontconfig1 — runtime deps for Pillow / font rendering
RUN apt-get update && apt-get install -y --no-install-recommends \
libosmesa6 \
libegl1 \
libgl1 \
libglib2.0-0 \
libfontconfig1 \
libdbus-1-3 && \
libfontconfig1 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Register the NVIDIA EGL ICD so libEGL finds the GPU driver
RUN mkdir -p /usr/share/glvnd/egl_vendor.d && \
echo '{"file_format_version":"1.0.0","ICD":{"library_path":"libEGL_nvidia.so.0"}}' \
> /usr/share/glvnd/egl_vendor.d/10_nvidia.json

RUN pip install --upgrade pip

COPY . /WhipperSnapPy
Expand Down
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,23 @@ For interactive 3D in Jupyter notebooks:
pip install 'whippersnappy[notebook]'
```

Off-screen (headless) rendering on **Linux** uses a three-path fallback:
1. **GLFW invisible window** — used when a display is available (`DISPLAY` set).
2. **EGL** (GPU, no display needed) — used when no display is detected and a
GPU render device (`/dev/dri/renderD*`) is accessible with `libEGL` installed
(`libegl1` on Debian/Ubuntu). This is the recommended path for SSH servers
with a GPU — no `DISPLAY`, `xvfb`, or OSMesa required.
3. **OSMesa** (CPU software renderer) — final fallback; requires
`sudo apt-get install libosmesa6` (Debian/Ubuntu) or
`sudo dnf install mesa-libOSMesa` (RHEL/Fedora).
Off-screen (headless) rendering on **Linux** uses **EGL** with Mesa's llvmpipe
CPU software renderer — no GPU or display server required. The log reports:
```
EGL context active — CPU software rendering (llvmpipe (...), ...)
```
When a GPU is accessible (native install, Docker with `--gpus all`, or
Singularity with `--nv`), EGL selects it automatically:
```
EGL context active — GPU rendering (...)
```
OSMesa (`libosmesa6`) is a last-resort CPU fallback used only when EGL
itself cannot initialise (e.g. `libegl1` not installed).

On **Windows**, GLFW creates an invisible window; a GPU driver is sufficient.
On **macOS**, a real display connection is required (NSGL does not support
headless rendering).
See the <a href="DOCKER.md">Docker guide</a> for headless Linux usage.
See the <a href="DOCKER.md">Docker/Singularity guide</a> for container usage.

## Command-Line Usage

Expand Down Expand Up @@ -217,8 +220,8 @@ See `tutorials/whippersnappy_tutorial.ipynb` for complete notebook examples.

## Docker

The Docker image provides a fully headless rendering environment using
OSMesa (CPU software renderer) — no display server, `xvfb`, or GPU required.
The Docker image provides a fully headless rendering environment using EGL —
CPU software rendering by default, GPU rendering with `--gpus all` (NVIDIA).
See <a href="DOCKER.md"><strong>DOCKER.md</strong></a> for details.

## API Documentation
Expand Down
45 changes: 24 additions & 21 deletions whippersnappy/cli/whippersnap.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,22 +720,21 @@ def run():
# ------------------------------------------------------------------
if QApplication is None:
print(
"ERROR: Interactive mode requires PyQt6. "
"Error: Interactive mode requires PyQt6. "
"Install with: pip install 'whippersnappy[gui]'",
file=sys.stderr,
)
raise RuntimeError(
"Interactive mode requires PyQt6. "
"Install with: pip install 'whippersnappy[gui]'"
)
sys.exit(1)

try:
from ..gui import ConfigWindow # noqa: PLC0415
except ModuleNotFoundError as e:
raise RuntimeError(
"Interactive mode requires PyQt6. "
"Install with: pip install 'whippersnappy[gui]'"
) from e
print(
f"Error: Interactive mode requires PyQt6 ({e}). "
"Install with: pip install 'whippersnappy[gui]'",
file=sys.stderr,
)
sys.exit(1)

current_fthresh_ = args.fthresh
current_fmax_ = args.fmax
Expand All @@ -756,18 +755,22 @@ def run():

# show_window creates the GLFW window, sets up a QTimer render loop,
# then calls app.exec() — returns when either window is closed.
show_window(
mesh=mesh_path,
overlay=overlay,
annot=args.annot,
bg_map=bg_map,
roi=roi,
invert=args.invert,
specular=args.specular,
view=view,
app=app,
config_window=config_window,
)
try:
show_window(
mesh=mesh_path,
overlay=overlay,
annot=args.annot,
bg_map=bg_map,
roi=roi,
invert=args.invert,
specular=args.specular,
view=view,
app=app,
config_window=config_window,
)
except (RuntimeError, FileNotFoundError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
Expand Down
7 changes: 5 additions & 2 deletions whippersnappy/cli/whippersnap1.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
import argparse
import logging
import os
import sys
import tempfile

import numpy as np

if __name__ == "__main__" and __package__ is None:
import sys
os.execv(sys.executable, [sys.executable, "-m", "whippersnappy.cli.whippersnap1"] + sys.argv[1:])

from .. import snap1, snap_rotate
Expand Down Expand Up @@ -309,8 +309,11 @@ def run():
ambient=args.ambient,
)
log.info("Snapshot saved to %s (%dx%d)", outpath, img.width, img.height)
except (RuntimeError, FileNotFoundError, ValueError, ImportError) as e:
except ValueError as e:
parser.error(str(e))
except (RuntimeError, FileNotFoundError, ImportError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
Expand Down
7 changes: 5 additions & 2 deletions whippersnappy/cli/whippersnap4.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
import argparse
import logging
import os
import sys
import tempfile

import numpy as np

if __name__ == "__main__" and __package__ is None:
import sys
os.execv(sys.executable, [sys.executable, "-m", "whippersnappy.cli.whippersnap4"] + sys.argv[1:])

from .. import snap4
Expand Down Expand Up @@ -210,8 +210,11 @@ def run():
logger.info(
"Snapshot saved to %s (%dx%d)", args.output_path, img.width, img.height
)
except (RuntimeError, FileNotFoundError, ValueError) as e:
except ValueError as e:
parser.error(str(e))
except (RuntimeError, FileNotFoundError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
Expand Down
Loading