Live Display
THOR can stream projection frames to an interactive viewer while the simulation runs. The native window (SDL2 + ImGui) runs on the machine running THOR and has an optional direct-GPU transport on NVIDIA systems, where the frame never touches host memory. A WebSocket server can stream the same frames to a remote client — though THOR ships no client for it today, see below.
Building
Both display backends are off by default. Enable them through build.sh:
| Flag | Effect |
|---|---|
--display |
Native ImGui window (THOR_ENABLE_LIVE_DISPLAY=ON) |
--live-server |
WebSocket server for remote clients (THOR_ENABLE_LIVE_SERVER=ON) |
--cuda-gl-interop |
Direct CUDA↔GL frame transport, NVIDIA only (implies --display) |
--gui-tests |
ImGui Test Engine GUI tests (implies --display) |
build.sh builds the thor target only, so the GUI test binary needs an
explicit target: ./build.sh --gui-tests --target thor_gui_tests.
./build.sh --display --live-server # typical interactive build
./build.sh --display --cuda-gl-interop # + direct-GPU transport
The interop option needs the CUDA toolkit (runtime API only); CMake resolves it
via find_package(CUDAToolkit) and prints the toolkit path at configure time —
pin an explicit installation by setting the environment variable
CUDAToolkit_ROOT=<path> ./build.sh --cuda-gl-interop if several are present
(build.sh rejects unknown arguments, so -D... cannot be passed through).
Configuration
Both backends are configured per driver. The blocks below are shown for
raytracer; sphprojector accepts the same ones but only displays its
projections — it does not consume camera, field, or resolution changes, so the
controls have no effect there.
raytracer:
live_display: # native window
enabled: true
interactive: true # camera/field/resolution driven from the UI
window_width: 1600
window_height: 900
window_title: "THOR Live Display"
update_interval_ms: 100 # minimum interval between rendered frames
rotation: 0 # displayed image rotation: 0 / 90 / 180 / 270
backend: voronoi_raytrace # voronoi_raytrace | volume_render | sph_preview
direct_gpu: auto # auto | off — CUDA-GL transport policy (see below)
# global colormap / scale defaults
colormap: viridis # viridis | inferno | plasma | magma | grayscale
log_scale: true
vmin: 1.0e12 # setting vmin/vmax enables the manual range
vmax: 1.0e22
# per-field settings — a complete replacement, not a merge
field_settings:
temperature:
colormap: inferno
log_scale: true
vmin: 1.0e2
vmax: 1.0e8
websocket_display: # WebSocket server
enabled: true
port: 9001
host: 127.0.0.1 # bind address; loopback by default
max_clients: 10
The server is unauthenticated
A connected client can drive the camera, the displayed field and the render
resolution. There is no authentication, so host defaults to 127.0.0.1
and reaching it from another machine should go through an SSH tunnel
(ssh -L 9001:localhost:9001 <host>). Setting host: 0.0.0.0 exposes that
control to everyone who can route to the port, and THOR logs a warning when
you do.
A top-level headless: true (also set by the --headless CLI flag) skips
native display initialization entirely.
Field settings precedence
A field resolves to the first of: its field_settings entry, THOR's
built-in default for a recognized field name (density, temperature,
velocity, metallicity and similar), then the global block. Two consequences:
an entry replaces the globals wholesale rather than merging with them, so
list every value you want; and the globals never reach a recognized field —
override it by name to change it.
Interactive mode is implicit for the WebSocket server; for the native window it
is opt-in via interactive: true, and without it the window passively displays
whatever the configured projection produces.
When both are interactive at once, the first one drives: the native window is registered before the server, so it owns the camera, field, backend, and resolution, and a connected client's controls are ignored while it runs. Disable the native display to hand control to a remote client.
Render resolution
The rendered resolution comes from the projection operator's npixels,
multiplied by the UI's resolution setting (0.25×–2×). During camera motion
the resolution is temporarily halved for responsiveness and restored once
the camera settles.
The viewer
The native window shows the projection with a colorbar (editable vmin/vmax,
log toggle, auto range), panels for camera control, render settings (colormap,
interpolation: nearest / bilinear / bicubic, resolution), field selection, and
a Tools panel:
- Tracer — click the image to probe the line of sight; per-cell profiles are plotted and exportable to HDF5.
- Histogram (2D pixels) — drag a rectangle to histogram displayed pixel values.
- Histogram (3D cells) / Tracer grid (area) — cast ray bundles through the selected region on the simulation side and aggregate per-cell values.
The WebSocket server streams frames to a client at ws://<host>:<port>,
computing the frame range so the client only has to apply the colormap.
No client currently ships with THOR: the Emscripten browser client was
retired once its behaviour moved into core/, and its native replacement is
not built yet. The server, the wire protocol (src/display/core/Protocol.h)
and the portable renderer all remain, so remote viewing is unavailable rather
than removed.
Rendering pipeline and the direct-GPU transport
Frames are staged on the device: one kernel normalizes and narrows the
projection to float32, a reduction computes the display range, and the
colormap is applied in the fragment shader from a lookup-table texture —
settings changes (colormap, log scale, manual range) are uniform updates and
re-render instantly without touching pixel data. This covers scalar projection
and SPH frames. backend: volume_render publishes already-coloured RGBA
instead, which the shader passes straight through: colormap, log scale, and
range controls do not affect a volume frame.
On NVIDIA systems the optional direct-GPU transport
(--cuda-gl-interop builds) goes one step further: the staged frame is
device-copied into a CUDA-registered OpenGL pixel buffer, so a displayed frame
never crosses to host memory. That applies to the native scalar-frame path
only: volume_render builds its RGBA frame on the host, and running a
WebSocket display alongside the native one forces the staged frame to be copied
back for the server. It activates automatically when all of the
following hold, and falls back to the portable path otherwise — one log line
states the chosen transport and, on fallback, the reason:
- built with
THOR_ENABLE_CUDA_GL_INTEROP, live_display.direct_gpuisauto(default;off,false,no,0disable it),- single-rank run,
- the primary SYCL device is a CUDA GPU, and it is the same physical device backing the GL context (multi-GPU systems fall back cleanly).
The environment variable THOR_DIRECT_GPU=0|1 overrides the YAML policy per
run — useful for A/B benchmarking. Any CUDA/GL error at runtime triggers a
permanent fallback to the portable path for the rest of the session; the
in-flight frame is preserved.
As a reference point (RTX 3090, ~2 MP frames): the pre-fast-path display cost ~27 ms of simulation-thread time per frame; the portable staging path costs ≈0.6–1.2 ms, and the direct-GPU transport ≈0.2 ms with the remaining GL work on the display thread.
Troubleshooting
- No window appears: the build needs
--display, the YAML needslive_display.enabled: true, and a display server must be reachable (check the log forLiveDisplay: SDL_Init failed). Use the WebSocket display for remote/headless machines. - Which transport am I on? In a
--cuda-gl-interopbuild, look forLive display transport: direct GPU (CUDA device N, R32F PBO)orLive display transport: portable (<reason>)in the log. A build without that flag logs neither line — it is always on the portable path. - GUI tests / headless machines:
thor_gui_tests -noguiruns underSDL_VIDEODRIVER=offscreen(or Xvfb) without a display server.