headless_render.sh 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. #!/usr/bin/env bash
  2. # Renders a wallpaper headlessly (no real display/GPU needed - Xvfb + Mesa llvmpipe software
  3. # rendering) and takes a screenshot, for environments with no way to see the real screen
  4. # (sandboxes, CI, this project's own dev container). Confirmed working there; a real machine
  5. # with a GPU and a display doesn't need this, just run the binary normally.
  6. #
  7. # Usage:
  8. # tools/headless_render.sh <path-to-linux-wallpaperengine-binary> <output.png> [engine args...]
  9. #
  10. # Example:
  11. # tools/headless_render.sh build/output/linux-wallpaperengine /tmp/out.png \
  12. # --assets-dir /path/to/wallpaper_engine/assets --window 0x0x1920x1080 --fps 25 \
  13. # /path/to/workshop/item/dir
  14. #
  15. # Requirements: Xvfb, a software GL renderer (mesa llvmpipe is normally already present
  16. # alongside libgl1-mesa-dri), gcc (to build the D-Bus shim once, cached after that).
  17. #
  18. # Known limitations:
  19. # - No real D-Bus session bus is assumed - dbus_noop_shim.so (built automatically) patches the
  20. # one call path that isn't already null-safe against a missing bus.
  21. # - The engine is stopped as soon as the screenshot file appears (HEADLESS_RENDER_KEEP_RUNNING=1 disables
  22. # that and runs until HEADLESS_RENDER_TIMEOUT, default 60s).
  23. # - --screenshot-delay is capped at 5000 frames by the engine (ApplicationContext.cpp).
  24. # - CImage.cpp's puppet/effect diagnostics are one-shot logs that fire on the first draw call,
  25. # not a chosen frame.
  26. set -euo pipefail
  27. if [ "$#" -lt 2 ]; then
  28. echo "Usage: $0 <binary> <output.png> [engine args...]" >&2
  29. exit 1
  30. fi
  31. BINARY="$1"; shift
  32. OUTPUT="$1"; shift
  33. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  34. BINARY_DIR="$(cd "$(dirname "$BINARY")" && pwd)"
  35. SHIM_SRC="$SCRIPT_DIR/dbus_noop_shim.c"
  36. SHIM_SO="/tmp/dbus_noop_shim.so"
  37. if [ ! -f "$SHIM_SO" ] || [ "$SHIM_SRC" -nt "$SHIM_SO" ]; then
  38. gcc -shared -fPIC -o "$SHIM_SO" "$SHIM_SRC" -ldl
  39. fi
  40. XVFB_DISPLAY="${HEADLESS_RENDER_DISPLAY:-:99}"
  41. XVFB_SOCKET="/tmp/.X11-unix/X${XVFB_DISPLAY#:}"
  42. if [ ! -S "$XVFB_SOCKET" ]; then
  43. Xvfb "$XVFB_DISPLAY" -screen 0 1920x1080x24 &
  44. XVFB_PID=$!
  45. trap 'kill "$XVFB_PID" 2>/dev/null || true' EXIT
  46. # give it a moment to bind before launching anything against it
  47. for _ in $(seq 1 20); do
  48. [ -S "$XVFB_SOCKET" ] && break
  49. sleep 0.2
  50. done
  51. fi
  52. rm -f "$OUTPUT"
  53. DISPLAY="$XVFB_DISPLAY" XDG_SESSION_TYPE=x11 \
  54. LD_LIBRARY_PATH="$BINARY_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
  55. LD_PRELOAD="$SHIM_SO" \
  56. timeout "${HEADLESS_RENDER_TIMEOUT:-60}" "$BINARY" \
  57. --window 0x0x1920x1080 \
  58. --screenshot "$OUTPUT" \
  59. --screenshot-delay "${HEADLESS_RENDER_DELAY:-3}" \
  60. "$@" &
  61. ENGINE_PID=$!
  62. # the engine keeps rendering after the screenshot is taken, so without this the run always lasts
  63. # the full timeout - stop it as soon as the file is written (set HEADLESS_RENDER_KEEP_RUNNING=1 to
  64. # let it run until the timeout instead)
  65. if [ -z "${HEADLESS_RENDER_KEEP_RUNNING:-}" ]; then
  66. while kill -0 "$ENGINE_PID" 2>/dev/null; do
  67. if [ -s "$OUTPUT" ]; then
  68. # wait for the file to stop growing so a half-written PNG isn't cut off
  69. prev=-1
  70. cur=$(stat -c %s "$OUTPUT")
  71. while [ "$cur" != "$prev" ]; do
  72. sleep 0.3
  73. prev=$cur
  74. cur=$(stat -c %s "$OUTPUT")
  75. done
  76. kill "$ENGINE_PID" 2>/dev/null || true
  77. break
  78. fi
  79. sleep 0.2
  80. done
  81. fi
  82. wait "$ENGINE_PID" || true
  83. if [ -f "$OUTPUT" ]; then
  84. echo "Wrote $OUTPUT"
  85. else
  86. echo "No screenshot was produced - check the engine's stderr output above" >&2
  87. exit 1
  88. fi