#!/usr/bin/env bash # Run a command inside a disposable headless desktop for capture tests. # # scripts/rig/headless.sh [--x11] [args...] # # Starts a private D-Bus session with PipeWire, WirePlumber, a virtual audio # sink, headless Sway on the GPU and xdg-desktop-portal-wlr (the portal # auto-selects the only output). With --x11, the game and the command run as # X11 clients of Sway's Xwayland instead, which has GPU rendering (DRI3) and an # EWMH window manager like a real X11 desktop. A 60 FPS test pattern with a # 440 Hz tone stands in for a game. Everything is torn down when the command # exits. # # The command sees CLIPBIN_RIG_DIR (logs, plus game.pid and xdpw.pid for the # test game and the portal backend) and, with --x11, CLIPBIN_RIG_GAME_WINDOW # (the test window's X id). Set CLIPBIN_RIG_KEEP_LOGS= to keep the logs. # # This exercises the capture paths on a machine without a display. It does not # replace validation on real hardware (Hyprland, an X11 desktop, Windows). set -euo pipefail mode=wayland if [[ "${1:-}" == "--x11" ]]; then mode=x11 shift fi if [[ $# -eq 0 ]]; then echo "usage: $0 [--x11] [args...]" >&2 exit 2 fi if [[ -z "${CLIPBIN_RIG_INNER:-}" ]]; then args=("$@") [[ "${mode}" == x11 ]] && args=(--x11 "${args[@]}") exec env CLIPBIN_RIG_INNER=1 dbus-run-session -- "$0" "${args[@]}" fi rig_dir="$(mktemp -d "${TMPDIR:-/tmp}/clipbin-rig.XXXXXX")" pids=() cleanup() { for pid in "${pids[@]}"; do kill "${pid}" 2>/dev/null || true; done wait 2>/dev/null || true # The D-Bus-activated document portal mounts a FUSE filesystem here. fusermount3 -u "${XDG_RUNTIME_DIR}/doc" 2>/dev/null || true if [[ -n "${CLIPBIN_RIG_KEEP_LOGS:-}" ]]; then mkdir -p "${CLIPBIN_RIG_KEEP_LOGS}" cp "${rig_dir}"/*.log "${CLIPBIN_RIG_KEEP_LOGS}/" 2>/dev/null || true fi rm -rf "${rig_dir}" } trap cleanup EXIT export XDG_RUNTIME_DIR="${rig_dir}/runtime" export XDG_CONFIG_HOME="${rig_dir}/config" export XDG_CURRENT_DESKTOP=sway XDG_SESSION_TYPE=wayland mkdir -p "${XDG_RUNTIME_DIR}" "${XDG_CONFIG_HOME}/sway" "${XDG_CONFIG_HOME}/xdg-desktop-portal-wlr" chmod 700 "${XDG_RUNTIME_DIR}" unset WAYLAND_DISPLAY DISPLAY width="${CLIPBIN_RIG_WIDTH:-1920}" height="${CLIPBIN_RIG_HEIGHT:-1080}" wait_for() { local what="$1" check="$2" for _ in $(seq 100); do if eval "${check}"; then return 0; fi sleep 0.1 done echo "rig: ${what} did not start; logs in ${rig_dir}" >&2 exit 1 } pipewire > "${rig_dir}/pipewire.log" 2>&1 & pids+=($!) wait_for pipewire '[[ -S "${XDG_RUNTIME_DIR}/pipewire-0" ]]' wireplumber > "${rig_dir}/wireplumber.log" 2>&1 & pids+=($!) pipewire-pulse > "${rig_dir}/pipewire-pulse.log" 2>&1 & pids+=($!) echo "${pids[-1]}" > "${rig_dir}/pulse.pid" wait_for pipewire-pulse '[[ -S "${XDG_RUNTIME_DIR}/pulse/native" ]]' # No sound card here: route playback to a virtual sink whose monitor OBS # records as desktop audio. pactl load-module module-null-sink sink_name=clipbin-rig-sink > /dev/null pactl set-default-sink clipbin-rig-sink printf '%s\n' \ "output HEADLESS-1 resolution ${width}x${height}@60Hz" \ "default_border none" \ "xwayland force" \ "exec echo \"\$DISPLAY\" > ${rig_dir}/xwayland-display" \ > "${XDG_CONFIG_HOME}/sway/config" printf '%s\n' \ "[screencast]" \ "chooser_type=none" \ "output_name=${CLIPBIN_RIG_PORTAL_OUTPUT:-HEADLESS-1}" \ "max_fps=60" \ > "${XDG_CONFIG_HOME}/xdg-desktop-portal-wlr/config" export SWAYSOCK="${XDG_RUNTIME_DIR}/clipbin-sway.sock" WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_HEADLESS_OUTPUTS=1 \ sway --config "${XDG_CONFIG_HOME}/sway/config" > "${rig_dir}/sway.log" 2>&1 & pids+=($!) wait_for sway 'compgen -G "${XDG_RUNTIME_DIR}/wayland-*" > /dev/null' WAYLAND_DISPLAY="$(basename "$(compgen -G "${XDG_RUNTIME_DIR}/wayland-*" | grep -v lock | head -1)")" wait_for "sway IPC" '[[ -S "${SWAYSOCK}" ]]' export WAYLAND_DISPLAY SWAYSOCK dbus-update-activation-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_RUNTIME_DIR XDG_CONFIG_HOME SWAYSOCK /usr/lib/xdg-desktop-portal-wlr > "${rig_dir}/xdpw.log" 2>&1 & pids+=($!) echo "${pids[-1]}" > "${rig_dir}/xdpw.pid" /usr/lib/xdg-desktop-portal -r > "${rig_dir}/xdp.log" 2>&1 & pids+=($!) video_driver=wayland if [[ "${mode}" == x11 ]]; then wait_for Xwayland '[[ -s "${rig_dir}/xwayland-display" ]]' DISPLAY="$(cat "${rig_dir}/xwayland-display")" export DISPLAY XDG_SESSION_TYPE=x11 wait_for Xwayland 'xwininfo -root > /dev/null 2>&1' unset WAYLAND_DISPLAY video_driver=x11 fi # A moving 60 FPS picture, optionally without any audio stream for idle tests. pattern="testsrc2=size=${width}x${height}:rate=60,format=yuv420p[out0]" if [[ "${CLIPBIN_RIG_NO_AUDIO:-0}" != 1 ]]; then pattern+=';sine=frequency=440:sample_rate=48000[out1]' fi SDL_VIDEODRIVER="${video_driver}" ffplay -loglevel error -fs -window_title clipbin-test-game \ -f lavfi "${pattern}" \ > "${rig_dir}/game.log" 2>&1 & pids+=($!) echo "${pids[-1]}" > "${rig_dir}/game.pid" if [[ "${mode}" == x11 ]]; then wait_for "the test window" 'xwininfo -name clipbin-test-game > /dev/null 2>&1' CLIPBIN_RIG_GAME_WINDOW="$(xwininfo -name clipbin-test-game | awk '/Window id:/ { print strtonum($4) }')" export CLIPBIN_RIG_GAME_WINDOW fi sleep 1 export CLIPBIN_RIG_DIR="${rig_dir}" echo "rig: ready (${mode}, logs in ${rig_dir})" >&2 status=0 "$@" || status=$? exit "${status}"