diff --git a/README.md b/README.md index 869b4fa..c367124 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,28 @@ nix develop --command bash -c "cargo build" 2. Clone the repository. 3. Navigate to the project directory. 4. Run the game using the command: `nix develop --command bash -c "cargo run"` + +## Headless Screenshots + +The flake exposes a `take-screenshots` app that launches a binary inside an +Xvfb display backed by lavapipe (software Vulkan), waits, and captures one or +more PNG screenshots. Useful for smoke-testing rendering without a real GPU. + +``` +nix run .#take-screenshots -- EXE NUM DELAY_START PAUSE_INBETWEEN [OUTPUT_DIR] +``` + +* `EXE` — path to the executable to launch +* `NUM` — number of screenshots to take +* `DELAY_START` — seconds to wait after launch before the first shot +* `PAUSE_INBETWEEN` — seconds between consecutive shots +* `OUTPUT_DIR` — where to write `shot-NNN.png` files (default: current directory) + +Example, capturing three frames of the game one second apart after a six-second +warm-up (Bevy + software Vulkan needs roughly that long to render its first +frame): + +``` +cargo build +nix run .#take-screenshots -- ./target/debug/bglga 3 6 1 ./shots +``` diff --git a/flake.nix b/flake.nix index ce0d3a3..4106933 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,98 @@ vulkan-loader glfw ]; + + takeScreenshots = pkgs.writeShellApplication { + name = "take-screenshots"; + runtimeInputs = with pkgs; [ + xorg-server + imagemagick + coreutils + ]; + text = '' + set -euo pipefail + + if [ "$#" -lt 4 ]; then + cat >&2 <<'USAGE' + Usage: take-screenshots EXE NUM DELAY_START PAUSE_INBETWEEN [OUTPUT_DIR] + + EXE path to executable to launch + NUM number of screenshots to take + DELAY_START seconds to wait after launching EXE before first shot + PAUSE_INBETWEEN seconds between consecutive shots + OUTPUT_DIR output directory (default: current directory) + USAGE + exit 1 + fi + + EXE=$1 + NUM=$2 + DELAY_START=$3 + PAUSE=$4 + OUTDIR=''${5:-.} + + mkdir -p "$OUTDIR" + + # Locate lavapipe (software Vulkan) ICD; needed because Xvfb has no GPU. + LVP_ICD= + for c in \ + "${pkgs.mesa}/share/vulkan/icd.d/lvp_icd.x86_64.json" \ + /run/opengl-driver/share/vulkan/icd.d/lvp_icd.x86_64.json + do + if [ -f "$c" ]; then LVP_ICD=$c; break; fi + done + if [ -z "$LVP_ICD" ]; then + echo "take-screenshots: could not locate lavapipe Vulkan ICD" >&2 + exit 1 + fi + + # Pick a free X display. + DISPLAY_NUM=99 + while [ -e "/tmp/.X$DISPLAY_NUM-lock" ] || [ -e "/tmp/.X11-unix/X$DISPLAY_NUM" ]; do + DISPLAY_NUM=$((DISPLAY_NUM + 1)) + done + + Xvfb ":$DISPLAY_NUM" -screen 0 800x900x24 \ + +extension GLX +extension RANDR +render -ac & + XVFB_PID=$! + + GAME_PID= + cleanup() { + if [ -n "$GAME_PID" ]; then + kill "$GAME_PID" 2>/dev/null || true + wait "$GAME_PID" 2>/dev/null || true + fi + kill "$XVFB_PID" 2>/dev/null || true + wait "$XVFB_PID" 2>/dev/null || true + rm -f "/tmp/.X$DISPLAY_NUM-lock" + } + trap cleanup EXIT + + sleep 0.5 + + export DISPLAY=":$DISPLAY_NUM" + export VK_ICD_FILENAMES=$LVP_ICD + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + + "$EXE" & + GAME_PID=$! + + sleep "$DELAY_START" + + for i in $(seq 1 "$NUM"); do + if ! kill -0 "$GAME_PID" 2>/dev/null; then + echo "take-screenshots: process exited before screenshot $i" >&2 + exit 1 + fi + out=$(printf "%s/shot-%03d.png" "$OUTDIR" "$i") + import -window root "$out" + echo "$out" + if [ "$i" -lt "$NUM" ]; then + sleep "$PAUSE" + fi + done + ''; + }; in { devShells.default = pkgs.mkShell { @@ -49,6 +141,11 @@ LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath runtimeLibs; }; + + apps.take-screenshots = { + type = "app"; + program = "${takeScreenshots}/bin/take-screenshots"; + }; } ); }