diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9949206 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# CLAUDE.md — lumen + +Low-latency desktop streaming stack, Linux-first, with a shared Rust protocol core +(`lumen-core`) exposed over a C ABI and native clients per platform. Full design: +[`docs/implementation-plan.md`](docs/implementation-plan.md). Status table: `README.md`. + +## Where the work stands + +- **M1 (`lumen-core` + C ABI) is complete, tested, and hardened.** It builds and its full + suite passes (FEC recovery, loopback-under-loss, proptests, a C ABI harness). It was put + through an adversarial review and 13 verified findings were fixed + regression-tested + (commit `a913042`). +- **The host backends are `#[cfg(target_os = "linux")]` stubs.** They compile everywhere + but `bail!` until implemented. This is the next work (**M0**, then **M2**) and needs a + real Linux GPU + Wayland stack — which is why this repo is being moved to the NVIDIA + Ubuntu VM. + +## Build / test / run + +```sh +cargo build --workspace # green on Linux and macOS +cargo test --workspace # unit + loopback + proptest + C ABI harness +cargo clippy --workspace --all-targets -- -D warnings +cargo fmt --all --check + +cargo run -p loss-harness # FEC loss-resilience sweep (no network needed) +bash crates/lumen-core/tests/c/run.sh # standalone C-ABI link + round-trip proof +``` + +`include/lumen_core.h` is generated from `crates/lumen-core/src/abi.rs` by cbindgen +(`build.rs`) on every build and is **checked in**; CI fails if it drifts, so commit the +regenerated header when the ABI changes. + +## Layout + +``` +crates/lumen-core/ protocol · FEC · pacing · crypto — the C ABI (lib + cdylib + staticlib) +crates/lumen-host/ Linux host: vdisplay · capture · encode · inject · web · pipeline (cfg-gated) +crates/lumen-client-rs/ reference client (M4) +tools/{loss-harness,latency-probe}/ measurement (plan §10) +clients/{apple,android}/ native client scaffolds (import lumen_core.h) +include/lumen_core.h generated C header +``` + +## Design invariants — do not regress + +- **One core, linked everywhere.** Protocol/FEC/crypto/pacing live only in `lumen-core`, + behind a stable, versioned C ABI (`lumen_abi_version`, `LumenConfig.struct_size`). +- **No async on the hot path.** The per-frame pipeline uses native threads only; + `tokio`/`quinn` are gated behind the off-by-default `quic` feature (control plane only). +- **FEC is the wall-breaker.** GF(2⁸) (≤255 shards/block, Moonlight-compatible) and + GF(2¹⁶) Leopard-RS (≤65535 shards/block, SIMD) — the latter removes the ~1 Gbps ceiling. +- **Security hardening from the M1 review must stay intact:** the reassembler bounds every + attacker-controlled header field against negotiated limits *before allocating* + (`ReassemblerLimits` in `packet.rs`); AES-GCM uses per-direction nonce salts + seq-as-AAD + (`crypto.rs`); the ABI enforces `struct_size` and range-checks inputs. There are + regression tests for these — keep them green. + +## Next: M0 (the pipeline spike) on this VM + +**Start here on the NVIDIA Ubuntu VM:** [`docs/linux-setup.md`](docs/linux-setup.md), then +run `bash scripts/bootstrap-ubuntu.sh` (verifies NVIDIA/NVENC, installs the Rust/PipeWire/ +wlroots/FFmpeg-dev deps) and bring up headless Sway with `scripts/headless/`. + +Per plan §8/§12: drive a headless Sway/wlroots output → capture via PipeWire (ScreenCast +portal, `ashpd` 0.13 + `pipewire` 0.10) → encode with NVENC (`ffmpeg-next` 7.x, +`hevc_nvenc`) → write a playable H.265 file. Then wire that pipeline into a `lumen-core` +host `Session` (M2). The module seams exist in +`crates/lumen-host/src/{vdisplay,capture,encode,inject,pipeline}.rs`. **Budget for the +CPU-copy fallback first** — dmabuf→NVENC zero-copy import is unreliable across NVIDIA +driver versions (plan §9 risk); the setup doc covers it. + +## Conventions + +- Rust 2021, `rustfmt` + `clippy -D warnings` clean before commit. +- Match the surrounding code's comment density and naming. +- Commit messages end with the Co-Authored-By trailer (see `git log`). diff --git a/docs/linux-setup.md b/docs/linux-setup.md new file mode 100644 index 0000000..a18a79d --- /dev/null +++ b/docs/linux-setup.md @@ -0,0 +1,110 @@ +# Linux host setup — NVIDIA GPU VM (M0/M2) + +How to bring up the build environment for the lumen Linux host on an NVIDIA-GPU Ubuntu VM +and run the **M0** capture→encode spike. `lumen-core` already builds and is tested +cross-platform; this is about the platform backends in `crates/lumen-host`. + +> Target **Ubuntu 24.04 (noble)**: Sway 1.9, FFmpeg 6.1.1, xdg-desktop-portal 1.18. +> 22.04 (jammy) ships Sway 1.7 / FFmpeg 4.4 — too old for this path; build from source or +> upgrade. Package names/versions below were verified against the live Ubuntu archive. + +## 1. Bootstrap + +```sh +git clone git@git.unom.io:unom/lumen.git && cd lumen && git checkout m1-lumen-core +bash scripts/bootstrap-ubuntu.sh +``` + +It **verifies** the (already-installed) NVIDIA + NVENC stack, installs the Rust toolchain +(rustup) and the build/runtime deps (PipeWire, xdg-desktop-portal + the wlroots backend, +Sway, Wayland/DRM/EGL/GBM/VA dev libs, capture tools), **gates** the FFmpeg `-dev` +headers so it can't clobber your custom NVENC FFmpeg, and drops headless-Sway + portal +config templates into `~/.config` (only if absent). It does **not** reboot or edit GRUB. + +After it runs, sanity-check the core on Linux: + +```sh +cargo test --workspace # 21 tests; same suite that's green on macOS +``` + +## 2. NVIDIA prerequisites (one-time, may need a reboot) + +Wayland on NVIDIA requires KMS modeset. The bootstrap checks it; if it isn't `Y`: + +```sh +echo 'options nvidia-drm modeset=1 fbdev=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf +sudo update-initramfs -u && sudo reboot +cat /sys/module/nvidia_drm/parameters/modeset # must print Y after reboot +``` + +- Driver **≥ 535** is the floor for headless wlroots (EGL/dmabuf); 550+ recommended. +- A **headless VM GPU exposes no DRM connectors** — that's expected. We don't use the DRM + backend; `WLR_BACKENDS=headless` renders to an offscreen GBM/EGL surface and creates a + virtual `HEADLESS-1` output. Use the render node `/dev/dri/renderD128`. +- **NVENC in a VM:** full PCI **passthrough** = bare-metal NVENC, no license. **vGPU** + needs a valid license (vWS) or NVENC runs degraded — the bootstrap's smoke-encode tells + you if it actually works. Consumer GeForce cards also cap concurrent NVENC sessions + (~8); datacenter/RTX-pro are effectively unlimited — relevant once we serve many clients. + +## 3. Bring up the headless compositor + prove capture→NVENC + +```sh +# shell 1 — start headless Sway (prints WAYLAND_DISPLAY, default wayland-1) +bash scripts/headless/run-headless-sway.sh + +# shell 2 — same user +export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-1 +swaymsg -t get_outputs # confirm HEADLESS-1 +swaymsg output HEADLESS-1 resolution 2560x1440@60Hz # set your client size +bash scripts/headless/capture-smoke-test.sh # wf-recorder (wlr-screencopy) -> hevc_nvenc +ffprobe /tmp/lumen-headless-test.mkv # confirm a real H.265 stream +``` + +`wf-recorder` uses `wlr-screencopy` directly (no portal/D-Bus) — the fastest way to +de-risk the GPU encode path. **Note:** screencopy encodes straight to a file and *cannot* +feed PipeWire; the real integration uses the ScreenCast portal (see M0). + +The wlroots-on-NVIDIA env workarounds (`WLR_RENDERER=gles2`, `WLR_NO_HARDWARE_CURSORS=1`, +`GBM_BACKEND=nvidia-drm`, `sway --unsupported-gpu`, …) live in +`scripts/headless/env.sh` — `source` it before launching anything Wayland. + +## 4. M0 proper — wire it into `lumen-core` + +Goal (plan §8): headless output → PipeWire ScreenCast → NVENC → a playable file, then feed +the encoded access units into a `lumen_core::Session` (host role). The module seams exist +in `crates/lumen-host/src/{vdisplay,capture,encode,inject,pipeline}.rs`. + +Crate choices, verified current: +- **Capture (portal path):** [`ashpd`](https://docs.rs/ashpd) **0.13** with the + `screencast` + `pipewire` features → `ScreenCast::create_session` → `select_sources` + (`Monitor`) → `start` → `pipe_wire_node_id()` + `open_pipe_wire_remote()`; pull frames + with [`pipewire`](https://docs.rs/pipewire) **0.10**. (crates.io's "newest" field shows + 0.9 for ashpd — ignore it, pin `0.13`.) Set `XDG_CURRENT_DESKTOP=sway` so the wlr + portal backend is chosen. +- **Encode:** [`ffmpeg-next`](https://crates.io/crates/ffmpeg-next) **7.x** (binds the + system FFmpeg 6.1.1 via pkg-config; needs `clang`/`libclang`). Select the encoder by + name — `encoder::find_by_name("hevc_nvenc")`, *not* by codec id (that's the SW encoder). + Low-latency opts: `preset=p1`, `tune=ull`, `rc=cbr`, `bf=0`, `delay=0`, large `g`. + If your FFmpeg is in a non-standard prefix, `export FFMPEG_DIR=/that/prefix`. +- **Zero-copy is the hard part.** There's no direct dmabuf→CUDA import in FFmpeg. + **Start with the CPU-copy fallback** (download frame → `hwupload_cuda` → `hevc_nvenc`) + to get an end-to-end stream, then chase true dmabuf zero-copy. The plan flags this + (§9) and the `capture` module already has a `cpu_bytes` fallback field. +- **Input (M2):** [`reis`](https://crates.io/crates/reis) (pure-Rust libei — no native + `libei` needed) with `input-linux`/uinput as the universal fallback. + +Then continue toward **M2**: `serverinfo`/RTSP/pairing enough for a stock Moonlight client +to connect, a KWin virtual output created on connect, input via reis/uinput — the +shippable milestone. + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| Sway aborts on NVIDIA | add `--unsupported-gpu` (the helper scripts do) | +| `not a KMS device` / no connectors | expected on a headless VM GPU — use `WLR_BACKENDS=headless`, not the DRM backend | +| Sway won't start at all | `WLR_RENDERER_ALLOW_SOFTWARE=1 WLR_RENDERER=pixman` to prove the pipeline, then fix EGL | +| ScreenCast portal finds no output | ensure `xdg-desktop-portal-wlr` is running in the same session, `XDG_CURRENT_DESKTOP=sway`, and `~/.config/xdg-desktop-portal-wlr/config` has `output_name=HEADLESS-1` | +| `Cannot load libnvidia-encode.so.1` | NVENC runtime lib missing (driver) or unlicensed vGPU | +| `cargo build` can't find FFmpeg | `export FFMPEG_DIR=$(pkg-config --variable=prefix libavcodec)` or point `PKG_CONFIG_PATH` at the custom build | +| bindgen: libclang not found | `export LIBCLANG_PATH=$(llvm-config --libdir)` | diff --git a/scripts/bootstrap-ubuntu.sh b/scripts/bootstrap-ubuntu.sh new file mode 100755 index 0000000..1421809 --- /dev/null +++ b/scripts/bootstrap-ubuntu.sh @@ -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 diff --git a/scripts/headless/capture-smoke-test.sh b/scripts/headless/capture-smoke-test.sh new file mode 100755 index 0000000..32fe822 --- /dev/null +++ b/scripts/headless/capture-smoke-test.sh @@ -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" diff --git a/scripts/headless/env.sh b/scripts/headless/env.sh new file mode 100644 index 0000000..36f3306 --- /dev/null +++ b/scripts/headless/env.sh @@ -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 diff --git a/scripts/headless/portals.conf b/scripts/headless/portals.conf new file mode 100644 index 0000000..32192fa --- /dev/null +++ b/scripts/headless/portals.conf @@ -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 diff --git a/scripts/headless/run-headless-sway.sh b/scripts/headless/run-headless-sway.sh new file mode 100755 index 0000000..f0c9359 --- /dev/null +++ b/scripts/headless/run-headless-sway.sh @@ -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 diff --git a/scripts/headless/sway.config b/scripts/headless/sway.config new file mode 100644 index 0000000..753910b --- /dev/null +++ b/scripts/headless/sway.config @@ -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 diff --git a/scripts/headless/xdpw.config b/scripts/headless/xdpw.config new file mode 100644 index 0000000..6810f7d --- /dev/null +++ b/scripts/headless/xdpw.config @@ -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