From fca34270beb2a394a67f806772090c63f583345f Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 16:22:45 +0000 Subject: [PATCH 1/3] fix(kiosk): pass XDG_RUNTIME_DIR so Chromium can reach PipeWire-Pulse The launcher's sudo -u archipelago invocation set DISPLAY/HOME but not XDG_RUNTIME_DIR, so Chromium's audio backend couldn't find the PipeWire-Pulse socket at /run/user//pulse/native. It silently fell back to raw ALSA "default", which also failed ("Connection refused"), producing no HDMI audio at all with no visible error since --noerrdialogs suppresses it. Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/archipelago-kiosk-launcher.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 90d77e43..10cb435a 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -89,8 +89,14 @@ 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 \ From e559ccb76738e57240100961eedf8a224903a33b Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 17:02:43 +0000 Subject: [PATCH 2/3] fix(kiosk): restore in-process-gpu + CPUQuota=200% to stop choppy HDMI audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both had regressed back to the pre-2026-06-28-incident state: GPU_FLAGS used --enable-gpu-rasterization on any node with a GPU, and CPUQuota was 75%. On archy-x250-exp (Intel HD 5500 under X11) this spins a dedicated GPU process at 55-92% CPU (falls back to software compositing anyway), and the 75% cgroup quota throttled the kiosk unit ~81% of the time (nr_throttled 4856/6005) — the exact CPU-starvation signature documented as the choppy- audio root cause. Restores the validated fix: --in-process-gpu, --num-raster-threads=1, GpuRasterization disabled via --disable-features, CPUQuota=200%. Verified on archy-x250-exp: no separate gpu-process, throttling dropped to ~3% (nr_throttled 14/503). Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/archipelago-kiosk-launcher.sh | 14 ++++++++------ image-recipe/configs/archipelago-kiosk.service | 5 ++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 10cb435a..3651fd3c 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -79,12 +79,14 @@ 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 @@ -103,7 +105,7 @@ while true; do --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 5db41f3c..d05e425f 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% MemoryMax=1500M MemoryHigh=1200M From 9b6ec0be97ef09d2ee344c6b8612309b5f0a6c2b Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 20:25:34 +0000 Subject: [PATCH 3/3] fix(kiosk): track /etc/asound.conf in image-recipe and sync it on deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes ALSA's "default" device through PulseAudio/PipeWire. Was a manual, untracked fix living only on archy-x250-exp — a reprovision or fresh image would silently lose it, same failure mode that already bit the GPU-flags and CPUQuota fixes. Wired into deploy-to-target.sh (both the primary --live path and the .198/.253 secondary-copy path) and deploy-tailscale.sh, mirroring the existing 99-mesh-radio.rules udev sync pattern (diff-check, copy if changed). Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/asound.conf | 7 +++++++ scripts/deploy-tailscale.sh | 16 ++++++++++++++++ scripts/deploy-to-target.sh | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 image-recipe/configs/asound.conf 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 cc85eb77..bfb88346 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 c7533c89..7b133fda 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -456,6 +456,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 @@ -762,6 +778,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" '