fix(demo): fail truncated recordings and document the AMD GPU requirement
4 files · +44 -17
M docs/dev/headless.md +4 -2
| @@ -7,7 +7,7 @@ neferwl --backend=headless --screenshot /tmp/neferwl-shots | |||
| 7 | 7 | WAYLAND_DISPLAY=<name from the log> foot | |
| 8 | 8 | ``` | |
| 9 | 9 | ||
| 10 | - | - `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. | |
| 10 | + | - `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. `latest.png` is a hard link to the newest frame, so replace it rather than editing it in place. | |
| 11 | 11 | - `--size 640x360,640x360` creates two outputs, arranged left to right, | |
| 12 | 12 | for testing `focus-monitor-left/right` and moving columns between outputs. | |
| 13 | 13 | - With several outputs, each writes to `<dir>/HEADLESS-<n>/`. | |
| @@ -25,12 +25,14 @@ ffmpeg -f concat -safe 0 -i /tmp/shots/frames.ffconcat -vf "fps=60,format=yuv420 | |||
| 25 | 25 | For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filters. | |
| 26 | 26 | ||
| 27 | 27 | ### Demo recording | |
| 28 | - | Run `tools/demo/record.sh` with Go 1.27, rootless Docker, ffmpeg and network access installed. | |
| 28 | + | Run `tools/demo/record.sh`. It requires Go 1.27, rootless Docker, ffmpeg, network access and an AMD GPU. | |
| 29 | 29 | It builds the Arch image (apps, tesseract, AT-SPI) and the compositor binary, then records the 1080p README demo. | |
| 30 | 30 | The compositor runs headless inside a disposable container, with the binary and a work directory bind-mounted and `/dev/dri` passed for the GPU. | |
| 31 | + | The image only carries Mesa's radeon Vulkan driver, and hint scans need the sync files that a hardware driver exports; the script stops early without an AMD render node. | |
| 31 | 32 | Firefox opens a live DuckDuckGo page, so the recording needs the network and its content varies. | |
| 32 | 33 | A FIFO feeds `input.txt` while the script launches the apps on the same clock. | |
| 33 | 34 | The hint step makes Firefox fullscreen and labels the page with `hint`; `hint-click <words>` then types the label of the link that shows those words, found from the scan's dump and an OCR read of the still page (`hint-read`). | |
| 35 | + | The recording only counts if the compositor stops on the quit key that ends `input.txt`; reaching its `--timeout` first fails the run as truncated. | |
| 34 | 36 | Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs. | |
| 35 | 37 | The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`. | |
| 36 | 38 | ||
| @@ -7,7 +7,7 @@ neferwl --backend=headless --screenshot /tmp/neferwl-shots | |||||
| 7 | WAYLAND_DISPLAY=<name from the log> foot | 7 | WAYLAND_DISPLAY=<name from the log> foot | ||
| 8 | ``` | 8 | ``` | ||
| 9 | 9 | ||||
| 10 | - | - `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. | 10 | + | - `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. `latest.png` is a hard link to the newest frame, so replace it rather than editing it in place. |
| 11 | - `--size 640x360,640x360` creates two outputs, arranged left to right, | 11 | - `--size 640x360,640x360` creates two outputs, arranged left to right, | ||
| 12 | for testing `focus-monitor-left/right` and moving columns between outputs. | 12 | for testing `focus-monitor-left/right` and moving columns between outputs. | ||
| 13 | - With several outputs, each writes to `<dir>/HEADLESS-<n>/`. | 13 | - With several outputs, each writes to `<dir>/HEADLESS-<n>/`. | ||
| @@ -25,12 +25,14 @@ ffmpeg -f concat -safe 0 -i /tmp/shots/frames.ffconcat -vf "fps=60,format=yuv420 | |||||
| 25 | For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filters. | 25 | For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filters. | ||
| 26 | 26 | ||||
| 27 | ### Demo recording | 27 | ### Demo recording | ||
| 28 | - | Run `tools/demo/record.sh` with Go 1.27, rootless Docker, ffmpeg and network access installed. | 28 | + | Run `tools/demo/record.sh`. It requires Go 1.27, rootless Docker, ffmpeg, network access and an AMD GPU. |
| 29 | It builds the Arch image (apps, tesseract, AT-SPI) and the compositor binary, then records the 1080p README demo. | 29 | It builds the Arch image (apps, tesseract, AT-SPI) and the compositor binary, then records the 1080p README demo. | ||
| 30 | The compositor runs headless inside a disposable container, with the binary and a work directory bind-mounted and `/dev/dri` passed for the GPU. | 30 | The compositor runs headless inside a disposable container, with the binary and a work directory bind-mounted and `/dev/dri` passed for the GPU. | ||
| 31 | + | The image only carries Mesa's radeon Vulkan driver, and hint scans need the sync files that a hardware driver exports; the script stops early without an AMD render node. | |||
| 31 | Firefox opens a live DuckDuckGo page, so the recording needs the network and its content varies. | 32 | Firefox opens a live DuckDuckGo page, so the recording needs the network and its content varies. | ||
| 32 | A FIFO feeds `input.txt` while the script launches the apps on the same clock. | 33 | A FIFO feeds `input.txt` while the script launches the apps on the same clock. | ||
| 33 | The hint step makes Firefox fullscreen and labels the page with `hint`; `hint-click <words>` then types the label of the link that shows those words, found from the scan's dump and an OCR read of the still page (`hint-read`). | 34 | The hint step makes Firefox fullscreen and labels the page with `hint`; `hint-click <words>` then types the label of the link that shows those words, found from the scan's dump and an OCR read of the still page (`hint-read`). | ||
| 35 | + | The recording only counts if the compositor stops on the quit key that ends `input.txt`; reaching its `--timeout` first fails the run as truncated. | |||
| 34 | Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs. | 36 | Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs. | ||
| 35 | The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`. | 37 | The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`. | ||
| 36 | 38 | ||||
M tools/demo/Dockerfile +2 -1
| @@ -16,7 +16,7 @@ COPY foot.ini /root/.config/foot/foot.ini | |||
| 16 | 16 | COPY btop.conf /root/.config/btop/btop.conf | |
| 17 | 17 | COPY btop-vesper.theme /root/.config/btop/themes/vesper.theme | |
| 18 | 18 | COPY demo-entry demo-hint-label /usr/local/bin/ | |
| 19 | - | COPY config /root/config | |
| 19 | + | # One config for both users: the compositor (root) reads it, Mousepad (demo) edits it. | |
| 20 | 20 | COPY config /home/demo/config | |
| 21 | 21 | COPY mousepad.keyfile /home/demo/.config/glib-2.0/settings/keyfile | |
| 22 | 22 | RUN chown -R demo:demo /home/demo | |
| @@ -24,6 +24,7 @@ ENV HOME=/root XDG_RUNTIME_DIR=/run/wl WAYLAND_DISPLAY=wayland-0 \ | |||
| 24 | 24 | GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \ | |
| 25 | 25 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/wl/bus \ | |
| 26 | 26 | MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor | |
| 27 | + | # LIBGL_ALWAYS_SOFTWARE applies to the apps' OpenGL, not to the compositor (Vulkan). | |
| 27 | 28 | WORKDIR /root | |
| 28 | 29 | # Everything runs as the container's root, which rootless Docker maps to the | |
| 29 | 30 | # host user, so the bind-mounted work directory stays owned by the host user. | |
| @@ -16,7 +16,7 @@ COPY foot.ini /root/.config/foot/foot.ini | |||||
| 16 | COPY btop.conf /root/.config/btop/btop.conf | 16 | COPY btop.conf /root/.config/btop/btop.conf | ||
| 17 | COPY btop-vesper.theme /root/.config/btop/themes/vesper.theme | 17 | COPY btop-vesper.theme /root/.config/btop/themes/vesper.theme | ||
| 18 | COPY demo-entry demo-hint-label /usr/local/bin/ | 18 | COPY demo-entry demo-hint-label /usr/local/bin/ | ||
| 19 | - | COPY config /root/config | 19 | + | # One config for both users: the compositor (root) reads it, Mousepad (demo) edits it. |
| 20 | COPY config /home/demo/config | 20 | COPY config /home/demo/config | ||
| 21 | COPY mousepad.keyfile /home/demo/.config/glib-2.0/settings/keyfile | 21 | COPY mousepad.keyfile /home/demo/.config/glib-2.0/settings/keyfile | ||
| 22 | RUN chown -R demo:demo /home/demo | 22 | RUN chown -R demo:demo /home/demo | ||
| @@ -24,6 +24,7 @@ ENV HOME=/root XDG_RUNTIME_DIR=/run/wl WAYLAND_DISPLAY=wayland-0 \ | |||||
| 24 | GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \ | 24 | GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \ | ||
| 25 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/wl/bus \ | 25 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/wl/bus \ | ||
| 26 | MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor | 26 | MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor | ||
| 27 | + | # LIBGL_ALWAYS_SOFTWARE applies to the apps' OpenGL, not to the compositor (Vulkan). | |||
| 27 | WORKDIR /root | 28 | WORKDIR /root | ||
| 28 | # Everything runs as the container's root, which rootless Docker maps to the | 29 | # Everything runs as the container's root, which rootless Docker maps to the | ||
| 29 | # host user, so the bind-mounted work directory stays owned by the host user. | 30 | # host user, so the bind-mounted work directory stays owned by the host user. | ||
M tools/demo/demo-hint-label +6 -3
| @@ -33,7 +33,8 @@ def ocr(): | |||
| 33 | 33 | if text and float(row["conf"]) > 30: | |
| 34 | 34 | x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height")) | |
| 35 | 35 | words.append([text, x + w / 2, y + h / 2]) | |
| 36 | - | json.dump(words, open(WORDS, "w")) | |
| 36 | + | with open(WORDS, "w") as f: | |
| 37 | + | json.dump(words, f) | |
| 37 | 38 | ||
| 38 | 39 | ||
| 39 | 40 | def newest_dump(since): | |
| @@ -47,8 +48,10 @@ def newest_dump(since): | |||
| 47 | 48 | ||
| 48 | 49 | ||
| 49 | 50 | def label(since, wanted): | |
| 50 | - | meta = json.load(open(newest_dump(since))) | |
| 51 | - | words = json.load(open(WORDS)) | |
| 51 | + | with open(newest_dump(since)) as f: | |
| 52 | + | meta = json.load(f) | |
| 53 | + | with open(WORDS) as f: | |
| 54 | + | words = json.load(f) | |
| 52 | 55 | # Core keeps the targets whose centre is on the output, then sorts them. | |
| 53 | 56 | width, height = meta["width"], meta["height"] | |
| 54 | 57 | targets = [t for t in meta["targets"] | |
| @@ -33,7 +33,8 @@ def ocr(): | |||||
| 33 | if text and float(row["conf"]) > 30: | 33 | if text and float(row["conf"]) > 30: | ||
| 34 | x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height")) | 34 | x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height")) | ||
| 35 | words.append([text, x + w / 2, y + h / 2]) | 35 | words.append([text, x + w / 2, y + h / 2]) | ||
| 36 | - | json.dump(words, open(WORDS, "w")) | 36 | + | with open(WORDS, "w") as f: |
| 37 | + | json.dump(words, f) | |||
| 37 | 38 | ||||
| 38 | 39 | ||||
| 39 | def newest_dump(since): | 40 | def newest_dump(since): | ||
| @@ -47,8 +48,10 @@ def newest_dump(since): | |||||
| 47 | 48 | ||||
| 48 | 49 | ||||
| 49 | def label(since, wanted): | 50 | def label(since, wanted): | ||
| 50 | - | meta = json.load(open(newest_dump(since))) | 51 | + | with open(newest_dump(since)) as f: |
| 51 | - | words = json.load(open(WORDS)) | 52 | + | meta = json.load(f) |
| 53 | + | with open(WORDS) as f: | |||
| 54 | + | words = json.load(f) | |||
| 52 | # Core keeps the targets whose centre is on the output, then sorts them. | 55 | # Core keeps the targets whose centre is on the output, then sorts them. | ||
| 53 | width, height = meta["width"], meta["height"] | 56 | width, height = meta["width"], meta["height"] | ||
| 54 | targets = [t for t in meta["targets"] | 57 | targets = [t for t in meta["targets"] | ||
M tools/demo/record.sh +32 -11
| @@ -1,7 +1,13 @@ | |||
| 1 | 1 | #!/usr/bin/env bash | |
| 2 | - | # Requires Go 1.27, rootless Docker, bash, ffmpeg and network access. | |
| 2 | + | # Requires Go 1.27, rootless Docker, bash, ffmpeg, network access and an AMD GPU. | |
| 3 | 3 | # Run from any directory. The image holds the compositor's runtime (tesseract, | |
| 4 | - | # AT-SPI, a Mesa Vulkan driver) and the apps; the host builds the binary. | |
| 4 | + | # AT-SPI, Mesa's radeon Vulkan driver) and the apps; the host builds the binary. | |
| 5 | + | # The compositor renders on the host GPU through /dev/dri, and hint scans need | |
| 6 | + | # the sync files only a hardware driver exports. The image only carries | |
| 7 | + | # vulkan-radeon, so another GPU needs its Mesa driver in tools/demo/Dockerfile | |
| 8 | + | # and the vendor check below changed. | |
| 9 | + | # A recording only succeeds if the compositor stops on the quit key that ends | |
| 10 | + | # tools/demo/input.txt: hitting --timeout first means a truncated video. | |
| 5 | 11 | # KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up. | |
| 6 | 12 | set -euo pipefail | |
| 7 | 13 | root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) | |
| @@ -10,6 +16,15 @@ for command in docker go ffmpeg; do | |||
| 10 | 16 | command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; } | |
| 11 | 17 | done | |
| 12 | 18 | docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; } | |
| 19 | + | amd='' | |
| 20 | + | for vendor in /sys/class/drm/renderD*/device/vendor; do | |
| 21 | + | if [[ -r $vendor && $(<"$vendor") == 0x1002 ]]; then amd=yes; fi | |
| 22 | + | done | |
| 23 | + | [[ -n $amd ]] || { | |
| 24 | + | echo 'No AMD GPU render node (PCI vendor 0x1002) found: the demo image only carries the radeon Vulkan driver.' >&2 | |
| 25 | + | echo 'See the header of this script to use another GPU.' >&2 | |
| 26 | + | exit 1 | |
| 27 | + | } | |
| 13 | 28 | work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX") | |
| 14 | 29 | launcher='' feeder='' success='' | |
| 15 | 30 | container=neferwl-demo-$$ | |
| @@ -47,15 +62,14 @@ exec 3<>"$work/input" | |||
| 47 | 62 | # it. Root in a rootless container is the host user: the bind-mounted work | |
| 48 | 63 | # directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user. | |
| 49 | 64 | # The GPU render node gives the compositor the sync files that hint scans need. | |
| 50 | - | gpu=() | |
| 51 | - | if [[ -d /dev/dri ]]; then gpu=(--device /dev/dri); else echo 'No /dev/dri: hint scans will fail.' >&2; fi | |
| 52 | - | docker run -d --name "$container" --hostname neferwl-demo "${gpu[@]}" \ | |
| 65 | + | docker run -d --name "$container" --hostname neferwl-demo --device /dev/dri \ | |
| 53 | 66 | -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \ | |
| 54 | 67 | -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \ | |
| 55 | - | --backend=headless --size 1920x1080 --config /root/config --no-xwayland \ | |
| 68 | + | --backend=headless --size 1920x1080 --config /home/demo/config --no-xwayland \ | |
| 56 | 69 | --screenshot /work/shots --input /work/input --timeout 75s >/dev/null | |
| 57 | 70 | display='' | |
| 58 | - | for ((attempt = 0; attempt < 300; attempt++)); do | |
| 71 | + | deadline=$((SECONDS + 30)) | |
| 72 | + | while ((SECONDS < deadline)); do | |
| 59 | 73 | [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] || | |
| 60 | 74 | { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | |
| 61 | 75 | if [[ -f $log ]]; then | |
| @@ -64,7 +78,7 @@ for ((attempt = 0; attempt < 300; attempt++)); do | |||
| 64 | 78 | fi | |
| 65 | 79 | sleep 0.1 | |
| 66 | 80 | done | |
| 67 | - | [[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor; see $log" >&2; exit 1; } | |
| 81 | + | [[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor to listen; see $log" >&2; exit 1; } | |
| 68 | 82 | # Mousepad runs as another user (see launch_apps) and needs the socket. | |
| 69 | 83 | docker exec "$container" chmod 666 "/run/wl/${display##*/}" | |
| 70 | 84 | app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; } | |
| @@ -134,17 +148,24 @@ feeder=$! | |||
| 134 | 148 | launch_apps & | |
| 135 | 149 | launcher=$! | |
| 136 | 150 | set +m | |
| 137 | - | # The compositor ends with the quit key at the end of input.txt, or at --timeout. | |
| 151 | + | # The compositor ends with the quit key at the end of input.txt. Hitting | |
| 152 | + | # --timeout first (a stalled timeline) leaves a truncated recording: that fails. | |
| 138 | 153 | status=$(docker wait "$container") | |
| 139 | - | if [[ $status != 0 ]]; then | |
| 140 | - | grep -q '"reason":"timeout"' "$log" || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | |
| 154 | + | if grep -q '"reason":"timeout"' "$log"; then | |
| 155 | + | echo 'The compositor hit --timeout before the quit key: the recording is truncated.' >&2 | |
| 156 | + | exit 1 | |
| 141 | 157 | fi | |
| 158 | + | [[ $status == 0 ]] || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | |
| 142 | 159 | wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; } | |
| 143 | 160 | feeder= | |
| 144 | 161 | wait "$launcher" | |
| 145 | 162 | launcher= | |
| 146 | 163 | # Firefox's links come from the accessibility tree; OCR (tesseract) must still load. | |
| 147 | 164 | if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi | |
| 165 | + | if grep -q 'capture needs sync file export' "$log"; then | |
| 166 | + | echo 'Hint scans failed: the compositor is not on a hardware Vulkan driver (radeon); see the header of this script.' >&2 | |
| 167 | + | exit 1 | |
| 168 | + | fi | |
| 148 | 169 | grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" || | |
| 149 | 170 | { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; } | |
| 150 | 171 | grep '"message":"hint scan"' "$log" | sed -E 's/.*"atspi_targets":([0-9]+).*"ocr_links":([0-9]+).*"targets":([0-9]+).*/hint scan: \1 accessibility + \2 OCR links = \3 targets/' | tail -3 | |
| @@ -1,7 +1,13 @@ | |||||
| 1 | #!/usr/bin/env bash | 1 | #!/usr/bin/env bash | ||
| 2 | - | # Requires Go 1.27, rootless Docker, bash, ffmpeg and network access. | 2 | + | # Requires Go 1.27, rootless Docker, bash, ffmpeg, network access and an AMD GPU. |
| 3 | # Run from any directory. The image holds the compositor's runtime (tesseract, | 3 | # Run from any directory. The image holds the compositor's runtime (tesseract, | ||
| 4 | - | # AT-SPI, a Mesa Vulkan driver) and the apps; the host builds the binary. | 4 | + | # AT-SPI, Mesa's radeon Vulkan driver) and the apps; the host builds the binary. |
| 5 | + | # The compositor renders on the host GPU through /dev/dri, and hint scans need | |||
| 6 | + | # the sync files only a hardware driver exports. The image only carries | |||
| 7 | + | # vulkan-radeon, so another GPU needs its Mesa driver in tools/demo/Dockerfile | |||
| 8 | + | # and the vendor check below changed. | |||
| 9 | + | # A recording only succeeds if the compositor stops on the quit key that ends | |||
| 10 | + | # tools/demo/input.txt: hitting --timeout first means a truncated video. | |||
| 5 | # KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up. | 11 | # KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up. | ||
| 6 | set -euo pipefail | 12 | set -euo pipefail | ||
| 7 | root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) | 13 | root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) | ||
| @@ -10,6 +16,15 @@ for command in docker go ffmpeg; do | |||||
| 10 | command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; } | 16 | command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; } | ||
| 11 | done | 17 | done | ||
| 12 | docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; } | 18 | docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; } | ||
| 19 | + | amd='' | |||
| 20 | + | for vendor in /sys/class/drm/renderD*/device/vendor; do | |||
| 21 | + | if [[ -r $vendor && $(<"$vendor") == 0x1002 ]]; then amd=yes; fi | |||
| 22 | + | done | |||
| 23 | + | [[ -n $amd ]] || { | |||
| 24 | + | echo 'No AMD GPU render node (PCI vendor 0x1002) found: the demo image only carries the radeon Vulkan driver.' >&2 | |||
| 25 | + | echo 'See the header of this script to use another GPU.' >&2 | |||
| 26 | + | exit 1 | |||
| 27 | + | } | |||
| 13 | work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX") | 28 | work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX") | ||
| 14 | launcher='' feeder='' success='' | 29 | launcher='' feeder='' success='' | ||
| 15 | container=neferwl-demo-$$ | 30 | container=neferwl-demo-$$ | ||
| @@ -47,15 +62,14 @@ exec 3<>"$work/input" | |||||
| 47 | # it. Root in a rootless container is the host user: the bind-mounted work | 62 | # it. Root in a rootless container is the host user: the bind-mounted work | ||
| 48 | # directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user. | 63 | # directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user. | ||
| 49 | # The GPU render node gives the compositor the sync files that hint scans need. | 64 | # The GPU render node gives the compositor the sync files that hint scans need. | ||
| 50 | - | gpu=() | 65 | + | docker run -d --name "$container" --hostname neferwl-demo --device /dev/dri \ |
| 51 | - | if [[ -d /dev/dri ]]; then gpu=(--device /dev/dri); else echo 'No /dev/dri: hint scans will fail.' >&2; fi | |||
| 52 | - | docker run -d --name "$container" --hostname neferwl-demo "${gpu[@]}" \ | |||
| 53 | -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \ | 66 | -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \ | ||
| 54 | -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \ | 67 | -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \ | ||
| 55 | - | --backend=headless --size 1920x1080 --config /root/config --no-xwayland \ | 68 | + | --backend=headless --size 1920x1080 --config /home/demo/config --no-xwayland \ |
| 56 | --screenshot /work/shots --input /work/input --timeout 75s >/dev/null | 69 | --screenshot /work/shots --input /work/input --timeout 75s >/dev/null | ||
| 57 | display='' | 70 | display='' | ||
| 58 | - | for ((attempt = 0; attempt < 300; attempt++)); do | 71 | + | deadline=$((SECONDS + 30)) |
| 72 | + | while ((SECONDS < deadline)); do | |||
| 59 | [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] || | 73 | [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] || | ||
| 60 | { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | 74 | { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | ||
| 61 | if [[ -f $log ]]; then | 75 | if [[ -f $log ]]; then | ||
| @@ -64,7 +78,7 @@ for ((attempt = 0; attempt < 300; attempt++)); do | |||||
| 64 | fi | 78 | fi | ||
| 65 | sleep 0.1 | 79 | sleep 0.1 | ||
| 66 | done | 80 | done | ||
| 67 | - | [[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor; see $log" >&2; exit 1; } | 81 | + | [[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor to listen; see $log" >&2; exit 1; } |
| 68 | # Mousepad runs as another user (see launch_apps) and needs the socket. | 82 | # Mousepad runs as another user (see launch_apps) and needs the socket. | ||
| 69 | docker exec "$container" chmod 666 "/run/wl/${display##*/}" | 83 | docker exec "$container" chmod 666 "/run/wl/${display##*/}" | ||
| 70 | app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; } | 84 | app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; } | ||
| @@ -134,17 +148,24 @@ feeder=$! | |||||
| 134 | launch_apps & | 148 | launch_apps & | ||
| 135 | launcher=$! | 149 | launcher=$! | ||
| 136 | set +m | 150 | set +m | ||
| 137 | - | # The compositor ends with the quit key at the end of input.txt, or at --timeout. | 151 | + | # The compositor ends with the quit key at the end of input.txt. Hitting |
| 152 | + | # --timeout first (a stalled timeline) leaves a truncated recording: that fails. | |||
| 138 | status=$(docker wait "$container") | 153 | status=$(docker wait "$container") | ||
| 139 | - | if [[ $status != 0 ]]; then | 154 | + | if grep -q '"reason":"timeout"' "$log"; then |
| 140 | - | grep -q '"reason":"timeout"' "$log" || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | 155 | + | echo 'The compositor hit --timeout before the quit key: the recording is truncated.' >&2 |
| 156 | + | exit 1 | |||
| 141 | fi | 157 | fi | ||
| 158 | + | [[ $status == 0 ]] || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; } | |||
| 142 | wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; } | 159 | wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; } | ||
| 143 | feeder= | 160 | feeder= | ||
| 144 | wait "$launcher" | 161 | wait "$launcher" | ||
| 145 | launcher= | 162 | launcher= | ||
| 146 | # Firefox's links come from the accessibility tree; OCR (tesseract) must still load. | 163 | # Firefox's links come from the accessibility tree; OCR (tesseract) must still load. | ||
| 147 | if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi | 164 | if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi | ||
| 165 | + | if grep -q 'capture needs sync file export' "$log"; then | |||
| 166 | + | echo 'Hint scans failed: the compositor is not on a hardware Vulkan driver (radeon); see the header of this script.' >&2 | |||
| 167 | + | exit 1 | |||
| 168 | + | fi | |||
| 148 | grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" || | 169 | grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" || | ||
| 149 | { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; } | 170 | { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; } | ||
| 150 | grep '"message":"hint scan"' "$log" | sed -E 's/.*"atspi_targets":([0-9]+).*"ocr_links":([0-9]+).*"targets":([0-9]+).*/hint scan: \1 accessibility + \2 OCR links = \3 targets/' | tail -3 | 171 | grep '"message":"hint scan"' "$log" | sed -E 's/.*"atspi_targets":([0-9]+).*"ocr_links":([0-9]+).*"targets":([0-9]+).*/hint scan: \1 accessibility + \2 OCR links = \3 targets/' | tail -3 | ||