Skip to content

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_gpu is auto (default; off, false, no, 0 disable 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 needs live_display.enabled: true, and a display server must be reachable (check the log for LiveDisplay: SDL_Init failed). Use the WebSocket display for remote/headless machines.
  • Which transport am I on? In a --cuda-gl-interop build, look for Live display transport: direct GPU (CUDA device N, R32F PBO) or Live 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 -nogui runs under SDL_VIDEODRIVER=offscreen (or Xvfb) without a display server.