headless_render.sh 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env bash
  2. # Renders a wallpaper headlessly and takes a screenshot, for environments with no way to see the
  3. # real screen (sandboxes, CI, this project's own dev container). When a GPU render node
  4. # (/dev/dri/renderD*) is accessible it renders there through the engine's headless EGL driver
  5. # (XDG_SESSION_TYPE=headless, no X server, a 4K scene takes ~1s instead of ~20s). Otherwise, or
  6. # with HEADLESS_RENDER_SOFTWARE=1, it falls back to Xvfb + Mesa llvmpipe software rendering.
  7. # LWE_HEADLESS_DEVICE=/dev/dri/renderDN picks the GPU when there are several.
  8. #
  9. # Usage:
  10. # tools/headless_render.sh <path-to-linux-wallpaperengine-binary> <output.png> [engine args...]
  11. #
  12. # Example:
  13. # tools/headless_render.sh build/output/linux-wallpaperengine /tmp/out.png \
  14. # --assets-dir /path/to/wallpaper_engine/assets --window 0x0x1920x1080 --fps 25 \
  15. # /path/to/workshop/item/dir
  16. #
  17. # Requirements: a GPU render node, or Xvfb and a software GL renderer (mesa llvmpipe is normally
  18. # already present alongside libgl1-mesa-dri); gcc (to build the D-Bus shim once, cached after that).
  19. #
  20. # Known limitations:
  21. # - No real D-Bus session bus is assumed - dbus_noop_shim.so (built automatically) patches the
  22. # one call path that isn't already null-safe against a missing bus.
  23. # - The engine is stopped as soon as the screenshot file appears (HEADLESS_RENDER_KEEP_RUNNING=1 disables
  24. # that and runs until HEADLESS_RENDER_TIMEOUT, default 60s).
  25. # - HEADLESS_RENDER_SIZE=WxH changes the window size (default 1920x1080).
  26. # - LWE_HEADLESS_CURSOR=x,y puts the GPU path's pointer at that fraction of the output (default 0.5,0.5).
  27. # - --screenshot-delay is capped at 5000 frames by the engine (ApplicationContext.cpp).
  28. # - CImage.cpp's puppet/effect diagnostics are one-shot logs that fire on the first draw call,
  29. # not a chosen frame.
  30. set -euo pipefail
  31. if [ "$#" -lt 2 ]; then
  32. echo "Usage: $0 <binary> <output.png> [engine args...]" >&2
  33. exit 1
  34. fi
  35. BINARY="$1"; shift
  36. OUTPUT="$1"; shift
  37. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  38. BINARY_DIR="$(cd "$(dirname "$BINARY")" && pwd)"
  39. SHIM_SRC="$SCRIPT_DIR/dbus_noop_shim.c"
  40. SHIM_SO="/tmp/dbus_noop_shim.so"
  41. if [ ! -f "$SHIM_SO" ] || [ "$SHIM_SRC" -nt "$SHIM_SO" ]; then
  42. gcc -shared -fPIC -o "$SHIM_SO" "$SHIM_SRC" -ldl
  43. fi
  44. USE_GPU=
  45. if [ -z "${HEADLESS_RENDER_SOFTWARE:-}" ]; then
  46. for node in /dev/dri/renderD*; do
  47. if [ -r "$node" ] && [ -w "$node" ]; then
  48. USE_GPU=1
  49. break
  50. fi
  51. done
  52. fi
  53. XVFB_DISPLAY="${HEADLESS_RENDER_DISPLAY:-:99}"
  54. XVFB_SOCKET="/tmp/.X11-unix/X${XVFB_DISPLAY#:}"
  55. # a private runtime dir and no desktop variables: libwayland falls back to $XDG_RUNTIME_DIR/wayland-0 when
  56. # WAYLAND_DISPLAY is unset, so a build without the headless driver would otherwise open a window on the real
  57. # desktop session (and reach its D-Bus / audio server)
  58. RUNTIME_DIR="$(mktemp -d /tmp/lwe-headless-run.XXXXXX)"
  59. chmod 700 "$RUNTIME_DIR"
  60. DBUS_PID=
  61. trap 'kill $DBUS_PID 2>/dev/null; rm -rf "$RUNTIME_DIR"' EXIT
  62. # the engine needs a session bus (MPRIS media source), it gets a private one
  63. BUS_ADDRESS=unix:path=/nonexistent-bus
  64. if command -v dbus-daemon >/dev/null; then
  65. DBUS_PID=$(dbus-daemon --session --fork --print-pid --address="unix:path=$RUNTIME_DIR/bus")
  66. BUS_ADDRESS="unix:path=$RUNTIME_DIR/bus"
  67. fi
  68. ISOLATE=(-u WAYLAND_DISPLAY -u WAYLAND_SOCKET -u KDE_FULL_SESSION -u KDE_SESSION_VERSION
  69. -u DESKTOP_SESSION -u XDG_CURRENT_DESKTOP -u XDG_SESSION_DESKTOP XDG_RUNTIME_DIR="$RUNTIME_DIR"
  70. DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS"
  71. PULSE_SERVER=unix:/nonexistent-pulse PIPEWIRE_REMOTE=/nonexistent-pipewire)
  72. if [ -n "$USE_GPU" ]; then
  73. SESSION=(env -u DISPLAY "${ISOLATE[@]}" XDG_SESSION_TYPE=headless)
  74. else
  75. SESSION=(env "${ISOLATE[@]}" DISPLAY="$XVFB_DISPLAY" XDG_SESSION_TYPE=x11)
  76. fi
  77. if [ -z "$USE_GPU" ] && [ ! -S "$XVFB_SOCKET" ]; then
  78. Xvfb "$XVFB_DISPLAY" -screen 0 1920x1080x24 &
  79. XVFB_PID=$!
  80. trap 'kill "$XVFB_PID" $DBUS_PID 2>/dev/null || true; rm -rf "$RUNTIME_DIR"' EXIT
  81. # give it a moment to bind before launching anything against it
  82. for _ in $(seq 1 20); do
  83. [ -S "$XVFB_SOCKET" ] && break
  84. sleep 0.2
  85. done
  86. fi
  87. rm -f "$OUTPUT"
  88. "${SESSION[@]}" \
  89. LD_LIBRARY_PATH="$BINARY_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
  90. LD_PRELOAD="$SHIM_SO" \
  91. timeout "${HEADLESS_RENDER_TIMEOUT:-60}" "$BINARY" \
  92. --window "0x0x${HEADLESS_RENDER_SIZE:-1920x1080}" \
  93. --screenshot "$OUTPUT" \
  94. --screenshot-delay "${HEADLESS_RENDER_DELAY:-3}" \
  95. "$@" &
  96. ENGINE_PID=$!
  97. # the engine keeps rendering after the screenshot is taken, so without this the run always lasts
  98. # the full timeout - stop it as soon as the file is written (set HEADLESS_RENDER_KEEP_RUNNING=1 to
  99. # let it run until the timeout instead)
  100. if [ -z "${HEADLESS_RENDER_KEEP_RUNNING:-}" ]; then
  101. while kill -0 "$ENGINE_PID" 2>/dev/null; do
  102. if [ -s "$OUTPUT" ]; then
  103. # wait for the file to stop growing so a half-written PNG isn't cut off
  104. prev=-1
  105. cur=$(stat -c %s "$OUTPUT")
  106. while [ "$cur" != "$prev" ]; do
  107. sleep 0.3
  108. prev=$cur
  109. cur=$(stat -c %s "$OUTPUT")
  110. done
  111. kill "$ENGINE_PID" 2>/dev/null || true
  112. break
  113. fi
  114. sleep 0.2
  115. done
  116. fi
  117. wait "$ENGINE_PID" || true
  118. if [ -f "$OUTPUT" ]; then
  119. echo "Wrote $OUTPUT"
  120. else
  121. echo "No screenshot was produced - check the engine's stderr output above" >&2
  122. exit 1
  123. fi