jtube-tv/scripts/start-emulator.sh
Jeena 730bda0b40 emu: pin windowed mode to the integrated GPU
The discrete card now runs continuous AI inference, so the emulator must
not claim GL/Vulkan contexts on it. In windowed mode the emulator uses
the host GL/Vulkan stack, and the default GLVND selection picks the
discrete card on this machine. The script now discovers the first
display device whose driver is not nvidia and pins the emulator process
to it (Mesa GLX/EGL + DRI_PRIME + the Intel Vulkan ICD).

Headless (the default) needs no change: it always ran on swiftshader,
strictly software, and never touched any GPU.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 13:30:52 +00:00

169 lines
No EOL
6.4 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# start-emulator.sh — boot the JTube TV test emulator on the HOST (not inside the
# agent container), with GPU acceleration, headless by default (drive it via
# adb/scrcpy). JTUBE_EMU_MODE=windowed shows the Qt window.
#
# Boots on console :5600 (adbd :5601); the agent container bridges
# rutherford:5556 -> 127.0.0.1:5601 with socat, so after this script the emulator is
# reachable from anywhere as:
#
# adb connect rutherford:5556 && scrcpy -s rutherford:5556
#
# An RSA authorization popup appears on the emulator window on first connect —
# click "Always allow"/OK.
set -euo pipefail
SDK="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}"
EMU="$SDK/emulator/emulator"
ADB="$SDK/platform-tools/adb"
SDKM="$SDK/cmdline-tools/latest/bin/sdkmanager"
AVDM="$SDK/cmdline-tools/latest/bin/avdmanager"
AVD="${1:-jtubesc}"
PORT=5600
IMAGE="system-images;android-36;android-tv;x86_64"
[ -x "$EMU" ] || { echo "emulator not found at $EMU — install Android SDK cmdline-tools first"; exit 1; }
# --- bootstrap: java, system image, AVD -------------------------------------
if ! command -v java >/dev/null; then
echo "no java on PATH — required by sdkmanager/AVD tools"
echo " install it (Arch): sudo pacman -S --noconfirm jdk21-openjdk"
echo " then re-run this script"
exit 1
fi
if ! "$SDKM" --list_installed 2>/dev/null | grep -q "$IMAGE"; then
echo "installing $IMAGE (~1.5 GB, once)…"
yes | "$SDKM" "$IMAGE" > /dev/null
echo "installed"
fi
if [ ! -f "$HOME/.android/avd/$AVD.ini" ]; then
echo "creating AVD '$AVD'…"
echo no | "$AVDM" create avd -n "$AVD" -k "$IMAGE" -d tv_1080p > /dev/null
fi
# --- free stale emulator instances holding our port --------------------------
for pid in $(ps aux | awk '/[q]emu-system-x86_64/ {print $2}' 2>/dev/null); do
if tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q "\-port $PORT"; then
echo "Killing stale emulator pid $pid"
kill -9 "$pid" 2>/dev/null || true
fi
done
sleep 1
MODE="${JTUBE_EMU_MODE:-headless}" # headless (default) | windowed
WINDOWED=1
ACCEL=()
if [ "$MODE" = windowed ]; then
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then
echo "windowed (JTUBE_EMU_MODE=windowed) — emulator window + GPU host"
else
echo "JTUBE_EMU_MODE=windowed but no display detected — falling back to headless"
WINDOWED=0
ACCEL+=(-no-window -gpu swiftshader_indirect)
fi
else
WINDOWED=0
# Headless has no X display, so -gpu host/GLX can't init EGL ("Failed to get EGL
# display") and the emulator dies on launch — fall back to software rendering.
ACCEL+=(-no-window -gpu swiftshader_indirect)
echo "headless (default; JTUBE_EMU_MODE=windowed for a window) — no emulator window"
fi
if [ -e /dev/kvm ]; then
ACCEL+=(-accel on)
echo "/dev/kvm found — hardware acceleration on"
fi
# Pin the emulator process to the integrated GPU so the discrete card stays free
# for AI inference (windowed mode uses the host GL/Vulkan stack; headless runs on
# swiftshader — pure software — and never touches any GPU at all). Discovered
# from the PCI tree: first display device NOT driven by the nvidia driver.
IGPU=""
for dev in /sys/bus/pci/devices/*; do
class=$(cat "$dev/class" 2>/dev/null) || continue
case "$class" in
0x0300*|0x0380*) ;; # VGA / display controller
*) continue ;;
esac
if [ "$(basename "$(readlink "$dev/driver" 2>/dev/null)")" = nvidia ]; then continue; fi
IGPU=$(basename "$dev")
break
done
if [ -n "$IGPU" ]; then
# Mesa GL/GLES: force the Mesa vendors, then the Intel render device.
export __GLX_VENDOR_LIBRARY_NAME=mesa
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export DRI_PRIME="pci-${IGPU//./_}"
# Vulkan: restrict the loader to the iGPU's ICD (both var names — loader
# version differences).
for icd in /usr/share/vulkan/icd.d/intel_icd.x86_64.json \
/usr/share/vulkan/icd.d/Intel_icd.x86_64.json; do
if [ -f "$icd" ]; then
export VK_ICD_FILENAMES="$icd"
export VK_DRIVER_FILES="$icd"
break
fi
done
echo "GPU pinned to iGPU $IGPU (Mesa/Intel env) — dGPU untouched"
else
echo "no iGPU found — leaving GPU selection to the driver (dGPU may be used)"
fi
# --- build combined adb vendor-keys file ------------------------------------
# The TV system image is a user build: adb push to /data/misc/adb/adb_keys is
# blocked, so remote keys are trusted via ADB_VENDOR_KEYS at launch instead.
# Drop extra public keys into /tmp/jtube-adbkeys/ (one per file), e.g. from the
# agent container: docker exec <ctr> sh -c 'cat ~/.android/adbkey.pub' > /tmp/jtube-adbkeys/agent-container.pub
mkdir -p /tmp/jtube-adbkeys
VKEYS=/tmp/jtube-adbkeys-combined
{
cat "$HOME/.android/adbkey.pub"
# Optional extra keys — must not fail the script under `set -e` when absent.
shopt -s nullglob
for k in /tmp/jtube-adbkeys/*.pub; do cat "$k"; done
shopt -u nullglob
} > "$VKEYS"
chmod 600 "$VKEYS"
echo "Booting AVD '$AVD' …"
ADB_VENDOR_KEYS="$VKEYS" setsid nohup "$EMU" -avd "$AVD" -port "$PORT" "${ACCEL[@]:-}" \
-no-snapshot > /tmp/jtube-emu.log 2>&1 < /dev/null &
echo "emulator PID $! — log: /tmp/jtube-emu.log"
for i in $(seq 1 90); do
# adb exits non-zero while the device is still booting — don't let
# pipefail+set -e kill the wait loop.
ok=$( ("$ADB" -s "emulator-$PORT" shell getprop sys.boot_completed 2>/dev/null || true) | tr -d '\r')
if [ "$ok" = "1" ]; then echo "BOOTED"; break; fi
sleep 5
done
# --- the bridge: advertise adbd on the LAN as :5556 ---------------------------
if ! ss -tln 2>/dev/null | grep -q ":5556"; then
if command -v socat > /dev/null; then
setsid nohup socat TCP-LISTEN:5556,bind=0.0.0.0,fork,reuseaddr TCP:127.0.0.1:$((PORT+1)) > /tmp/jtube-socat.log 2>&1 < /dev/null &
echo "socat bridge listening on :5556 (pid $!)"
else
echo "install socat for the remote bridge: sudo pacman -S --noconfirm socat"
fi
fi
# Keep the JTube TV app current + running so scrcpy lands on it.
APK="$HOME/Projects/jtube-tv/app/build/outputs/apk/debug/app-debug.apk"
if [ -f "$APK" ]; then
echo "Installing JTube TV…"
"$ADB" -s "emulator-$PORT" install -r "$APK" >/dev/null && echo "installed"
"$ADB" -s "emulator-$PORT" shell am start -n net.jeena.jtubetv/.MainActivity && echo "app started"
fi
cat <<EOF
Ready. From any machine that can reach rutherford:
adb connect rutherford:5556
scrcpy -s rutherford:5556
If scrcpy says "unauthorized", click the RSA dialog on the emulator window.
EOF