---
name: game-streaming
description: Self-hosted game streaming with Sunshine + Moonlight — server setup, client options, VRAM cohabitation with LLM inference, and network planning.
version: 1.6.0
---

# Game Streaming (Sunshine + Moonlight)

Stream games from a gaming PC/server to any client device on the LAN. Low latency (3-8ms), hardware-accelerated encode via NVENC, open source.

## When to use

- User wants to play PC games on a TV, laptop, tablet, or phone
- User has a gaming-capable GPU with NVENC (NVIDIA Turing or newer: 1650 Super+, 20-series+, 30-series+, 40-series+)
- User wants to stream without cloud services (GeForce Now, Stadia, etc.)
- User needs to co-manage GPU VRAM between game streaming and LLM inference

## Architecture

```
Controller (Bluetooth/USB to client)
    → Client device (Moonlight)
        → LAN (Ethernet or WiFi 6)
            → Server (Sunshine + GPU)
                → NVENC hardware encode (dedicated silicon, negligible FPS hit)
                    → Video stream back to client
                        → Display
```

**Key principle:** All traffic is LAN. Internet speed is irrelevant — game streaming never leaves the local network.

## Quick setup

### Server (Sunshine)

```bash
# Ubuntu/Debian — download .deb from GitHub releases (PPA is deprecated)
# Check https://github.com/LizardByte/Sunshine/releases/latest for the current version
# Example for Ubuntu 26.04 amd64:
curl -LO "https://github.com/LizardByte/Sunshine/releases/download/v2026.516.143833/sunshine-ubuntu-26.04-amd64.deb"
sudo dpkg -i sunshine-ubuntu-26.04-amd64.deb
sudo apt install -f -y   # resolve dependencies (miniupnpc, libayatana-appindicator3, libnotify)

# Web UI at https://localhost:47990
# Auto-detects NVIDIA GPU and NVENC
```

From the web UI: add individual `.exe` files, Steam shortcuts, or stream the full desktop.

Enable Sunshine as a systemd service so it auto-starts:

```bash
# Service name is app-dev.lizardbyte.app.Sunshine (aliased as sunshine.service)
systemctl --user enable --now app-dev.lizardbyte.app.Sunshine
```

#### Restarting Sunshine

```bash
# Sunshine runs as a USER service (from .deb or Flatpak), NOT a system service.
# Do NOT use 'sudo systemctl restart sunshine' — it will fail with "Unit not found."
systemctl --user restart app-dev.lizardbyte.app.Sunshine
```

#### Starting from a non-graphical (tty/SSH) session

Sunshine's systemd user service is bound to `graphical-session.target` — it auto-starts only when XFCE/X11 is running. To start it manually from a tty when Xorg is already active:

```bash
systemctl --user set-environment DISPLAY=:0
systemctl --user start app-dev.lizardbyte.app.Sunshine
```

The Xauthority cookie must also be valid (see X auth merge in DPMS lockup pitfall).

After any restart, verify both Sunshine and the input bridge are alive:
```bash
ps aux | grep -E 'sunshine|input_bridge' | grep -v grep
# Expect: /usr/bin/sunshine + python3 input_bridge.py
```

If the input bridge predates the new Sunshine PID by hours/days, it's stale — kill and restart it (see input bridge pitfall).

### Client (Moonlight)

Install Moonlight on the client device from the native app store (Windows, macOS, Linux, Android, iOS, Android TV). The Shield TV is the best client — gigabit Ethernet, proper surround sound passthrough, native Moonlight app on Play Store.

Pairing: Moonlight discovers Sunshine automatically on LAN. Enter the 4-digit PIN shown in the Sunshine web UI. One-time pairing per client.

## Bandwidth requirements

| Resolution | Bitrate | % of gigabit LAN |
|---|---|---|
| 1080p 60fps | 20-30 Mbps | 2-3% |
| 1440p 60fps | 40-60 Mbps | 4-6% |
| 4K 60fps | 80-100 Mbps | 8-10% |

## Client recommendations

**NVIDIA Shield TV (best):** Gigabit Ethernet, Dolby/DTS passthrough, native Moonlight app, Bluetooth controller support. The gold standard for living room streaming.

**Other good options:** Apple TV 4K, Steam Deck, any x86 mini PC, Android tablets/phones.

**WiFi 6 clients:** WiFi 6 (802.11ax) handles 4K60 fine in practice (~600-800 Mbps real throughput). WiFi 5 (AC) is adequate for 1080p. Always prefer Ethernet on the client when possible — jitter, not bandwidth, is the enemy.

## VRAM cohabitation with LLM inference

This is the critical planning dimension. A modern AAA game needs 4-8 GB VRAM. If an LLM is resident on the same GPU, they compete.

| Card | VRAM | Q4_K_M 7B | Q4_K_M 14B | Remaining for games |
|---|---|---|---|---|
| RTX 2080 Ti | 11 GB | ~4.7 GB resident | ~9 GB resident | 6.3 GB / 2 GB |
| RTX 3070 | 8 GB | ~4.7 GB resident | Won't fit | 3.3 GB / N/A |
| RTX 3080 | 10 GB | ~4.7 GB resident | ~9 GB resident | 5.3 GB / ~1 GB |

**Solution: socket-activated llama-server.** See `llama-cpp` skill, `references/deployment-patterns.md` — the socket activation pattern. Stop the model while gaming, warm-restart in 3-4 seconds when needed.

## Input handling

Controllers connect to the **client**, not the server. Moonlight forwards input over the network. Works with Xbox, PlayStation, Switch Pro, 8BitDo. Keyboard+mouse via USB hub on the client device.

## References

- **[headless-server-setup.md](references/headless-server-setup.md)** — full headless pipeline: dummy plug, Xorg, Steam/Proton, Lutris, game compatibility table, VRAM cohabitation strategies.
- **[headless-troubleshooting.md](references/headless-troubleshooting.md)** — real-world debugging: CUDA/driver mismatch, group permissions, systemd DISPLAY override, CSRF, encoder failure diagnosis.
- **[troubleshooting-crashes.md](references/troubleshooting-crashes.md)** — Sunshine crash diagnosis: "Hang detected" watchdog kills, silent deaths, stale process cleanup, log preservation, step-by-step debug workflow.
- **[hp-omen-30l-8703-gpu-passthrough.md](references/hp-omen-30l-8703-gpu-passthrough.md)** — HP OMEN 30L (8703 motherboard) specifics: disabled iGPU, IOMMU group layout, single vs dual GPU passthrough options, VRAM cohabitation with LLM workloads.
- **[windows-vm-gpu-passthrough.md](references/windows-vm-gpu-passthrough.md)** — options for running a Windows VM alongside Linux for anti-cheat games: second GPU, single GPU passthrough, Proton-only, dual boot. OEM motherboard pitfalls (HP OMEN, iGPU disabling).
- **[setup-sunshine-audio-sinks.sh](scripts/setup-sunshine-audio-sinks.sh)** — executable script to pre-create PipeWire virtual sinks to prevent Sunshine crashing during RTSP handshake. Must be `chmod +x` after any skill update.
- **[setup-headless-steam.sh](scripts/setup-headless-steam.sh)** — executable script to bypass the Zenity license popup when installing Steam on a headless server.
- **[steam-bigpicture.sh](scripts/steam-bigpicture.sh)** — executable wrapper script that forces Steam Big Picture to fullscreen on headless X11 with a dummy plug. Use as the `detached` command in Sunshine's apps.json instead of raw `setsid steam steam://open/bigpicture`. Requires `wmctrl` and `xdotool`.
- **[pairing-bypass-inject-cert.py](references/pairing-bypass-inject-cert.py)** — emergency Python script to extract the most recent client certificate from sunshine.log, decode its hex-encoded PEM, inject it directly into sunshine_state.json as a pre-paired named_device, and optionally restart Sunshine. Use when the PIN pairing flow deadlocks (Sunshine v2026.516 bug) — run with `--restart` to restart Sunshine automatically after injection. After running, Moonlight must be fully closed and reopened on the client to skip the PIN screen.
- **[client-troubleshooting-prompt.md](references/client-troubleshooting-prompt.md)** — deployable prompt for a Hermes agent on the Moonlight client PC to diagnose Windows Firewall, Tailscale routing, and port connectivity issues that manifest as RTSP handshake timeouts (error 10060).
- **[diagnose-uinput.sh](scripts/diagnose-uinput.sh)** — executable diagnostic script that tests whether uinput is functional on this kernel using the exact libevdev API Sunshine uses for keyboard/mouse injection. Run BEFORE any other input troubleshooting — if this fails (returns ENOTTY / -25), no amount of Moonlight settings or WM changes will fix input. Workaround: Steam Remote Play.
- **[uinput-shim.c](scripts/uinput-shim.c)** — C source for LD_PRELOAD shim that fixes Sunshine keyboard/mouse input on kernel 7.0+. Overrides libevdev_uinput_create_from_device to insert the required UI_DEV_SETUP ioctl. Compile: `gcc -shared -fPIC -o uinput_fix.so uinput-shim.c -ldl`. Deploy to `~/.config/sunshine/uinput_fix.so` and add `Environment=LD_PRELOAD=/home/ray/.config/sunshine/uinput_fix.so` to the Sunshine systemd override.
- **[remote_play.vdf](templates/remote_play.vdf)** — Steam Remote Play config template. Copy to `~/.steam/debian-installation/config/` to enable host-side streaming when uinput is broken and Sunshine can't inject input. Client must have Steam installed and logged into the same account.
- **[sunshine-input-bridge.py](scripts/sunshine-input-bridge.py)** — Python bridge that tails Sunshine's log for keyboard/mouse packets and injects them via xdotool XTEST. Uses Windows Virtual Key code mapping (not USB HID) for correct keyboard injection. Reliable fallback when uinput is broken AND the LD_PRELOAD shim can't intercept Inputtino. Run alongside Sunshine: `nohup python3 ~/.hermes/skills/gaming/game-streaming/scripts/sunshine-input-bridge.py &`. Requires `xdotool`. Must be restarted after every Sunshine restart.
- **[kernel-7-uinput-regression.md](references/kernel-7-uinput-regression.md)** — Full diagnostic reference for the kernel 7.0 uinput breakage: C test code, Python/libevdev diagnostic, Inputtino vs libevdev clarification, and why the LD_PRELOAD shim may not reach Inputtino's internal code path.
- **[quick-browser-vnc.md](references/quick-browser-vnc.md)** — Lightweight browser-based remote desktop via x11vnc + websockify + noVNC. No client install, no pairing — just open a URL. Use when the user needs to view their desktop in a laptop browser without setting up Moonlight.
- **[debug-black-screen.md](references/debug-black-screen.md)** — Systematic diagnostic workflow for black screen in any capture tool (Sunshine, x11vnc, noVNC): hardware screenshot verification, DRM sysfs inspection, nvidia-settings cross-check. Distinguishes capture-layer bugs from kernel-level DPMS lockup requiring reboot.
- **[clean-reset-workflow.md](references/clean-reset-workflow.md)** — Remove all user modifications (overrides, shims, bridges, custom config) and return to stock package state. What to delete, what to keep, and what breaks on Ubuntu 26.04 without workarounds.: C test code, Python/libevdev diagnostic, Inputtino vs libevdev clarification, and why the LD_PRELOAD shim may not reach Inputtino's internal code path.

## Pitfalls

- **Sunshine requires a display.** Headless servers need a dummy HDMI plug (~$5-10) to trick the GPU into full-power mode. Without it, NVIDIA GPUs throttle and may refuse to render. See `references/headless-server-setup.md` for the full pipeline.
- **NvFBC capture fails on modern NVIDIA drivers (two causes).** NvFBC is Sunshine's default capture backend on NVIDIA but frequently fails. Two independent causes: (1) **CUDA version mismatch** — Sunshine v2026 bundles CUDA 13.1.1 which requires driver ≥590.48.01; older drivers (e.g., 580.x) cause NvFBC to fail silently. (2) **Kernel modesetting enabled** (`nvidia_drm modeset=1`, the default on driver 580+) — NvFBC refuses to create a capture session with error `Cannot create capture session: the display server is in modeset`. Check with `grep \"Screencasting with\" ~/.config/sunshine/sunshine.log` — if it says `NvFBC` and errors follow, NvFBC is the active backend. **Fix:** add `capture = x11` to `sunshine.conf` to use X11 SHM capture instead. NVENC encoding continues to work — only the capture path changes. The KMS capture backend (`capture = kms`) is also incompatible with this setup (fails with `Unable to find display or encoder during startup` even with correct `adapter_name` and `output_name`). See `references/headless-server-setup.md` for config examples.
- **Sunshine user must be in `video`, `render`, `input`, and `audio` groups.** Without these, `/dev/dri/card0` is permission-denied (no KMS capture), `/dev/uinput` can't create virtual keyboard/gamepad, and PipeWire can't enumerate ALSA hardware for audio capture. Fix: `sudo usermod -a -G video,render,input,audio $USER` + re-login or `systemctl --user daemon-reexec`. Also `sudo chown $USER:input /dev/uinput` for gamepad support. See `references/headless-troubleshooting.md` for the full group permission matrix.
- **`cap_sys_admin` may need `=ep` (not just `=p`).** Sunshine's startup explicitly drops elevated privileges (`drop_elevated_privileges succeeded in dropping capabilities` in the log). With only `cap_sys_admin=p` (permitted), the capability may be dropped before uinput device creation, causing silent input failure without errors. Run `sudo setcap cap_sys_admin=ep /usr/bin/sunshine` to make it both effective and permitted — this keeps it active through the drop. Verify with `getcap /usr/bin/sunshine` showing `=ep`. Revert with `sudo setcap -r /usr/bin/sunshine` if no effect. This is secondary to the kernel 7.0 uinput regression — if the uinput diagnostic returns -25, no capability tweak will help.
- **Systemd service needs DISPLAY override, audio sink pre-creation, and kernel 7.0 uinput shim for headless X11.** The sunshine systemd service does not inherit `DISPLAY=:0` from manual Xorg sessions. Create a drop-in at `~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/override.conf` with:
  ```
  [Service]
  Environment=DISPLAY=:0
  Environment=LD_PRELOAD=/home/ray/.config/sunshine/uinput_fix.so
  ExecStartPre=/path/to/setup-sunshine-audio-sinks.sh
  LimitNICE=-10
  ```
  Then run `systemctl --user daemon-reload`. The LD_PRELOAD line is needed on kernel 7.0+ to fix uinput (see uinput diagnostic pitfall). Without the DISPLAY override, X11 capture fails. The ExecStartPre runs the audio sink script before Sunshine starts so PipeWire sinks are ready on every reboot.
- **Run sunshine directly to debug encoder failures.** When systemd logs just say "nvenc failed" with no detail, run `DISPLAY=:0 /usr/bin/sunshine ~/.config/sunshine/sunshine.conf` from the terminal. The verbose output shows exactly which CUDA/NVENC calls fail and why.
- **Sunshine v2026.611+ KMS capture works on NVIDIA headless X11 (in theory).** Sunshine v2026.611's `capture = auto` and `capture = x11` fail on headless X11 with `Error: Unable to initialize capture method` / `Platform failed to initialize`. However, **`capture = kms` MAY work** — it uses Kernel Mode Setting (DRM/KMS) to capture the display directly from the GPU framebuffer. **In practice, KMS capture can fail with `Failed to initialize video capture/encoding. Is a display connected and turned on?` even when the HDMI dummy plug is present and visible in `/sys/class/drm/`.** The NVIDIA proprietary driver does not always expose the display through DRM/KMS the way Sunshine v2026.611+KMS expects. v2026.611 requires `libqt6widgets6` and `libminiupnpc21` — force-install with `sudo dpkg -i --force-depends` then `sudo ln -sf /usr/lib/x86_64-linux-gnu/libminiupnpc.so.21 /usr/lib/x86_64-linux-gnu/libminiupnpc.so.17` if the package demands the older soname. Also requires `libicuuc.so.70` symlink if rolling back to v2026.516 later. If KMS capture won't initialize, fall back to v2026.516.143833 with X11 capture — it remains the last release with native X11 capture support (though input injection may remain broken; see input injection pitfall).
- **ICU library mismatch when reinstalling old Sunshine.** Ubuntu 26.04 ships `libicu78` but Sunshine v2026.516 was compiled against `libicu70`. Reinstalling the package breaks with `symbol lookup error: undefined symbol: UCNV_TO_U_CALLBACK_SKIP_70` or `libicuuc.so.70 => not found` in `ldd`. Fix: download `libicu70_70.1-2_amd64.deb` from Ubuntu 22.04 archive (`http://archive.ubuntu.com/ubuntu/pool/main/i/icu/`) and install it alongside the current ICU version. Then `sudo dpkg --force-depends --configure sunshine`. **If ICU version is close (e.g., system has libicuuc.so.78):** symlinking won't work — ICU 78 doesn't export `_70`-suffixed symbols. You MUST install the actual libicu70 package.

- **Missing Qt6 libs after upgrading to v2026.611+.** The v2026.611 package added `libqt6widgets6` and `libminiupnpc17` dependencies. Ubuntu 26.04 has `libqt6widgets6` available (`apt install libqt6widgets6`) but `libminiupnpc17` was replaced by `libminiupnpc21`. Force-install with `sudo dpkg -i --force-depends`, then `sudo ln -sf /usr/lib/x86_64-linux-gnu/libminiupnpc.so.21 /usr/lib/x86_64-linux-gnu/libminiupnpc.so.17` to satisfy the soname. **However, do NOT use v2026.611+ on headless X11** — it drops X11 capture support entirely (see KMS capture pitfall).
- **CSRF protection blocks non-localhost access.** Sunshine v2026 added CSRF protection. Add `csrf_allowed_origins = https://<tailscale-ip>:47990` and `origin_pin_allowed = pc` to sunshine.conf to allow remote web UI access **and PIN pairing** from non-LAN addresses. **Sunshine v2026.516 rejects plain HTTP origins** — `http://192.168.50.98:47990` and `http://100.93.253.36:47990` are ignored with a warning in the log (`Invalid 'csrf_allowed_origins' entry rejected`). Only `https://` origins are accepted. If `origin_pin_allowed` is set to `lan`, Moonlight clients connecting via Tailscale will get "Request Timed out" during PIN pairing because Sunshine rejects the non-LAN source address. The `pc` value matches Moonlight's default behavior and allows pairing from any origin. Note: newer Sunshine versions may log "Unrecognized configurable option [origin_pin_allowed]" — this is cosmetic; the setting still works.
- **Audio requires PipeWire on headless servers.** Headless servers have no audio system by default. Install `pipewire pipewire-pulse wireplumber pulseaudio-utils` and add the user to the `audio` group. **DO NOT configure `audio_sink = auto_null` or `virtual_sink = auto_null` in `sunshine.conf`** on Sunshine v2026+ — doing so causes a fatal crash (`pa_simple_new() failed: Invalid argument`) the moment a client tries to connect. Let Sunshine auto-detect PipeWire instead. 
  **If it still crashes with 'Invalid argument' even without config**, it means Sunshine's internal attempt to dynamically create its expected sinks (`sink-sunshine-stereo`, etc.) is being rejected by your PipeWire daemon. **Fix:** Manually pre-create the expected sinks using `pactl` before a client connects: `pactl load-module module-null-sink sink_name=sink-sunshine-stereo` (plus `sink-sunshine-surround51` and `71` with correct channel_maps if needed). Because they already exist, Sunshine finds them and bypasses the crashy creation step. See `references/headless-server-setup.md` for the full audio pipeline.
- **NVENC session limit.** Consumer NVIDIA GPUs are limited to 3 concurrent NVENC sessions. Not a problem for single-stream gaming.
- **LightDM greeter captured by Sunshine — Moonlight shows login screen.** When Sunshine starts and no user is logged into the desktop on the console, X11 is running but only the LightDM greeter (login prompt) is visible. Sunshine captures this greeter — Moonlight users see the login screen asking for a password for the user shown (e.g., "Ray"). They cannot log in from Moonlight because Sunshine forwards input through uinput/Inputtino, not XTEST. **Fix:** Enable LightDM autologin so the desktop session starts automatically at boot: edit `/etc/lightdm/lightdm.conf`, uncomment `autologin-user=<username>` and `autologin-user-timeout=0`, then `sudo systemctl restart lightdm`. Verify with `loginctl list-sessions` — a user session (not `lightdm` greeter) must be on `seat0`. Restart Sunshine after the desktop is logged in.\n\n- **Headless servers need a desktop session for X11 capture.** Sunshine's X11 capture requires a running window manager or desktop environment on the display — an X server alone (login screen, no logged-in user) isn't enough. "Unable to initialize capture method" / "Platform failed to initialize" with Xorg running means no WM is active. **Openbox alone is insufficient and causes multiple problems:** (1) it provides a black screen — Moonlight users see nothing but black with a tiny taskbar edge because Xfce's desktop background isn't rendered, (2) Moonlight mouse input will not work (cursor appears frozen, Sunshine receives deltaX/deltaY but Inputtino can't inject them through uinput/uhid). **Fix:** Install a full XFCE desktop: `sudo apt install -y xfce4 xfce4-goodies`, start with `DISPLAY=:0 xfce4-session`, then replace Openbox with XFWM4: `DISPLAY=:0 xfwm4 --replace &`. XFCE is lightweight (~500MB RAM) and XFWM4 handles input forwarding correctly. **If the screen is still black even with XFCE**, the desktop background may be unset — run `DISPLAY=:0 xsetroot -solid "#204060"` to set a visible blue background. **Steam Big Picture:** Steam must be installed (`steam-installer` package) and fully set up (first-run download + login) for the Steam Big Picture Sunshine app to work. The Moonlight app list shows "Steam Big Picture" as a separate launch option from "Desktop." **Headless Steam setup trap:** The `steam` command may hang silently on first run due to a Zenity prompt for the proprietary license. You cannot click it. Fix: download the `steam_X.X.X.tar.gz` beta bootstrap directly from `repo.steampowered.com/steam/archive/beta/` and extract it to `~/.steam/debian-installation` to bypass. Then launch Steam — it will self-update through the normal update flow without needing a GUI click.
- **Running Sunshine as root forces NvFBC capture (no input).** When Sunshine is started as root (via `systemd-run`, system-level service, or `sudo`), it auto-selects NvFBC (NVIDIA Frame Buffer Capture) because root has GPU-level access. NvFBC captures the framebuffer directly from GPU memory — it has zero interaction with X11's input subsystem. Moonlight keyboard and mouse events arrive but can never be injected into the X session. Logs show video/audio streaming with zero input events (no XTEST, no keyboard, no mouse entries). Always run Sunshine as the same user who owns the X session, using `systemctl --user`. If you accidentally ran as root, stop and restart with the user service.
- **Remove explicit `adapter_name` for NVENC.** Setting `adapter_name = /dev/dri/renderD128` can prevent NVENC detection even when the render node belongs to the NVIDIA GPU. NVENC doesn't use DRI render nodes — it talks to the GPU through CUDA/NVENC libraries directly. Comment it out or set to empty for auto-detection: `# adapter_name = auto (let Sunshine detect GPU)`.
- **4K desktop makes icons tiny on Moonlight.** Headless servers with a 4K dummy plug or 4K monitor render at native resolution (2160p). On Moonlight, UI elements and icons become microscopic because the stream inherits the host's pixel density. Fix: set the desktop to 1080p or 1440p before starting Sunshine (whichever matches the streaming target). Use `DISPLAY=:0 xrandr --output HDMI-0 --mode 1920x1080` then `echo "Xft.dpi: 96" | DISPLAY=:0 xrdb -merge` to lock DPI. Verify with `DISPLAY=:0 xdpyinfo | grep "dimensions\\|resolution"`. Add a 1440p resolution mode via the Sunshine Web UI Apps page as a prep command so the user can switch without manual CLI intervention.
  **CAVEAT: Cheap dummy plugs may not support any xrandr mode change (see pitfall #4 under "Steam Big Picture app shows black screen").** If `xrandr --output HDMI-0 --mode 1920x1080` returns `BadMatch`, the plug is locked to its native 4K@60 and cannot be switched. In that case, either keep the desktop at 4K (accept the tiny icons) or replace the dummy plug with one that supports actual mode switching.
- **HDMI dummy plug DPMS lockup — connector "connected" but "disabled" at kernel level.** After extended uptime or display mode changes, the NVIDIA driver may put the HDMI dummy into a deep DPMS sleep where the DRM connector shows `status=connected` but `enabled=disabled` and `dpms=Off`. The `enabled` sysfs file is read-only on NVIDIA, and writing to `dpms` returns Permission denied even via sudo. **This can survive physical replug AND Xorg restart (lightdm stop/start).** The GPU display controller needs a full power cycle — a system reboot is the ONLY reliable fix. After reboot, the display typically comes back but X11 authorization is broken. All encoders fail because Sunshine can't find an enabled display. Symptoms: `Fatal: Unable to find display or encoder during startup` even with `encoder = nvenc` and NVENC confirmed working via `ffmpeg -encoders | grep nvenc`. `encoder = software` also fails, proving this is a DISPLAY problem, not an encoder problem. Diagnostic: `for c in /sys/class/drm/card1-*/; do echo "$(basename $c): status=$(cat $c/status) enabled=$(cat $c/enabled) dpms=$(cat $c/dpms)"; done` — HDMI shows `connected`/`disabled`/`Off`. Fix: (1) Reboot the system. (2) **After reboot, X11 authorization is broken** — LightDM's Xauthority cookie (`/var/run/lightdm/root/:0`) is only valid for root, and `ray` has no `/home/ray/.Xauthority`. `DISPLAY=:0 xrandr` returns "Authorization required." Sunshine cannot reach the display. **Fix:** (Option A — preferred, keeps access control intact) Merge the LightDM Xauthority cookie into ray's `.Xauthority` so Sunshine inherits it:
```bash
sudo xauth -f /var/run/lightdm/root/:0 extract - :0 | xauth -f ~/.Xauthority merge -
```
(Option B — global, disables access control) 
```bash
sudo bash -c 'export DISPLAY=:0 XAUTHORITY=/var/run/lightdm/root/:0; xhost +'
```
Option A is preferred because it doesn't open X11 to all local processes. Either approach works — verify with `DISPLAY=:0 xrandr | head -3` showing HDMI-0 connected. (3) Set display to 1080p and lock DPMS: `DISPLAY=:0 xrandr --output HDMI-0 --mode 1920x1080; DISPLAY=:0 xset -dpms; DISPLAY=:0 xset s off`. (4) Add `hw_cursor = disabled` to sunshine.conf for visible cursor in stream. (5) Restart Sunshine — it should now find the display and encoder.
- **Hardware cursor invisible in Moonlight stream.** X11 hardware cursor overlay is NOT captured by Sunshine's X11 screen capture (`capture = x11`). The cursor moves on the server (verify with `xdotool getmouselocation`) but Moonlight viewers see no cursor. Fix: add `hw_cursor = disabled` to `sunshine.conf` — this switches X11 to software cursor rendering where the cursor is drawn into the framebuffer and captured by the video encoder. Restart Sunshine and the cursor becomes visible. Verify by moving the cursor via xdotool and confirming the client can see it move.
- **ExecStartPre script must be executable.** The systemd drop-in at `override.conf` references `setup-sunshine-audio-sinks.sh`. If this script is not executable (`chmod +x`), systemd fails to start Sunshine with a cryptic "control process exited with error code." The script is created with mode 600 by default (hermes write_file). Fix: `chmod +x ~/.hermes/skills/gaming/game-streaming/scripts/setup-sunshine-audio-sinks.sh` after any skill update.
- **Sunshine config can be silently dropped during version upgrades.** Installing a new Sunshine `.deb` package does not overwrite `~/.config/sunshine/sunshine.conf` directly, but `dpkg --force-depends` operations and `apt --fix-broken install` cycles can trigger post-removal scripts that clean the config directory. After any package install/uninstall/reinstall cycle, verify the config exists: `cat ~/.config/sunshine/sunshine.conf`. If missing or truncated, restore from memory: `capture = x11`, `encoder = nvenc`, `output_name = N`, `sunshine_name = <name>`, `csrf_allowed_origins = <origins>`, `origin_pin_allowed = pc`. Also re-inject the client certificate into `sunshine_state.json` so paired clients aren't lost.
- **`origin_pin_allowed` is not recognized in Sunshine v2026.611+ (cosmetic).** Newer Sunshine releases log `Warning: Unrecognized configurable option [origin_pin_allowed]` but the setting still functions — it disables the IP-matching anti-spoofing check during PIN pairing, allowing a browser on any device to submit the PIN regardless of source IP. The warning is harmless. However, v2026.611+ drops X11 headless capture support entirely (`Error: Unable to initialize capture method` / `Platform failed to initialize`). Stay on v2026.516.143833 for headless X11.\n- **Unrecognized config options in v2026.516 (cosmetic): `input`, `gamepad`, `key_repeat_delay`, `key_repeat_rate`.** Sunshine v2026.516.143833 logs `Warning: Unrecognized configurable option [input]` (and similarly for `gamepad`, `key_repeat_delay`, `key_repeat_rate`). These options may have existed in older versions or documentation but are not recognized in this release. Remove them from `sunshine.conf` to keep the config clean. They have no effect and the warnings are harmless but noisy.\n- **`uinput` may be built into the kernel, not a module.** If `modprobe uinput` succeeds but `lsmod | grep uinput` shows nothing, uinput is built-in (check with `modinfo uinput` — `filename: (builtin)` or `grep CONFIG_INPUT_UINPUT /boot/config-$(uname -r)` showing `=y`). The device node at `/dev/uinput` still exists and permissions (user in `input` group) are what matter. Built-in uinput cannot be loaded/unloaded — skip module troubleshooting.\n- **Capture method visible in logs.** Grep for `Screencasting with` in `sunshine.log` to see which capture method Sunshine selected at startup. `Screencasting with X11` means input injection through XTEST/Inputtino is possible. `Screencasting with NvFBC` means Sunshine is capturing the GPU framebuffer directly and input will never work — this happens when running as root. Verify after any config or user change.
- **Audio over HDMI.** The Shield TV is the only client that does proper surround sound passthrough (Dolby Digital, DTS). Other clients may downmix to stereo.
- **WiFi interference.** 2.4 GHz WiFi is unusable for game streaming — too much latency and jitter. Use 5 GHz or Ethernet.
- **OEM boards may disable iGPU when dGPU installed.** On HP OMEN prebuilts and similar OEM desktops, the integrated GPU is often disabled at the firmware level when a discrete card is present — no video outputs routed, not visible in lspci. This blocks the "iGPU for Linux host + dGPU for Windows VM" passthrough strategy. Check with `lspci | grep -i intel | grep -iE "vga|graphics"` before committing to this architecture. See `references/windows-vm-gpu-passthrough.md` for the full option matrix.
- **Sunshine prep-cmd executes commands like an init system, not a shell.** The `"do"` and `"undo"` strings in `apps.json` prep-cmd arrays are passed to `execvp()` directly, NOT interpreted by a shell (`/bin/sh`, `/bin/bash`). This means:
  - **`export DISPLAY=:0` fails** with `Error: Unable to find executable [export]. Is it in your PATH?` — `export` is a shell builtin, not a binary.
  - **Pipes (`|`), redirects (`>`/`<`), chaining (`&&`/`||`), and variable expansion** all fail with the same error because they require shell parsing.
  - **Fix:** Wrap everything in `sh -c`:
    ```json
    "do": "sh -c 'DISPLAY=:0 xset -dpms s off && DISPLAY=:0 xset dpms force on'"
    ```
  - **Simple single-command calls work fine** (e.g., `"do": "xrandr --output HDMI-0 --mode 1920x1080"`) because the binary name is the first word and `execvp` finds it in PATH.
  - **Multi-word params in sh -c:** Use the `-c` argument style: `sh -c 'command args...'` as a single string (as shown above). The `-c` flag followed by a single shell-quoted string is the correct syntax for embedding shell scripts in execvp-friendly strings.
  - **Check for this in logs:** `grep "Unable to find executable" ~/.config/sunshine/sunshine.log` will reveal any failed prep-cmd with shell syntax exposed to execvp.

- **Steam Big Picture app shows black screen (no fatal error) on Moonlight.** This is the most common symptom when streaming to a dummy plug. Three root causes that compound:

  **1. Sunshine captures the wrong display (no `output_name` set).** Without `output_name` pointing to the connected display in `sunshine.conf`, Sunshine defaults to display index 0 (typically DP-0, which is disconnected). The log shows the telltale sequence:
  ```
  Configuring selected display (0) to stream
  Couldn't get requested display info, defaulting to recording entire virtual desktop
  ```
  Sunshine falls back to recording the virtual desktop, which produces black or garbled frames because no connected display is configured at the capture layer. **Fix:** add `output_name = 2` to sunshine.conf (a numeric display index, NOT the connector name). Determine the correct index by cross-referencing `DISPLAY=:0 xrandr` with Sunshine's log output:

```bash
grep "Detected display" ~/.config/sunshine/sunshine.log
# Detected display: DP-0 (id: 0)DP-0 connected: false
# Detected display: HDMI-0 (id: 2)HDMI-0 connected: true  ← set output_name = 2
```

The `(id: N)` value is the index. On X11, `output_name` accepts a numeric display index, NOT a connector name like "HDMI-0" — passing the string causes Sunshine to fail with `Could not stream display number, there are only [8] displays.` After setting the correct index, verify in the log: `Streaming display: HDMI-0 with res <res> offset by 0x0` confirms Sunshine is capturing the right display.

  **2. Empty `prep-cmd` `"do"` command or no display resolution set.** If the Steam Big Picture app has `"do": ""`, Sunshine does nothing to prepare the display before Steam launches. Steam Big Picture then renders at whatever resolution the dummy plug defaults to (often 4K@60Hz), which strains the GPU and may produce rendering artifacts or black frames in the capture. **Fix:** set a proper xrandr prep-cmd, e.g.:
  ```json
  "prep-cmd": [
    {
      "do": "xrandr --output HDMI-0 --mode 2560x1440 --rate 120",
      "undo": "xrandr --output HDMI-0 --mode 3840x2160 --rate 60"
    }
  ]
  ```
  If the app is `detached`, the prep-cmd still runs before the app spawns — use it to set the dummy plug to a game-friendly resolution.

  **3. Steam app is `detached` — Sunshine falls back to Desktop immediately.** When the app is `detached`, Sunshine spawns Steam then immediately logs `Executing [Desktop]` and captures the desktop. If Steam Big Picture is rendering in fullscreen on the same display, the X11 capture SHOULD still see it — but if causes #1 and #2 are also present, the cascade guarantees a black screen. Without `output_name`, Sunshine is capturing the virtual desktop through a disconnected display interface. Without a prep-cmd, the display is at an unoptimized resolution. Together, these produce no valid frames.

  **4. Dummy plug rejects all resolution changes (BadMatch).** Many cheap HDMI dummy plugs (common "FUEARAN 4K/120" type and similar unbranded adapters) advertise a wide range of modes through their EDID — 4K@60, 1440p@120, 1080p@120, etc. — but the hardware only supports the single native resolution it powers up with (typically 3840x2160@60). Any `xrandr --output HDMI-0 --mode <res>` attempt fails with:
  ```
  xrandr: Configure crtc 0 failed
  X Error of failed request:  BadMatch (invalid parameter attributes)
  ```
  This is NOT a config problem — the dummy plug's hardware cannot reconfigure its CRTC/PHY. The EDID lies. **Fix:** set the prep-cmd `"do"` to an empty string (`""`) and let the dummy plug stay at its fixed native resolution. Do not attempt xrandr mode switching. If you need a different display resolution for game streaming, buy a higher-quality dummy plug that actually supports mode switching (e.g., a "Headless Ghost" or "Dr. HDMI" series), or use a custom modeline generated with `cvt` and added via `xrandr --newmode` + `xrandr --addmode`.

  **How to diagnose a locked dummy plug:**
  ```bash
  # 1. Check what xrandr says is available vs what actually works
  DISPLAY=:0 xrandr | grep -A20 "^HDMI-0"

  # 2. Try switching — any failure means the plug is locked
  DISPLAY=:0 xrandr --output HDMI-0 --mode 1920x1080 && echo OK || echo "LOCKED"

  # 3. If the dummy plug is locked, verify Sunshine captures it correctly
  grep "Streaming display:" ~/.config/sunshine/sunshine.log
  # Expect: HDMI-0 with res 3840x2160 ... — captures at native resolution
  ```

  **If locked, update both Steam Big Picture and any other app's prep-cmd:**
  ```json
  "prep-cmd": [
    {
      "do": "",
      "undo": "setsid steam steam://close/bigpicture"
    }
  ]
  ```
  The `output_name` fix alone (point 1 above) resolves the black screen. xrandr mode switching is optional and only works on quality dummy plugs.

  **5. Steam Big Picture renders as a 1280x800 window, not fullscreen, on headless X11.** Even with `output_name` set correctly and the dummy plug at its native 4K@60, Steam Big Picture frequently launches as a small windowed client rather than fullscreen on headless setups:

    ```
    "Steam Big Picture Mode": ("steamwebhelper" "steam")  1280x800+1280+693  +1280+693
    ```

    This window sits in the center of the 3840x2160 display. Sunshine captures the entire root window, so Moonlight shows mostly desktop background (likely black/dark) with a tiny Steam window the user can't interact with. The user sees a "black screen" even though the stream is technically working.

    **Root cause:** On a headless X11 server with no physical monitor, Steam's Big Picture mode defaults to its software-rendered CEF window at 1280x800 instead of going fullscreen. The XFWM4 window manager is running but Steam's fullscreen request is either not sent or not honored in the headless environment. Steam itself confirms this with `--disable-gpu --disable-gpu-compositing` flags in its process arguments, meaning Big Picture is rendering via CPU, not GPU.

    **Fix: wrapper script that fullscreens the window after launch.** Instead of calling `setsid steam steam://open/bigpicture` directly from the `detached` array, use a script that:

     1. Kills any stale Steam processes
     2. Launches Steam Big Picture
     3. Polls `xdotool` for the "Steam Big Picture Mode" window (up to 15s)
     4. Fullscreens it with `wmctrl -ir <id> -b add,fullscreen` and `xdotool key F11`

    See `scripts/steam-bigpicture.sh` for the production-ready script. Deploy it to `~/.config/sunshine/scripts/steam-bigpicture.sh` and reference it in `apps.json`:

    ```json
    {
      "name": "Steam Big Picture",
      "detached": ["/home/ray/.config/sunshine/scripts/steam-bigpicture.sh"],
      "prep-cmd": [{"do": "", "undo": "setsid steam steam://close/bigpicture"}],
      "image-path": "steam.png"
    }
    ```

    **To detect a windowed Steam Big Picture on a running system:**
    ```bash
    DISPLAY=:0 xwininfo -root -tree | grep -i "Steam Big Picture"
    # If output shows a size smaller than the display resolution (e.g. 1280x800 on a 3840x2160 screen),
    # the window is not fullscreen and the wrapper script is needed.
    ```

  **6. DPMS blanks the dummy plug after ~15 minutes of inactivity — display powers off, X11 capture reads black.** The HDMI dummy plug behaves like a real monitor: after 15 minutes of inactivity, the X server's DPMS (Display Power Management Signaling) puts it into power-save Off state. Sunshine's X11 capture reads the GPU framebuffer which has been blanked to solid black. Moonlight shows a pitch-black screen even though Sunshine reports `Streaming display: HDMI-0 with res 3840x2160` and all encoders are working normally.

    **Diagnose:**
    ```bash
    export DISPLAY=:0; xset q 2>&1 | grep -i "monitor\|dpms"
    # If output says "Monitor is Off" — DPMS blanked the display
    ```

    **Fix in two places — both are needed:**

    **(a) prep-cmd in apps.json** — runs before every stream launch, guaranteeing the display is on:
    ```json
    "prep-cmd": [
      {
        "do": "sh -c 'DISPLAY=:0 xset -dpms s off; DISPLAY=:0 xset dpms force on'",
        "undo": "setsid steam steam://close/bigpicture"
      }
    ]
    ```
    This disables DPMS entirely (`xset -dpms`), disables the screen saver (`s off`), and forces the monitor back on (`xset dpms force on`). **The `sh -c '...'` wrapper is mandatory** — Sunshine executes prep-cmd `"do"` strings via `execvp()` directly, NOT through a shell. `export`, pipes (`|`), redirects (`>`), and `&&`/`||` are all shell features that fail. If you see `Error: Unable to find executable [export]. Is it in your PATH?` in the log, you hit this — fix by wrapping in `sh -c '...'`.

    **(b) Wrapper script** — adds a safety net in case the prep-cmd's environment doesn't reach X11:
    ```bash
    xset -dpms s off 2>/dev/null
    xset dpms force on 2>/dev/null
    ```
    Placed at the very top of the script, before launching Steam, so the display is awake before any window appears. See `scripts/steam-bigpicture.sh`.

    **Important:** The `xset -dpms` only lasts for the current X session — it does not persist across Xorg restarts (i.e., not across reboots). After every system reboot or `systemctl restart lightdm`, the DPMS settings are reset to defaults. The prep-cmd handles this automatically because it runs `xset` fresh on every stream launch. No additional persistence mechanism is needed.

  **Diagnosis checklist (in order):**
    ```bash
    # 0. Is the display blanked by DPMS?
    export DISPLAY=:0; xset q 2>&1 | grep -i "monitor"
    # Expect: "Monitor is On"

    # 1. Is output_name set?
    grep output_name ~/.config/sunshine/sunshine.conf

    # 2. Does Steam Big Picture have a prep-cmd?
    python3 -c "import json; a=json.load(open('$HOME/.config/sunshine/apps.json')); [print(a['apps'][i]['name'], a['apps'][i].get('prep-cmd',[])) for i in range(len(a['apps'])) if 'Big Picture' in a['apps'][i]['name']]"

    # 3. Check sunshine log for the fallback pattern
    grep -E "Configuring selected display|Couldn't get requested display info|Executing \\\\[Desktop\\\\]" ~/.config/sunshine/sunshine.log

    # 4. Check if Steam Big Picture is actually fullscreen
    DISPLAY=:0 xwininfo -root -tree | grep -i "Steam Big Picture"
    # If size is ~1280x800, window is not fullscreen — deploy the wrapper script

    # 5. Check if the dummy plug is locked (rejects all xrandr mode changes)
    DISPLAY=:0 xrandr --output HDMI-0 --mode 1920x1080 2>&1 | grep -q "BadMatch" && echo "LOCKED - cannot switch resolution"
    ```

  **Quick-fix template** that addresses all causes at once — add to `sunshine.conf`:
  ```bash
  output_name = 2    # numeric display index, NOT connector name; see pitfall for how to determine
  ```
  And update the Steam Big Picture app entry in `apps.json` to include a non-empty prep-cmd as shown above. Restart Sunshine: `systemctl --user restart app-dev.lizardbyte.app.Sunshine`. After restart, verify with:
  ```bash
  grep "Streaming display:" ~/.config/sunshine/sunshine.log
  # Expect: Streaming display: HDMI-0 with res ... offset by 0x0
  ```

- **Steam Big Picture app fails with "Failed to initialize video capture/encoding".** Moonlight client shows this error when launching Steam Big Picture. Three causes: (1) Ollama/LLM co-resident on GPU — `sudo systemctl stop ollama`, free VRAM should be &ge;8GB; (2) **Empty `prep-cmd` `"do"` command in apps.json** — if the Steam Big Picture entry has `"do": ""`, Sunshine tries to execute an empty command which silently fails and aborts the stream setup. Fix: remove the prep-cmd block entirely (the `detached` array alone is sufficient to launch Steam). (3) Steam not fully installed — first-run bootstrap hasn't completed. See headless Steam setup pitfall below.

- **"Hang detected" crash on session end.** When a Moonlight client connects or disconnects, Sunshine tears down the encoder session. If NVENC/X11 hangs during teardown (CUDA context stuck, display server unresponsive, or **no window manager running on DISPLAY=:0**), the internal watchdog kills the process after 10 seconds. Log shows: `Fatal: Hang detected! Session failed to terminate in 10 seconds.` followed by immediate death. This crash is **reproducible every time** a client connects until the root cause is fixed. **First diagnostic:** check for a running WM with `ps aux | grep -iE "openbox|xfce|gnome|xfwm"` — X11 alone (login screen, headless with no WM) causes this crash reproducibly on every connection attempt. Start the WM (`DISPLAY=:0 openbox --replace &`), then restart Sunshine. **Second diagnostic — VRAM exhaustion:** a co-resident LLM model (Ollama) can cause the same hang by starving NVENC of GPU memory. Check with `nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader`. If free VRAM is under ~1GB, stop the model: `sudo systemctl stop ollama`. Free VRAM should be ≥8GB for stable NVENC sessions. **Third diagnostic:** Steam not installed/stuck — if the "Steam Big Picture" Sunshine app tries to launch `setsid steam steam://open/bigpicture` and Steam has never completed first-run setup (blocked by the zenity license dialog on headless), the command hangs and the session teardown hangs with it. See `references/headless-server-setup.md` for headless Steam installation. If all three checks pass and this recurs, suspect GPU driver instability. Full workflow in `references/troubleshooting-crashes.md`.
- **Stale wrapper holds port 48010 after crash.** After a crash-restart cycle, the old Flatpak wrapper process may linger holding port 48010, causing `Fatal: Couldn't bind RTSP server to port [48010], Address already in use`. Moonlight uses RTSP (port 48010) for streaming negotiation — without it, clients get "RTSP handshake failed" (error 10060/timeout). Kill the stale PID then **restart Sunshine** so it rebinds all ports cleanly: `ps aux | grep sunshine | grep -v grep`, `kill <stale-pid>`, `systemctl --user restart sunshine`, verify with `ss -tlnp | grep -E "47984|47989|47990|48010"`.
- **RTSP connection reset (error 10054) = unpaired client.** When Moonlight gets error 10054 (connection reset) and Sunshine logs `Debug: No pending session for incoming RTSP connection`, the client was never paired or its pairing was lost. Check `~/.config/sunshine/sunshine_state.json` for `paired_clients` — if count is 0, pair via Moonlight + Sunshine Web UI PIN (`https://<ip>:47990/pin`). This is distinct from error 10060 (timeout = port not listening). With `origin_pin_allowed = pc`, all non-localhost clients must enter a PIN; there is no auto-pairing. Full diagnostic in `references/troubleshooting-crashes.md`.
- **"Success" in Web UI but pairing fails.** If Moonlight asks for a PIN, you enter it in the Sunshine Web UI, the UI says "Success!", but Moonlight still sits waiting or says "PC Offline" and `sunshine_state.json` shows 0 paired clients. Check these root causes in order:

  **CRITICAL: Do not retry the PIN flow more than twice.** Sunshine v2026.516.143833 has a known deadlock in its PIN validation. If the first two attempts fail, **stop trying and go straight to cert injection** (see pairing-bypass pitfall below). Repeated retries just frustrate the user without making progress.

  **Diagnostic: verify the PIN reached Sunshine.** After entering the PIN, grep the log: `grep "api/pin\|not authorized" ~/.config/sunshine/sunshine.log | tail -5`. If you see `POST /api/pin` with `Authorization -- CREDENTIALS REDACTED`, the browser was logged in and the PIN was submitted. If you see `Info: Web UI: [<ip>] -- not authorized` near the same timestamp, the browser was NOT logged in — the PIN was silently dropped. If you see the `POST /api/pin` with auth but NO `not authorized` message and pairing still failed: the PIN was accepted by the HTTP layer but Sunshine's internal pairing thread deadlocked — go to cert injection.

  0. **Web UI not authenticated (MOST COMMON).** The Sunshine Web UI silently rejects PIN submissions from browsers that haven't logged in. The `/api/pin` POST returns HTTP 200 (to prevent brute-force enumeration) and the Web UI shows "Success!", but Sunshine logs `Info: Web UI: [<ip>] -- not authorized` and the pairing never completes. This is the definitive diagnostic line — if you see it, the browser submitting the PIN was not logged in. **Fix:** Go to `https://<ip>:47990` FIRST, log in with the admin username/password set during setup, THEN navigate to `/pin` and enter the PIN. The username is stored in `sunshine_state.json` under `"username"`; the password was set interactively during first Web UI launch. If you forgot the password, reset the state file: `mv ~/.config/sunshine/sunshine_state.json ~/.config/sunshine/sunshine_state.json.bak`, restart Sunshine, and create new credentials at the Web UI.
  1. **Source IP mismatch (Anti-Spoofing):** Sunshine drops the pairing if the source IP of the browser submitting the PIN does not match the source IP of the Moonlight client requesting the pairing (unless `origin_pin_allowed = pc` is set). This happens if you open Moonlight on your PC but type the PIN into your phone's browser. **Fix:** Use a browser on the same device as Moonlight, or add `origin_pin_allowed = pc` to `sunshine.conf` to disable the IP check.
  2. **API Deadlock:** If IPs match, auth is valid, but pairing still hangs (Web UI spins or Moonlight times out), Sunshine's internal pairing thread is deadlocked (API requests to `/api/pin` hang indefinitely without logging). **Fix:** Hard reset the state database: `mv ~/.config/sunshine/sunshine_state.json ~/.config/sunshine/sunshine_state.json.bak`, restart Sunshine, go to the Web UI to create new initial admin credentials, and re-pair.
- **"RTSP handshake failed : error 10060" or "PC Offline" during stream launch.** If Moonlight pairs successfully but throws Error 10060 when you actually click the PC to start streaming, there are two primary causes:
  1. **Instant Crash:** Sunshine accepted the HTTP stream request but **instantly crashed** before the UDP/TCP RTSP streaming ports could connect (often due to the `pa_simple_new` audio crash). Check `sunshine.log` for immediate fatal errors. If Sunshine crashes, the Moonlight client is left hanging and eventually times out.
  2. **Ghost Port Binding / Firewall:** If the logs show `New streaming session started` but no subsequent errors (and the stream just times out), the RTSP port (48010) is either blocked by a client/router firewall, or a previous Sunshine crash left the port in a ghost TIME_WAIT state that refuses new connections. **Fix:** Force-close Moonlight on the client device, restart the `sunshine` service on the host to bind fresh ports, and try again. **If firewall is suspected:** deploy the prompt in `references/client-troubleshooting-prompt.md` to a Hermes agent running on the client PC to diagnose Windows Firewall/Tailscale interference.
- **The `--pin` CLI flag is removed.** In older versions, you could pair via CLI (`sunshine --pin 1234`). In Sunshine v2026+, this throws `Fatal: Unknown command: pin`. You MUST use the Web UI (`/pin`) or `POST /api/pin` to pair.
- **State file wipe changes `uniqueid` — breaks existing pairings.** When you reset `sunshine_state.json` (e.g., to fix an API deadlock or after creating new Web UI credentials), Sunshine generates a new `root.uniqueid`. All previously paired Moonlight clients still have their certificates cached locally, but those certs were bound to the old uniqueid. Clients will either get stuck at the PIN screen (Moonlight thinks it's paired, submits the old cert, Sunshine rejects it) or silently fail to connect. **Fix on every Moonlight client:** delete/forget the server entry in Moonlight's UI, then re-add the server IP to trigger a fresh pairing session. Without this step, clients appear as "paired" in Moonlight's UI but can never complete a handshake.
- **Pairing bypass via certificate injection (primary method for Sunshine v2026).** Sunshine v2026.516.143833's PIN validation pipeline is fundamentally broken — the `/api/pin` endpoint frequently deadlocks internally even when IP addresses match perfectly. **Try the PIN flow at most twice, then switch to injection.** Every `/pair` request from Moonlight logs a hex-encoded client certificate: `clientcert -- <hex>`. Decode it: the hex string is the client's self-signed X.509 PEM certificate in raw bytes. Inject it as a `named_devices` entry in `sunshine_state.json`. Restart Sunshine. Moonlight must then be **fully closed and re-opened** (not just minimized — kill the process) — it will see the pre-paired cert and skip the PIN screen. Full script at `references/pairing-bypass-inject-cert.py`. **Important:** the certificate from the log is the most recent one Moonlight sent. If the user retries the PIN flow multiple times, Moonlight may generate new certs — always use the LAST `clientcert` entry from the log, which corresponds to the current pairing attempt. **Restore proper pairing afterward:** once the stream works, remove the injected entry and re-pair normally.
- **Input packets arrive at Sunshine but don't inject — Sunshine uses Inputtino+uinput, NOT XTEST.** This is the single most important diagnostic fact. Sunshine does NOT use XTEST for keyboard/mouse injection — on Linux it uses the **Inputtino** library (a subproject inside Sunshine) which creates virtual input devices through the Linux uinput/uhid subsystems. The Inputtino library is **not** libevdev, despite libevdev being linked by Sunshine. Do NOT assume an LD_PRELOAD shim targeting libevdev will fix Inputtino — they are separate libraries. Verify with `nm -D /usr/bin/sunshine | grep -i "inputtino\|libevdev"` — both sets of symbols are present, but Inputtino handles keyboard/mouse while libevdev may only be used for device probing. Inputtino creates devices via `/dev/uinput` (keyboard/mouse) and `/dev/uhid` (gamepads/touch/pen), with gamepad creation appearing in the kernel log as `input: Sunshine PS5 (virtual) pad as /devices/virtual/misc/uhid/...`. xdotool or Python XTEST tests working is NOT evidence that Sunshine input should work — they use completely different mechanisms. Sunshine receives Moonlight input packets (`keyboard packet`, `relative mouse move packet` with real `deltaX`/`deltaY`) but must inject them through Inputtino→uinput. If uinput is broken, input silently fails with zero errors in the Sunshine log — verify with `grep "keyboard packet\|mouse move" ~/.config/sunshine/sunshine.log | tail -5` to confirm packets ARE arriving, then check uinput (see diagnostic below).

**Input debugging prioritization — STOP AFTER 2 FAILED ATTEMPTS per layer.** When keyboard/mouse don't work during a stream: (1) Verify Sunshine is receiving packets: `grep "keyboard packet\|mouse.*packet" ~/.config/sunshine/sunshine.log | tail -5`. If empty, the client isn't sending input → Moonlight settings issue. (2) If packets arrive, run the uinput diagnostic once. If uinput returns -25 → go directly to the xdotool bridge. Do NOT spend time on LD_PRELOAD shims, kernel module reloading, or WM switching — the bridge is the proven fix and takes 30 seconds to deploy. (3) If uinput returns 0 but input still doesn't work → check XFWM4 is running, `hw_cursor = disabled` is set, and Moonlight runs as admin on Windows.

**uinput diagnostic (BEFORE any other input troubleshooting):**
```bash
# Quick test: can Sunshine's uinput path create a device?
python3 -c "
import ctypes
libevdev = ctypes.CDLL('libevdev.so.2')
dev = libevdev.libevdev_new()
libevdev.libevdev_set_name(dev, b'test')
libevdev.libevdev_enable_event_type(dev, 1)  # EV_KEY
uinput_fd = ctypes.c_int(-1)
ret = libevdev.libevdev_uinput_create_from_device(dev, 3, ctypes.byref(uinput_fd))
print(f'uinput create returned: {ret} (0=success, negative=errno)')
"
```
Return value 0 = uinput works. Return value **-25** = `ENOTTY` — uinput broken on this kernel. Kernel 7.0.0-22 is confirmed affected. Kernel 6.x was working. This is not a Sunshine bug — it's a kernel uinput regression.

**Fix: LD_PRELOAD shim (attempted but may not reach Inputtino).** The kernel 7.0 uinput regression requires UI_DEV_SETUP before UI_DEV_CREATE. Sunshine's Inputtino/libevdev uses the old sequence. A trivial LD_PRELOAD shim overrides libevdev_uinput_create_from_device to insert the missing ioctl. Source, compile command, and deployment steps in scripts/uinput-shim.c. **CAVEAT — THIS HAS BEEN TESTED AND FOUND NOT TO WORK ON THIS SETUP:** The shim intercepts libevdev's uinput creation, but Sunshine's input backend is Inputtino (not libevdev). Inputtino creates uinput devices through a separate internal code path that the LD_PRELOAD shim does NOT intercept. Gamepad/touch/pen devices are created by Inputtino via /dev/uhid (which works on kernel 7.0), but keyboard/mouse uinput creation happens inside Inputtino's own compiled code. Deployment is harmless (the shim only adds a required ioctl) but should not be relied upon as the primary fix.

**Fix: xdotool input bridge (RECOMMENDED when uinput is broken).** When uinput is broken AND the LD_PRELOAD shim can't intercept Inputtino's internal calls, use a Python bridge that watches Sunshine's log for keyboard/mouse packets and injects them via xdotool into X11. This completely bypasses uinput. The bridge script is `scripts/sunshine-input-bridge.py`. Run it in the background alongside Sunshine: `python3 ~/.hermes/skills/gaming/game-streaming/scripts/sunshine-input-bridge.py &`. It tails the Sunshine log and calls `xdotool keydown/keyup` for keyboard events and `xdotool mousemove_relative` for mouse events. **IMPORTANT: Sunshine sends Windows Virtual Key codes (VK_*), NOT USB HID codes.** VK_LSHIFT=0xA0, VK_LCONTROL=0xA2, VK_LMENU=0xA4 (Alt), VK_RETURN=0x0D, etc. The bridge's VK_TO_X11 mapping handles this correctly — if keyboard doesn't work, verify the keycode appears in the mapping table. Requires `xdotool` (apt install xdotool). This is a reliable fallback that works on any kernel version because it uses XTEST (which Sunshine itself does not use).\n\n**PITFALL: Input bridge must be restarted after any Sunshine restart.** The bridge tails Sunshine's log file by following the specific PID's file descriptor. When Sunshine restarts, the old log handle is stale and the bridge stops receiving input packets. After any Sunshine restart (upgrades, crash recovery, config changes, systemctl --user restart), kill the old bridge and start a fresh one:\n```bash\n# Kill old bridge (if running) and start fresh\npkill -f sunshine-input-bridge.py 2>/dev/null\ncd ~/.config/sunshine && nohup python3 sunshine-input-bridge.py > /dev/null 2>&1 &\n# Or use the canonical path:\nnohup python3 ~/.hermes/skills/gaming/game-streaming/scripts/sunshine-input-bridge.py > /dev/null 2>&1 &\n```\nVerify both processes are alive: `ps aux | grep -E 'sunshine|input_bridge' | grep -v grep`. The bridge PID should be newer than or close to the Sunshine PID — if the bridge predates Sunshine by hours/days, it's stale.

**Workarounds when uinput is broken (fallback if shim cannot be compiled):**
1. **Steam Remote Play.** Steam's built-in streaming uses its own input protocol that does not depend on uinput. Enable it by copying templates/remote_play.vdf to ~/.steam/debian-installation/config/remote_play.vdf (the template has host_input and enabled pre-configured). On the client, install Steam, log into the same account, and use the Stream button instead of Play. Keyboard, mouse, and controller all work natively through Steam's input pipeline.
2. **Boot an older kernel** if available (Linux 6.x where uinput works).

**If uinput IS working (ret=0):**
1. Verify the user is in the `input` group and `/dev/uinput` is writable: `test -w /dev/uinput && echo OK`.
2. Add `hw_cursor = disabled` to `sunshine.conf` to force software cursor into the video stream.
3. Verify Sunshine is NOT running as root (root forces NvFBC capture which bypasses all input).
4. On Moonlight client: Settings → Input → "Capture system keyboard shortcuts" ON. Admin rights required on Windows.
5. Deep diagnostic: `grep -A 3 "begin relative mouse move packet" ~/.config/sunshine/sunshine.log | tail -20` to confirm packets have real deltas and aren't empty.
- **Sunshine log is overwritten on restart.** `~/.config/sunshine/sunshine.log` is truncated on each process start. Copy it before restarting to preserve the crash log: `cp ~/.config/sunshine/sunshine.log ~/sunshine-crash-$(date +%H%M).log`. Systemd journal (`journalctl --user -u sunshine`) persists across restarts but may be empty if Sunshine logs mainly to its own file.
- **Troubleshooting when Sunshine is offline.** Full diagnostic workflow in `references/troubleshooting-crashes.md` — covers checking systemd status, journal, sunshine.log, stale processes, and restart.
