diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 2f34a33e..1e227760 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -83,25 +83,33 @@ xset s noblank 2>/dev/null || true pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true sleep 1 -# GPU vs headless (#36). On a real kiosk display with a GPU, GPU rasterization is -# fast. On a GPU-less / headless server (no /dev/dri), --enable-gpu-rasterization -# forces GPU paths that fall back to software compositing and SPIN a full core at -# ~92% CPU, saturating the node. Detect the GPU and pick safe flags accordingly. +# GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization +# spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500) +# because under X11 it falls back to software compositing anyway — that CPU +# starvation is what caused choppy HDMI audio. --in-process-gpu avoids the +# separate process; GpuRasterization is also disabled via --disable-features below. +# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead. if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then - GPU_FLAGS="--enable-gpu-rasterization --num-raster-threads=2" + GPU_FLAGS="--in-process-gpu --num-raster-threads=1" else GPU_FLAGS="--disable-gpu --num-raster-threads=1" fi +ARCHIPELAGO_UID=$(id -u archipelago) + while true; do - sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago chromium --kiosk \ + # XDG_RUNTIME_DIR must be passed explicitly — without it Chromium's audio + # backend can't find PipeWire-Pulse's socket at /run/user//pulse/native, + # falls back to raw ALSA "default", fails to connect, and produces no audio + # at all with no visible error (--noerrdialogs suppresses it). + sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago XDG_RUNTIME_DIR=/run/user/$ARCHIPELAGO_UID chromium --kiosk \ --app=http://localhost/kiosk?safe_area_x=${KIOSK_SAFE_AREA_X_PX:-0}\&safe_area_y=${KIOSK_SAFE_AREA_Y_PX:-0} \ --noerrdialogs \ --disable-infobars \ --disable-translate \ --no-first-run \ --check-for-update-interval=31536000 \ - --disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled \ + --disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \ --disable-session-crashed-bubble \ --disable-save-password-bubble \ --disable-suggestions-service \ diff --git a/image-recipe/configs/archipelago-kiosk.service b/image-recipe/configs/archipelago-kiosk.service index ad996448..b388c4f5 100644 --- a/image-recipe/configs/archipelago-kiosk.service +++ b/image-recipe/configs/archipelago-kiosk.service @@ -25,8 +25,11 @@ RestartSec=5 # backend (it caused the .198 receive timeout + deploy storms). Cap CPU + memory # so a runaway kiosk can never take the whole machine down; Delegate so the cap # also binds the chromium/Xorg children in this unit's cgroup. +# CPUQuota=75% (0.75 cores) was too tight even for normal playback — the kiosk +# was throttled ~40% of the time, which is what caused choppy HDMI audio on +# archy-x250-exp (2026-06-28 incident). 200% (2 cores) gives enough headroom. Delegate=yes -CPUQuota=75% +CPUQuota=200% # Raised from 1500M/1200M: a Framework (Tiger Lake) kiosk sat at 806M used / # 1.1G peak, riding the old MemoryHigh reclaim-throttle line — the throttling # itself was the perceived UI lag. Keep Max well above real peaks; High stays diff --git a/image-recipe/configs/asound.conf b/image-recipe/configs/asound.conf new file mode 100644 index 00000000..8a1f1f07 --- /dev/null +++ b/image-recipe/configs/asound.conf @@ -0,0 +1,7 @@ +pcm.!default { + type pulse + hint.description "Default ALSA Device (via PulseAudio)" +} +ctl.!default { + type pulse +} diff --git a/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index 51853135..28a2b506 100755 --- a/scripts/deploy-tailscale.sh +++ b/scripts/deploy-tailscale.sh @@ -425,6 +425,22 @@ deploy_node() { ' 2>/dev/null || true fi + # ── Deploy ALSA default-device config ──────────────────────────── + ASOUND_CONF="$PROJECT_DIR/image-recipe/configs/asound.conf" + if [ -f "$ASOUND_CONF" ]; then + step "Deploying ALSA default-device config" + scp $SSH_OPTS "$ASOUND_CONF" "$TARGET:/tmp/asound.conf" 2>/dev/null || true + ssh $SSH_OPTS "$TARGET" ' + if ! diff -q /tmp/asound.conf /etc/asound.conf >/dev/null 2>&1; then + sudo cp /tmp/asound.conf /etc/asound.conf + echo " Installed" + else + echo " Unchanged" + fi + rm -f /tmp/asound.conf + ' 2>/dev/null || true + fi + # ── Step 18: NTP + swap ────────────────────────────────────────── step "Ensuring NTP + swap" ssh $SSH_OPTS "$TARGET" ' diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index c93f3a50..69824a6d 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -463,6 +463,22 @@ deploy_secondary() { ' 2>/dev/null || true fi + # Deploy ALSA default-device config (routes ALSA "default" through PulseAudio/PipeWire) + ASOUND_CONF="$PROJECT_DIR/image-recipe/configs/asound.conf" + if [ -f "$ASOUND_CONF" ]; then + echo " Syncing ALSA default-device config to .$SEC_LABEL..." + scp $SSH_OPTS "$ASOUND_CONF" "$SEC_TARGET:/tmp/asound.conf" 2>/dev/null || true + ssh $SSH_OPTS "$SEC_TARGET" ' + if ! diff -q /tmp/asound.conf /etc/asound.conf >/dev/null 2>&1; then + sudo cp /tmp/asound.conf /etc/asound.conf + echo " ALSA default-device config installed" + else + echo " ALSA default-device config unchanged" + fi + rm -f /tmp/asound.conf + ' 2>/dev/null || true + fi + # Dev mode + FileBrowser ssh $SSH_OPTS "$SEC_TARGET" ' # Dev mode @@ -805,6 +821,23 @@ if [ "$LIVE" = true ]; then ' 2>/dev/null || true fi + # Deploy ALSA default-device config (routes ALSA "default" through + # PulseAudio/PipeWire — without it Chromium's raw ALSA fallback can't + # reach the HDMI sink and kiosk HDMI audio is silent). + ASOUND_CONF="$PROJECT_DIR/image-recipe/configs/asound.conf" + if [ -f "$ASOUND_CONF" ]; then + scp $SSH_OPTS "$ASOUND_CONF" "$TARGET_HOST:/tmp/asound.conf" 2>/dev/null || true + ssh $SSH_OPTS "$TARGET_HOST" ' + if ! diff -q /tmp/asound.conf /etc/asound.conf >/dev/null 2>&1; then + sudo cp /tmp/asound.conf /etc/asound.conf + echo " ALSA default-device config installed" + else + echo " ALSA default-device config unchanged" + fi + rm -f /tmp/asound.conf + ' 2>/dev/null || true + fi + # Deploy Claude API proxy (auto-install if missing) progress "Setting up Claude API proxy" ssh $SSH_OPTS "$TARGET_HOST" '