docs: VM handoff — CLAUDE.md, Ubuntu bootstrap, headless-Sway setup for M0
Prepares the move to the NVIDIA-GPU Ubuntu VM where M0/M2 run (macOS can't drive the Wayland/GPU stack). The repo carries the context, since Claude Code sessions are machine-local and don't transfer. - CLAUDE.md: project state + design invariants + don't-regress security notes. Auto-loads every session, so a fresh session on the VM continues from here. - scripts/bootstrap-ubuntu.sh: verifies the (already-installed) NVIDIA/NVENC stack, installs rustup + PipeWire/portal/wlroots/Sway + DRM/EGL/GBM/VA dev deps; GATES the FFmpeg -dev headers so apt can't clobber a custom NVENC build; checks nvidia-drm.modeset. - scripts/headless/: headless-Sway + xdg-desktop-portal-wlr config templates, the NVIDIA-wlroots env workarounds, run-headless-sway.sh, and a wf-recorder->hevc_nvenc capture smoke test (proves capture->NVENC with no Rust). - docs/linux-setup.md: M0 walkthrough + verified gotchas (modeset, headless backend, vGPU NVENC licensing, dmabuf->NVENC CPU-copy fallback, FFmpeg-dev gate, crate versions). Ubuntu 24.04 package names/versions verified against the live archive; scripts pass shellcheck and `bash -n`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bootstrap an Ubuntu (24.04 "noble") NVIDIA-GPU VM to build/run the lumen Linux host
|
||||
# and the M0 capture spike (headless Sway/wlroots -> PipeWire -> NVENC).
|
||||
#
|
||||
# Assumes the NVIDIA driver + an FFmpeg-with-NVENC are ALREADY installed (verify-only).
|
||||
# Installs: rustup toolchain, build deps, PipeWire/portal/wlroots/Sway, DRM/EGL/VA dev
|
||||
# libs. Does NOT touch your existing FFmpeg (gated) and does NOT auto-reboot or edit GRUB
|
||||
# — it prints exact commands when a reboot-requiring change (nvidia-drm modeset) is needed.
|
||||
#
|
||||
# Idempotent; safe to re-run. Usage: bash scripts/bootstrap-ubuntu.sh
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
||||
ok() { printf '\033[1;32m ok\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m !!\033[0m %s\n' "$*" >&2; }
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Preflight"
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! have apt-get; then
|
||||
warn "This script targets Ubuntu/Debian (apt). Aborting."
|
||||
exit 1
|
||||
fi
|
||||
CODENAME="$(. /etc/os-release 2>/dev/null && echo "${VERSION_CODENAME:-unknown}")"
|
||||
case "$CODENAME" in
|
||||
noble) ok "Ubuntu 24.04 (noble) — the recommended target" ;;
|
||||
jammy) warn "Ubuntu 22.04 (jammy): Sway 1.7 / FFmpeg 4.4 are too old for the M0 path. \
|
||||
Strongly prefer 24.04, or build Sway/wlroots + FFmpeg 7.x from source here." ;;
|
||||
*) warn "Unrecognized release '$CODENAME' — proceeding, but package names are tuned for noble." ;;
|
||||
esac
|
||||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Verifying the NVIDIA + NVENC stack (no install — you said it's set up)"
|
||||
# ---------------------------------------------------------------------------
|
||||
if have nvidia-smi; then
|
||||
nvidia-smi --query-gpu=name,driver_version --format=csv,noheader | sed 's/^/ GPU: /'
|
||||
DRV="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -1 | cut -d. -f1)"
|
||||
[ "${DRV:-0}" -ge 535 ] 2>/dev/null \
|
||||
&& ok "driver ${DRV}.x (>=535 required for headless wlroots)" \
|
||||
|| warn "driver appears <535; headless wlroots EGL/dmabuf needs >=535 (550+ recommended)."
|
||||
if nvidia-smi -q 2>/dev/null | grep -qi 'vGPU Software Licensed Product'; then
|
||||
nvidia-smi -q | grep -iE 'License Status|Licensed' | sed 's/^/ vGPU /' || true
|
||||
warn "This looks like NVIDIA vGPU — NVENC needs a valid vGPU license (vWS). \
|
||||
Full PCI passthrough needs no license. Confirm the smoke-encode below actually succeeds."
|
||||
else
|
||||
ok "no vGPU license line — looks like bare-metal / PCI passthrough (NVENC unlicensed-OK)"
|
||||
fi
|
||||
else
|
||||
warn "nvidia-smi not found — the NVIDIA driver is not installed/visible. M0 encode will fail."
|
||||
fi
|
||||
|
||||
if have ffmpeg; then
|
||||
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -qE 'hevc_nvenc|h264_nvenc'; then
|
||||
ok "FFmpeg has NVENC: $(ffmpeg -hide_banner -encoders 2>/dev/null | grep -oE '(hevc|h264)_nvenc' | paste -sd' ' -)"
|
||||
log " smoke-test: 1s HEVC NVENC encode to null"
|
||||
if ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=black:s=1280x720:d=1 \
|
||||
-c:v hevc_nvenc -preset p1 -tune ull -f null - 2>/tmp/lumen_nvenc.err; then
|
||||
ok "hevc_nvenc encode succeeded — NVENC is usable in this guest"
|
||||
else
|
||||
warn "hevc_nvenc encode FAILED (see /tmp/lumen_nvenc.err). Common cause on a VM: \
|
||||
missing libnvidia-encode.so.1 or an unlicensed vGPU."
|
||||
fi
|
||||
else
|
||||
warn "FFmpeg present but no *_nvenc encoder listed — rebuild FFmpeg with --enable-nvenc."
|
||||
fi
|
||||
ldconfig -p 2>/dev/null | grep -qi 'libnvidia-encode.so' \
|
||||
&& ok "libnvidia-encode.so present (runtime NVENC lib)" \
|
||||
|| warn "libnvidia-encode.so not found by ldconfig — NVENC will fail at runtime."
|
||||
else
|
||||
warn "ffmpeg not on PATH."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Enabling universe + multiverse (needed for xdg-desktop-portal-wlr, libnvidia-egl-gbm1)"
|
||||
# ---------------------------------------------------------------------------
|
||||
$SUDO add-apt-repository -y universe >/dev/null 2>&1 || warn "could not enable universe (continuing)"
|
||||
$SUDO add-apt-repository -y multiverse >/dev/null 2>&1 || warn "could not enable multiverse (continuing)"
|
||||
$SUDO apt-get update -y
|
||||
|
||||
# apt_install GROUP_LABEL pkg... — required group (aborts on failure)
|
||||
apt_install() { local label="$1"; shift; log "apt: $label"; $SUDO apt-get install -y "$@"; }
|
||||
# apt_try GROUP_LABEL pkg... — optional group (warns, continues)
|
||||
apt_try() { local label="$1"; shift; log "apt (optional): $label"; $SUDO apt-get install -y "$@" || warn "$label: some packages unavailable on $CODENAME (continuing)"; }
|
||||
|
||||
apt_install "build toolchain" build-essential pkg-config cmake clang libclang-dev nasm git curl ca-certificates
|
||||
apt_install "PipeWire + dev" pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev
|
||||
apt_install "desktop portals" xdg-desktop-portal xdg-desktop-portal-wlr xdg-desktop-portal-gtk
|
||||
apt_install "Sway + wlroots" sway swaybg xwayland wlr-randr foot seatd
|
||||
apt_install "Wayland dev" libwayland-dev wayland-protocols wayland-utils
|
||||
apt_install "DRM/EGL/GBM/VA" libdrm-dev libgbm-dev libgbm1 libegl-dev libegl1 libgles-dev mesa-common-dev libva-dev
|
||||
apt_install "capture + dbus" wf-recorder grim dbus-user-session drm-info mesa-utils
|
||||
apt_try "NVIDIA EGL platform (multiverse)" libnvidia-egl-wayland1 libnvidia-egl-gbm1
|
||||
apt_try "libei (noble only; reis is pure-Rust so optional)" libei-dev
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "FFmpeg dev headers (gated — must NOT clobber your custom NVENC build)"
|
||||
# ---------------------------------------------------------------------------
|
||||
if pkg-config --exists libavcodec 2>/dev/null; then
|
||||
ok "system FFmpeg exposes pkg-config (libavcodec $(pkg-config --modversion libavcodec)). \
|
||||
The ffmpeg-next/rsmpeg crates will link it directly — NOT installing apt libav*-dev."
|
||||
PREFIX="$(pkg-config --variable=prefix libavcodec 2>/dev/null || true)"
|
||||
[ -n "$PREFIX" ] && echo " FFmpeg prefix: $PREFIX (if non-standard, export FFMPEG_DIR=$PREFIX before 'cargo build')"
|
||||
else
|
||||
warn "No FFmpeg .pc on the default pkg-config path. Your custom build's headers aren't discoverable."
|
||||
echo " Pick ONE before building the host encoder crate:"
|
||||
echo " A) export FFMPEG_DIR=/path/to/ffmpeg/prefix (and PKG_CONFIG_PATH=\$FFMPEG_DIR/lib/pkgconfig)"
|
||||
echo " B) last resort: sudo apt-get install -y libavcodec-dev libavformat-dev libavutil-dev \\"
|
||||
echo " libavfilter-dev libavdevice-dev libswscale-dev libswresample-dev"
|
||||
echo " (NOTE: apt's FFmpeg on noble is 6.1.1 and may shadow your NVENC build.)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Rust toolchain (rustup — never apt's rustc/cargo, which would conflict)"
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! have cargo; then
|
||||
log "installing rustup"
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
# shellcheck disable=SC1090
|
||||
. "$HOME/.cargo/env"
|
||||
fi
|
||||
have cargo && { rustup component add clippy rustfmt >/dev/null 2>&1 || true; ok "$(rustc --version)"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Checking NVIDIA DRM modeset (required for any Wayland on NVIDIA)"
|
||||
# ---------------------------------------------------------------------------
|
||||
MODESET="$(cat /sys/module/nvidia_drm/parameters/modeset 2>/dev/null || echo '?')"
|
||||
if [ "$MODESET" = "Y" ]; then
|
||||
ok "nvidia-drm.modeset=Y"
|
||||
else
|
||||
warn "nvidia-drm.modeset=$MODESET — Wayland/wlroots will not start. Enable it, then REBOOT:"
|
||||
echo " echo 'options nvidia-drm modeset=1 fbdev=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf"
|
||||
echo " sudo update-initramfs -u && sudo reboot"
|
||||
fi
|
||||
[ -e /dev/dri/renderD128 ] && ok "/dev/dri/renderD128 present (used for headless render + NVENC)" \
|
||||
|| warn "/dev/dri/renderD128 missing — check the NVIDIA driver / DRM."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Installing headless-Sway + portal config templates (only if absent)"
|
||||
# ---------------------------------------------------------------------------
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
install_cfg() { # src dest
|
||||
if [ -e "$2" ]; then ok "exists, leaving as-is: $2"; else mkdir -p "$(dirname "$2")"; cp "$1" "$2"; ok "wrote $2"; fi
|
||||
}
|
||||
install_cfg "$HERE/headless/sway.config" "$HOME/.config/sway/config"
|
||||
install_cfg "$HERE/headless/xdpw.config" "$HOME/.config/xdg-desktop-portal-wlr/config"
|
||||
install_cfg "$HERE/headless/portals.conf" "$HOME/.config/xdg-desktop-portal/sway-portals.conf"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Done. Next steps:"
|
||||
# ---------------------------------------------------------------------------
|
||||
cat <<'NEXT'
|
||||
1. If modeset was not Y above: enable it and reboot (commands printed above).
|
||||
2. Confirm the core still builds + passes on Linux:
|
||||
cargo test --workspace
|
||||
3. Bring up the headless compositor (prints WAYLAND_DISPLAY, default wayland-1):
|
||||
bash scripts/headless/run-headless-sway.sh
|
||||
4. In a second shell on the same user, prove capture->NVENC end to end (no Rust yet):
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-1
|
||||
swaymsg -t get_outputs # confirm HEADLESS-1
|
||||
bash scripts/headless/capture-smoke-test.sh # wf-recorder -> hevc_nvenc -> /tmp/*.mkv
|
||||
5. Then start M0 proper: see docs/linux-setup.md.
|
||||
NEXT
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prove the capture->NVENC chain end to end WITHOUT writing any Rust, using wf-recorder
|
||||
# (wlr-screencopy) piped into hevc_nvenc. Run from a shell where headless Sway is up and
|
||||
# WAYLAND_DISPLAY + XDG_RUNTIME_DIR are exported (see run-headless-sway.sh output).
|
||||
#
|
||||
# Usage: scripts/headless/capture-smoke-test.sh [output.mkv] [WxH-unused]
|
||||
# Ctrl-C to stop; then play/inspect the file (e.g. ffprobe out.mkv).
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${1:-/tmp/lumen-headless-test.mkv}"
|
||||
: "${WAYLAND_DISPLAY:?set WAYLAND_DISPLAY (e.g. wayland-1) — is headless Sway running?}"
|
||||
: "${XDG_RUNTIME_DIR:?set XDG_RUNTIME_DIR=/run/user/\$(id -u)}"
|
||||
|
||||
command -v wf-recorder >/dev/null || { echo "wf-recorder missing — run scripts/bootstrap-ubuntu.sh"; exit 1; }
|
||||
|
||||
echo "recording HEADLESS-1 -> $OUT (wlr-screencopy -> hevc_nvenc, low-latency). Ctrl-C to stop."
|
||||
# Low-latency NVENC params verified for FFmpeg 6.x/7.x: preset p1, tune ull, no B-frames.
|
||||
exec wf-recorder -o HEADLESS-1 -c hevc_nvenc \
|
||||
-p preset=p1 -p tune=ull -p rc=cbr -p bf=0 -p delay=0 \
|
||||
-f "$OUT"
|
||||
@@ -0,0 +1,16 @@
|
||||
# shellcheck shell=bash
|
||||
# Source before launching headless Sway / the lumen host on an NVIDIA VM:
|
||||
# source scripts/headless/env.sh
|
||||
# These are the wlroots-on-NVIDIA workarounds the research turned up (gles2 is the
|
||||
# known-good renderer; Vulkan is flaky on the proprietary driver — try it only later).
|
||||
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
export XDG_CURRENT_DESKTOP=sway # so xdg-desktop-portal selects the wlr backend
|
||||
export WLR_BACKENDS=headless # no physical connectors on a headless VM GPU
|
||||
export WLR_LIBINPUT_NO_DEVICES=1 # don't error on having no input devices
|
||||
export WLR_NO_HARDWARE_CURSORS=1 # NVIDIA hw cursors are broken under wlroots
|
||||
export WLR_RENDERER=gles2 # known-good on NVIDIA; Vulkan often won't start
|
||||
export __GLX_VENDOR_LIBRARY_NAME=nvidia # GLVND: dispatch GL/EGL to the NVIDIA driver
|
||||
export GBM_BACKEND=nvidia-drm # route GBM through nvidia-drm (libnvidia-egl-gbm1)
|
||||
# Escape hatch to prove the pipeline with a software renderer if EGL/GBM misbehaves:
|
||||
# export WLR_RENDERER_ALLOW_SOFTWARE=1 WLR_RENDERER=pixman
|
||||
@@ -0,0 +1,5 @@
|
||||
# ~/.config/xdg-desktop-portal/sway-portals.conf (xdg-desktop-portal 1.18+ format)
|
||||
# Force the wlroots backend to service ScreenCast; gtk handles the rest.
|
||||
[preferred]
|
||||
default=gtk
|
||||
org.freedesktop.impl.portal.ScreenCast=wlr
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch headless Sway on the NVIDIA VM with a private D-Bus session so the ScreenCast
|
||||
# portal activates. Prints the WAYLAND_DISPLAY (default wayland-1) to use from other shells.
|
||||
#
|
||||
# Prereq: scripts/bootstrap-ubuntu.sh has run and nvidia-drm.modeset=Y (reboot if not).
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$HERE/env.sh"
|
||||
mkdir -p "$XDG_RUNTIME_DIR" 2>/dev/null || true
|
||||
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true
|
||||
|
||||
echo "starting headless Sway (renderer=$WLR_RENDERER). From another shell on this user:"
|
||||
echo " export XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR WAYLAND_DISPLAY=wayland-1"
|
||||
echo " swaymsg -t get_outputs # expect HEADLESS-1"
|
||||
echo
|
||||
|
||||
# --unsupported-gpu is mandatory on Sway 1.9 with the proprietary NVIDIA driver.
|
||||
exec dbus-run-session -- sway --unsupported-gpu
|
||||
@@ -0,0 +1,10 @@
|
||||
# Minimal headless Sway config for the lumen M0 capture spike.
|
||||
# Under WLR_BACKENDS=headless, Sway 1.9 auto-creates one output named HEADLESS-1
|
||||
# (fixed 1920x1080); this just resizes it. For extra outputs: `swaymsg create_output`
|
||||
# (auto-named HEADLESS-2, ...). Set the resolution to your target client size.
|
||||
|
||||
xwayland disable
|
||||
output HEADLESS-1 resolution 1920x1080 position 0 0
|
||||
|
||||
# A terminal is handy when poking at the session over `swaymsg`.
|
||||
bindsym Mod4+Return exec foot
|
||||
@@ -0,0 +1,6 @@
|
||||
# ~/.config/xdg-desktop-portal-wlr/config
|
||||
# Headless ScreenCast: there is no GUI output chooser, so select the output by name.
|
||||
[screencast]
|
||||
chooser_type=none
|
||||
output_name=HEADLESS-1
|
||||
max_fps=60
|
||||
Reference in New Issue
Block a user