initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
---
description: Skills for setting up, configuring, and managing game servers, modpacks, and gaming-related infrastructure.
---
+413
View File
@@ -0,0 +1,413 @@
---
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.
@@ -0,0 +1,40 @@
# Clean Reset: Remove All User Modifications
When Sunshine was modified with workarounds (uinput shim, input bridge, custom config) and you want to go back to a completely clean package state — removing everything created by the user, keeping only what the .deb package installed.
## What to remove
```bash
# Systemd override (all user-added drop-ins)
rm -rf ~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/
# Stale manual service file (user-created, not from .deb package)
rm -f ~/.config/systemd/user/sunshine.service
# Workaround files
rm -f ~/.config/sunshine/input_bridge.py
rm -f ~/.config/sunshine/uinput_fix.so
rm -f ~/.config/sunshine/sunshine_state.json.bak
# Custom config (let package defaults regenerate)
rm -f ~/.config/sunshine/sunshine.conf
rm -f ~/.config/sunshine/apps.json
# Reload systemd
systemctl --user daemon-reload
```
## What to keep
- `~/.config/sunshine/credentials/` — pairing certificates, needed for Moonlight clients to reconnect
- `~/.config/sunshine/sunshine.log` — runtime log artifact, harmless
## After reset
Sunshine regenerates `sunshine.conf` and `apps.json` with package defaults on next start. The web UI at `https://localhost:47990` prompts for first-time username/password setup. All previously paired Moonlight clients must be re-paired.
## Ubuntu 26.04 note
Kernel 7.0.0 has broken uinput. A clean install means NO input workarounds — keyboard/mouse injection will fail silently. The uinput diagnostic (see `scripts/diagnose-uinput.sh`) returns -25. To get input working, you must re-deploy either:
- The LD_PRELOAD shim (`scripts/uinput-shim.c`) — but this may not intercept Inputtino's internal code path
- The xdotool input bridge (`scripts/sunshine-input-bridge.py`) — reliable fallback
@@ -0,0 +1,35 @@
# Moonlight Client-Side Firewall/Routing Diagnostic Prompt
Deploy this prompt to a Hermes agent running on the Moonlight CLIENT PC
when the Sunshine server is confirmed working (paired, ports listening)
but Moonlight throws `Error 10060` (RTSP handshake timeout) on stream launch.
The prompt assumes:
- Sunshine server IP is known (replace `<SERVER_IP>` below)
- Sunshine is confirmed listening on TCP 48010, 47984, 47989, 47990
- Pairing completed successfully (client cert in sunshine_state.json)
- The block is on the client side (Windows Firewall, Tailscale routing, etc.)
---COPY BELOW THIS LINE---
Hey Hermes, I'm trying to stream from my Sunshine server (IP: <SERVER_IP>) to
this PC using Moonlight, but I keep getting "RTSP Handshake failed: Error 10060".
The host agent confirmed that the PC is successfully paired, and the Sunshine
server is running and listening on all required ports (TCP 47984, 47989, 47990,
48010). However, when Moonlight attempts to start the stream, the RTSP handshake
on TCP 48010 is timing out before it ever reaches the server logs.
Please perform a full network and firewall diagnostic on this client PC to fix
the connection to the host:
1. Verify if Windows Defender / Windows Firewall is blocking Moonlight.exe or
the required ports (TCP 48010, UDP 48000, 48010) and add allow rules for them.
2. Test network reachability to the host using
`Test-NetConnection -ComputerName <SERVER_IP> -Port 48010`.
3. Check if there are any stale UDP/TCP connections held open in TIME_WAIT state
that might be blocking Moonlight.
4. If Tailscale is installed on this PC, check if it's interfering with local
LAN routing to <SERVER_IP>.
Fix any firewall or routing blockers you find so Moonlight can successfully
complete the handshake.
@@ -0,0 +1,82 @@
# Black Screen Diagnostic Workflow
When Sunshine, x11vnc, or any display capture tool shows a black screen, use this
workflow to determine whether it's a capture/encoding problem or the GPU is
genuinely outputting black pixels.
## Step 1: Take a hardware screenshot
```bash
DISPLAY=:0 xwd -root -out /tmp/screen.xwd
convert /tmp/screen.xwd /tmp/screen.png
ls -la /tmp/screen.png
```
If the PNG is tiny (~1.3KB regardless of resolution), the framebuffer is all
black. Verify with Python:
```python
from PIL import Image
img = Image.open('/tmp/screen.png')
samples = [img.getpixel((x, y)) for y in range(0, img.height, 200)
for x in range(0, img.width, 200)]
unique = set(samples)
# {0} means completely black — GPU is not rendering
```
## Step 2: If framebuffer is black, check DRM connector state
```bash
for c in /sys/class/drm/card*-HDMI-*/; do
echo "$(basename $c): status=$(cat $c/status) enabled=$(cat $c/enabled) dpms=$(cat $c/dpms)"
done
```
Normal output:
```
card1-HDMI-A-1: status=connected enabled=enabled dpms=On
```
Problem output (DPMS lockup):
```
card1-HDMI-A-1: status=connected enabled=disabled dpms=Off
```
## Step 3: Cross-check with nvidia-settings
```bash
nvidia-settings -q EnabledDisplays -q ConnectedDisplays
```
Healthy: `EnabledDisplays` matches `ConnectedDisplays` bitmask.
Locked: `EnabledDisplays = 0x00000000` while `ConnectedDisplays` shows the port.
## Step 4: Cross-check X11 state
```bash
DISPLAY=:0 xrandr | grep "HDMI-0"
DISPLAY=:0 xset q | grep -i "monitor\|dpms"
```
X11 may report "Monitor is On" and show modes available even when the kernel
connector is disabled. This is a driver-level mismatch — X11's DPMS state
doesn't reflect the kernel's actual state.
## Resolution
If `enabled=disabled` and `dpms=Off` at the kernel level:
- `xset dpms force on` and `xrandr --output HDMI-0 --auto` will NOT fix it
- Writing to `/sys/class/drm/card*-HDMI-*/dpms` returns Permission denied even
as root (NVIDIA driver read-only)
- The GPU display controller needs a full power cycle
- Only a reboot reliably fixes this
After reboot, X11 authorization is broken (LightDM's Xauthority is invalid for
the user). See the main SKILL.md DPMS lockup pitfall for the Xauth merge fix.
## If framebuffer is NOT black (screenshot shows desktop)
The problem is in the capture/streaming layer:
- Sunshine: check `output_name`, `capture` method, `hw_cursor`
- x11vnc: add `-noxdamage`, use `novnc_proxy` launcher
- noVNC: check UFW allows the port, browser console for WebSocket errors
@@ -0,0 +1,189 @@
# Headless Server Setup for Game Streaming
Full pipeline for running Sunshine + games on a headless Linux server (no monitor attached).
## Required components
1. **HDMI dummy plug** ($5-10) — plugs into GPU HDMI port, emulates a 4K display. Without it, NVIDIA GPUs run in low-power mode with reduced clocks and may refuse to render at full speed. Essential for headless setups.
**EDID reality check:** Many "4K/120" dummy plugs (e.g., FUEARAN) advertise 4K@120Hz on the label but their EDID caps 4K at 60Hz. Actual high-refresh options are 1440p@120Hz and 1080p@120Hz. Check with:
```bash
DISPLAY=:0 xrandr --query | grep -A 20 "^HDMI-0" | grep -E "^[ ]+[0-9]"
```
If 4K@120Hz is absent despite the label, the plug hardware doesn't support it — plan your game resolutions accordingly.
2. **Xorg** — needs to run headless pointing at the dummy display, and persist across reboots.
**Verify the dummy display:**
```bash
for conn in /sys/class/drm/card0-*/status; do echo "$conn: $(cat $conn)"; done
# Look for "connected" — that's your dummy plug
cat /sys/class/drm/card0-HDMI-A-1/modes | head -1 # should show 3840x2160 or similar
```
**Start Xorg (manual test):**
```bash
sudo Xorg :0 -ac -nolisten tcp vt7 &
sleep 2
DISPLAY=:0 xdpyinfo | grep dimensions # verify
```
**Xorg persistence (systemd system service):**
Xorg needs root for VT access on headless servers. Create a system service so it survives reboots:
```ini
# /etc/systemd/system/xorg-headless.service
[Unit]
Description=Xorg display server for headless game streaming
After=multi-user.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/Xorg :0 -ac -nolisten tcp vt7
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
```
Then: `sudo systemctl daemon-reload && sudo systemctl enable --now xorg-headless`
3. **Sunshine** — game streaming server. See main SKILL.md for install.
4. **Game launchers:**
- **Steam (+ Proton):** Requires 32-bit architecture on Ubuntu 26.04+.
```bash
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y steam-installer
```
**Headless blocker — zenity license dialog:** The `steam-installer` package installs a `/usr/games/steam` wrapper script that, on first run, pops up a zenity license dialog (`--ok-label=Install --cancel-label=Cancel`). On a headless display, nobody can click "Install", so the Steam runtime never downloads. **Do not attempt debconf preseeding** — the zenity check (lines 113139 of `/usr/games/steam`) is hardcoded and ignores debconf.
**Workaround — bootstrap Steam manually:** Run the `scripts/setup-headless-steam.sh` script included in this skill. It automatically detects the required version, downloads the bootstrap tarball, unpacks the embedded runtime, and links the directories so the GUI license dialog is bypassed.
After these steps, `DISPLAY=:0 steam` launches without any dialog. First run downloads the full Steam client (~200-500 MB) and displays a self-updating progress bar (zenity `--progress --auto-close`). The client goes through a multi-stage bootstrap: update download → restart → runtime unpack → restart → login screen. This takes 3-5 minutes on a fast connection. Steam must be running and logged in for Sunshine's "Steam Big Picture" app to work.
Proton is built into Steam — enable in Settings → Steam Play. Handles ~85% of Windows games.
- **Lutris:** `sudo apt install lutris`. For non-Steam games: GOG, Epic Games Store, Battle.net, emulators.
## Pipeline
```
Headless server (no monitor)
↳ HDMI dummy plug → GPU sees "4K display" → full clocks enabled
↳ Xorg virtual desktop on dummy display
↳ Game launches (Steam/Lutris)
↳ Sunshine captures X11 display via NVENC
↳ Moonlight on client device streams it
```
## GPU requirements
- **NVIDIA GPU with NVENC** (Turing or newer: GTX 1650 Super+, RTX 20-series+, RTX 30-series+, RTX 40-series+)
- Consumer cards limited to 3 concurrent NVENC sessions
- NVENC is dedicated silicon — negligible FPS hit during encoding
## Linux game compatibility
| Type | How | Compatibility |
|---|---|---|
| Native Linux games | Steam → Install → Play | 100% |
| Windows games (Proton) | Steam → Enable Steam Play → Install | ~85% works out of box, ~10% needs tweaks, ~5% broken |
| Anti-cheat games | Valorant, CoD, Fortnite, Destiny 2 | 0% — kernel-level anti-cheat incompatible with Proton |
| Non-Steam games | Lutris (GOG, Epic, Battle.net) | Varies, mostly good |
ProtonDB (protondb.com) is the definitive compatibility reference for any specific game.
## Audio setup (headless)
Headless servers have no physical speakers and often no audio system at all. Sunshine needs an audio sink to capture game audio and stream it to Moonlight.
### Install PipeWire
```bash
sudo apt install -y pipewire pipewire-pulse wireplumber pulseaudio-utils
```
This gives you a PulseAudio-compatible interface over PipeWire 1.6+. The `auto_null` (Dummy Output) sink is created automatically — it accepts audio from games and makes it available for Sunshine capture without needing physical hardware.
### User must be in `audio` group
```bash
sudo usermod -a -G audio $USER
# Requires re-login or systemctl --user daemon-reexec to take effect
```
Without the audio group, `/dev/snd/*` devices are permission-denied and PipeWire can't enumerate ALSA hardware. The `auto_null` sink still works for capture, but NVIDIA HDMI audio (from the dummy plug's EDID) won't appear as a proper hardware sink until the group is active.
### Sunshine config for headless dummy plug
**Required config — tell Sunshine which display to capture:**
Without `output_name`, Sunshine defaults to display index 0 (DP-0), which may be disconnected even with a dummy plug in HDMI-0. The log shows:
```
Configuring selected display (0) to stream
Couldn't get requested display info, defaulting to recording entire virtual desktop
```
Fix: add to `~/.config/sunshine/sunshine.conf`:
```
output_name = 2
```
⚠️ **IMPORTANT:** `output_name` takes a **numeric display index**, NOT the connector name string. Passing `HDMI-0` as the value causes Sunshine to fail with `Could not stream display number, there are only [8] displays.`
Determine the correct index from Sunshine's own log output:
```bash
grep "Detected display" ~/.config/sunshine/sunshine.log
# Detected display: DP-0 (id: 0)DP-0 connected: false
# Detected display: DP-1 (id: 1)DP-1 connected: false
# Detected display: HDMI-0 (id: 2)HDMI-0 connected: true ← use output_name = 2
# Detected display: DP-2 (id: 3)DP-2 connected: false
```
The `(id: N)` value is the zero-based display index. Only one display should be `connected: true` on a headless system with a single dummy plug. After setting, verify Sunshine captures the correct display:
```
Streaming display: HDMI-0 with res 3840x2160 offset by 0x0
```
This log line confirms Sunshine is streaming the right display. Required alongside `capture = x11` for NVIDIA headless setups.
**Do NOT set audio sink config:**
`audio_sink` or `virtual_sink` in `sunshine.conf` causes an immediate fatal crash (`pa_simple_new() failed: Invalid argument`) on Sunshine v2026+. Let Sunshine auto-detect PipeWire instead.
**CRITICAL FIX FOR CRASHING STREAMS:** If Sunshine still crashes with `Invalid argument` upon stream start, PipeWire is rejecting Sunshine's attempt to dynamically create virtual surround sinks. You must pre-create them so Sunshine finds them instead of crashing:
```bash
pactl load-module module-null-sink sink_name=sink-sunshine-stereo
pactl load-module module-null-sink sink_name=sink-sunshine-surround51 channels=6 channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right
pactl load-module module-null-sink sink_name=sink-sunshine-surround71 channels=8 channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right
```
*(Run these commands before Sunshine starts, e.g. in a wrapper script or systemd ExecStartPre).*
### Verify audio
```bash
pactl info | grep "Default Sink" # should show auto_null
pactl list short sinks # should show auto_null, SUSPENDED
journalctl --user -u sunshine | grep -i audio # should have no warnings/errors
```
The earlier "Warning: There will be no audio" in sunshine logs should be gone after this config.
### Post-reboot: NVIDIA HDMI audio
After a reboot (once the `audio` group takes effect), WirePlumber discovers the NVIDIA HDMI audio device exposed by the dummy plug. An additional sink like `alsa_output.pci-0000_01_00.1.hdmi-stereo` appears. Either sink works — games output to the default sink and Sunshine captures whichever one is configured. The real HDMI sink has the advantage of proper EDID-negotiated sample rates (up to 96kHz on the FUERAN dummy plug).
## VRAM cohabitation strategy
When the server also runs LLM inference:
| Scenario | Solution |
|---|---|
| Model always loaded, game occasionally | Socket-activated llama-server (see llama-cpp skill, deployment-patterns.md). Model unloads when idle, 3-4s warm restart. |
| Gaming always, model occasionally | Stop llama-server before gaming. Start manually when needed. |
| Both simultaneously | Only works if VRAM fits both — e.g., 24 GB card with a 7B model (~4.7 GB) leaves ~19 GB for games. |
## Testing before buying hardware
- Verify `nvidia-smi` shows NVENC support (`nvidia-smi -q | grep -A5 Encoder`)
- Run Sunshine and confirm web UI at `https://localhost:47990`
- Install Moonlight on any device and verify auto-discovery works
@@ -0,0 +1,260 @@
# Headless Sunshine Troubleshooting
Real-world issues and fixes from a headless Sunshine + Moonlight setup on Ubuntu 26.04 with NVIDIA RTX 2080 Ti (driver 580.159.03).
## Symptom: "Video failed to find working encoder" / "Unable to find display or encoder"
Sunshine starts but immediately exits. Logs show it trying vulkan → vaapi → software, all failing, never reaching NVENC.
### Root cause: cascade of silent failures
Several things can cause this, often in combination:
1. **User not in `video` and `render` groups** — can't access `/dev/dri/card0` or `/dev/dri/renderD128`
2. **Systemd service missing `DISPLAY`** — X11 capture can't connect to Xorg
3. **CUDA/driver mismatch** — NVFBC capture fails, and if X11 fallback is also broken, nothing works
### Debugging technique: run sunshine directly
Systemd journal logs are terse — "nvenc failed" with no detail. Run sunshine from the terminal to see the full verbose output:
```bash
systemctl --user stop app-dev.lizardbyte.app.Sunshine
DISPLAY=:0 /usr/bin/sunshine ~/.config/sunshine/sunshine.conf
```
The verbose output shows every CUDA symbol load, every NVENC initialization call, and the exact failure point. In the working case, you'll see:
```
Found H.264 encoder: h264_nvenc [nvenc]
Found HEVC encoder: hevc_nvenc [nvenc]
```
**Note:** When checking DRM device permissions, use `stat /dev/dri/card0` — do NOT `cat` or `dd` from DRM devices as they can hang the shell when read without a proper DRM master context.
## Fix 1: Group permissions
```bash
# Check current groups
groups $USER | grep -oP '(video|render|input|audio)'
# Add missing groups
sudo usermod -a -G video,render,input,audio $USER
# Refresh groups in current session (no re-login needed for systemd user)
systemctl --user daemon-reexec
# Fix uinput for gamepad support
sudo chown $USER:input /dev/uinput
```
Without these:
- `/dev/dri/card0` → "Permission denied" → no KMS capture
- `/dev/dri/renderD128` → can't initialize NVENC CUDA context
- `/dev/uinput` → "Gamepad ds5 is disabled due to Permission denied"
- `/dev/snd/*` → PipeWire can't enumerate ALSA hardware (auto_null sink still works for capture, but NVIDIA HDMI audio sink won't appear until after reboot)
## Fix 1b: Give Sunshine effective `cap_sys_admin` for input injection
Sunshine has `cap_sys_admin=pe` (permitted only) by default and **drops it on startup** (log: `drop_elevated_privileges succeeded in dropping capabilities`). After dropping, `/dev/uinput` access may fail silently even when the user is in the `input` group — no visible error, just zero input events in the log.
```bash
# Check current capabilities
getcap /usr/bin/sunshine
# cap_sys_admin,cap_sys_nice=p ← 'p' = permitted only, dropped on startup
# Set to effective (keeps the capability active at runtime)
sudo setcap cap_sys_admin=ep /usr/bin/sunshine
getcap /usr/bin/sunshine
# cap_sys_admin=ep ← 'e' = effective, won't be dropped
```
Without `cap_sys_admin=ep`, Sunshine processes zero input events from Moonlight — the log shows stream activity but no `XTEST`, `keyboard`, or `mouse` entries. With it, you'll see `XTEST relative movement` and key events in the log as the client sends input.
### Check if uinput is available
On modern kernels (Ubuntu 7.0+), `uinput` is often built-in rather than a loadable module:
```bash
# Check if uinput is built-in or a module
modinfo uinput | head -3
# filename: (builtin) ← no need to modprobe, already available
# OR: filename: /lib/modules/.../uinput.ko ← loadable, may need modprobe
# Verify the device node exists with correct permissions
stat /dev/uinput
# crw-rw---- 1 root input 10, 223 ...
# Ray must be in the input group to access it
groups $USER | grep input
```
Do NOT waste time running `modprobe uinput` if it's built-in — it will silently succeed without loading anything. The key checks are: device node exists, user in `input` group, and `cap_sys_admin=ep` on the binary.
## Fix 2: Systemd DISPLAY override
The sunshine systemd service doesn't know about manually-started Xorg sessions. Create a drop-in:
```bash
mkdir -p ~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d
cat > ~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/override.conf << 'EOF'
[Service]
Environment=DISPLAY=:0
# Clear the 5-second sleep delay (was waiting for desktop session)
ExecStartPre=
LimitNICE=-10
EOF
systemctl --user daemon-reload
systemctl --user restart app-dev.lizardbyte.app.Sunshine
```
Without this, X11 capture fails with "Unable to initialize capture method."
**Pitfall: Do NOT add `SupplementaryGroups=input` to this override.** If the user's login session doesn't have the `input` group (because group changes need a fresh login), systemd will fail to start the service with `status=216/GROUP` — "Changing group credentials failed: Operation not permitted." Adding supplementary groups only works if the user manager itself has those groups. For gamepad support, fix uinput permissions instead (see Fix 1), or wait until next login for group changes to propagate.
## Fix 2b: `output_name` must match the connected display
Sunshine's `output_name` is a zero-based index into the list of detected displays. If it points to a disconnected output, streaming will produce a black screen or fall back to "recording entire virtual desktop." Check which displays are connected:
```bash
# Method 1: xrandr
DISPLAY=:0 xrandr --listmonitors
# Shows: Monitors: 1
# 0: +*HDMI-0 3840/697x2160/392+0+0 HDMI-0
# The monitor name is HDMI-0, but output_name needs the INDEX (0 in this case)
# Method 2: Check sunshine log
grep "Detected display" ~/.config/sunshine/sunshine.log
# Shows: Detected display: DP-0 (id: 0)DP-0 connected: false
# Detected display: HDMI-0 (id: 2)HDMI-0 connected: true
# → Set output_name = 2
```
Sunshine logs each display as it detects them with `(id: N)` and `connected: true/false`. Match the index to the one that's `connected: true`. On headless systems with a dummy plug, only one display will be connected. Setting the wrong index produces: `Warning: Couldn't get requested display info, defaulting to recording entire virtual desktop.`
## Fix 3: CUDA/driver version mismatch (NVFBC → X11 fallback)
Sunshine v2026+ bundles CUDA 13.1.1 which requires NVIDIA driver ≥590.48.01. On systems with older drivers (e.g., 580.159.03), NVFBC capture fails because the CUDA runtime can't initialize. However, NVENC encoding works fine — the NVIDIA encode library (`libnvidia-encode.so.1`) is driver-versioned separately and doesn't need CUDA 13.
**Solution:** Force X11 capture instead of NVFBC:
```ini
# ~/.config/sunshine/sunshine.conf
capture = x11
encoder = nvenc
adapter_name = /dev/dri/renderD128
output_name = 0
```
X11 capture + NVENC works reliably. NVFBC is faster (direct GPU memory) but requires compatible CUDA. KMS capture also works but needs `video` group and `cap_sys_admin`.
### Verifying NVENC works independently
Even when Sunshine's NVFBC path fails, ffmpeg can confirm NVENC is functional:
```bash
DISPLAY=:0 ffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i :0.0+0,0 \
-c:v h264_nvenc -preset p1 -t 3 -f null /dev/null
```
If this works (shows encoding fps), NVENC is fine — the problem is Sunshine's capture/init path.
## Fix 4: CSRF protection for remote web UI access
Sunshine v2026 added CSRF protection. Accessing the web UI from anything other than localhost is blocked. Add to sunshine.conf:
```ini
origin_pin_allowed = pc
csrf_allowed_origins = https://100.93.253.36:47990,https://192.168.50.98:47990
```
Both `origin_pin_allowed = pc` and `csrf_allowed_origins` are required for LAN access. `origin_pin_allowed` relaxes the PIN/credential creation check for LAN origins; `csrf_allowed_origins` whitelists the specific origin URLs for CSRF token validation. Without `origin_pin_allowed`, credential creation is blocked even with the CSRF origin whitelisted.
Replace the IPs with your server's LAN and Tailscale IPs. Multiple origins are comma-separated. Restart sunshine after changing.
## Fix 5: Start-limit hit from repeated failures
After too many restart attempts, systemd blocks further starts:
```
app-dev.lizardbyte.app.Sunshine.service: Start request repeated too quickly.
Failed with result 'start-limit-hit'.
```
Reset with:
```bash
systemctl --user reset-failed app-dev.lizardbyte.app.Sunshine
systemctl --user start app-dev.lizardbyte.app.Sunshine
```
## Working sunshine.conf (headless X11 + NVENC)
```ini
sunshine_name = rayserver
capture = x11
encoder = nvenc
# adapter_name should NOT be set for NVENC — Sunshine auto-detects the GPU
# adapter_name = auto
output_name = 0
min_log_level = info
upnp = disabled
origin_pin_allowed = pc
csrf_allowed_origins = https://100.93.253.36:47990
audio_sink = auto_null
virtual_sink = auto_null
```
**IMPORTANT:** `output_name` is the zero-based index of the connected display. It varies per system. Check with `grep "Detected display" ~/.config/sunshine/sunshine.log` after starting Sunshine — use the index of the display marked `connected: true`. See Fix 2b above for details.
## Fix 6: Root process picks NvFBC (no input forwarding)
When Sunshine runs as root (via `sudo systemd-run`, a system-level service, or direct `sudo sunshine`), it auto-selects NvFBC capture because root has direct GPU access. NvFBC reads the framebuffer from GPU memory — it bypasses X11 entirely. Result: video streams fine, audio works, but **zero input events reach the desktop**. Sunshine logs show no `XTEST`, `keyboard`, or `mouse` activity at all.
The fix is simple — don't run as root. Use the user-level systemd service:
```bash
# STOP: never do this
sudo systemd-run --unit=sunshine-streamer sunshine # → NvFBC, no input
# DO: always use the user service
systemctl --user start app-dev.lizardbyte.app.Sunshine
# Verify it's running as the right user
ps aux | grep sunshine | grep -v grep
# Should show: ray ... /usr/bin/sunshine ← NOT root
```
If you accidentally started as root, kill it and restart:
```bash
sudo pkill -f "sunshine" # kills all sunshine processes (including root one)
sleep 2
systemctl --user start app-dev.lizardbyte.app.Sunshine
```
## Verification checklist
```bash
# 1. Xorg running on dummy display
DISPLAY=:0 xdpyinfo | grep dimensions # should show 3840x2160
# 2. Groups correct
groups $USER | grep -E 'video|render|input|audio'
# 3. NVENC encoders detected by ffmpeg
ffmpeg -encoders 2>/dev/null | grep nvenc
# 4. Sunshine running
systemctl --user status app-dev.lizardbyte.app.Sunshine
# 5. Web UI accessible
curl -sk https://localhost:47990/ | head -5
# 6. Avahi advertising
avahi-browse -at 2>/dev/null | grep sunshine
# 7. Audio running (no warnings in sunshine logs)
pactl info | grep "Default Sink"
journalctl --user -u app-dev.lizardbyte.app.Sunshine --no-pager -n 30 | grep -i "no audio" # should return nothing
```
@@ -0,0 +1,74 @@
# HP OMEN 30L (8703 motherboard) — GPU Passthrough & Game Streaming
Hardware-specific findings from a session analyzing game streaming feasibility on an HP OMEN 30L desktop with motherboard model 8703.
## Hardware
- **Motherboard:** HP 8703 (HP OMEN 30L prebuilt)
- **CPU:** Intel Core i7-10700K (Comet Lake, 10th gen) — has Intel UHD Graphics 630
- **GPU:** NVIDIA RTX 2080 Ti (11GB VRAM, Turing, NVENC H.264/HEVC/AV1)
- **RAM:** DDR4-3200, 4 DIMM slots
- **OS:** Ubuntu Server, headless (no monitor, no display server)
## iGPU Status: DISABLED
The i7-10700K's UHD Graphics 630 does NOT appear in `lspci` or `/sys/class/drm/`. HP OMEN 30L prebuilts disable the iGPU in BIOS when a discrete GPU is installed. The 8703 motherboard has no physical video outputs for the iGPU even if enabled. The only detected GPU is the RTX 2080 Ti at `card0`.
## IOMMU Group Layout
```
Group 1:
- NVIDIA TU102 [GeForce RTX 2080 Ti] (10de:1e07)
- NVIDIA TU102 HD Audio Controller (10de:10f7)
- NVIDIA TU102 USB 3.1 Host Controller (10de:1ad6)
- NVIDIA TU102 USB Type-C UCSI Controller (10de:1ad7)
```
The GPU is alone in its IOMMU group — no PCIe bridge, no root port, no other devices. This is ideal for VFIO passthrough. The USB controller on the GPU is the Type-C VirtualLink port — you'd pass it through with the GPU but it's rarely used.
## GPU Features
- NVENC: H.264, HEVC, AV1 (Turing encoder — excellent quality, ~x264 medium at 1/10th latency)
- 3 concurrent NVENC sessions (consumer cap, sufficient for single-stream gaming)
- 11 GB VRAM
## Simultaneous Windows + Linux Options
Without a second GPU or enabled iGPU, here are the real options:
| Option | Cost | Windows gaming | Linux GPU | Complexity |
|---|---|---|---|---|
| **Second GPU** (GT 1030 / GTX 1050) | ~$40-80 | Yes (2080 Ti passthrough) | Yes (second GPU) | Medium |
| **Single GPU passthrough** | Free | Yes | None | High |
| **Proton only** | Free | ~85% of games | Yes (native) | Low |
| **Dual boot** | Free | Yes (native) | N/A when in Windows | Low |
### Second GPU (recommended)
Cheap low-power GPU for Linux host (GT 1030 ~30W, $40-80 used). RTX 2080 Ti passed to Windows VM. Linux keeps dedicated GPU for Ollama/LLM, Docker GPU, Immich ML. Both OSes run simultaneously with dedicated hardware.
The HP 8703 has at least one PCIe x16 slot (occupied by 2080 Ti) and one x4 slot (free). Verify physical space and PSU headroom — a GT 1030 draws 30W max, negligible.
### Single GPU passthrough
Bind RTX 2080 Ti to VFIO at boot. Linux host loses ALL GPU: no Ollama, no Immich ML, no NVENC. When Windows VM shuts down, GPU can be rebound to nvidia driver. Fragile — reboot is cleaner. Use only if you're comfortable with kernel parameters and VFIO scripts.
### Proton only
Zero cost, zero complexity. Sunshine + Moonlight on Linux. ~85% of Windows games work via Proton. Kernel anti-cheat games (Valorant, CoD, Fortnite, Destiny 2) are 0% compatible — no workaround.
## VRAM Cohabitation
RTX 2080 Ti: 11 GB VRAM.
- qwen2.5:14b (Q4_K_M): ~9 GB
- AAA game (modern): 4-8 GB
- They don't fit simultaneously. Must stop Ollama before gaming, restart after.
- Socket-activated llama-server: stop model, warm-restart in 3-4 seconds when gaming ends.
## Headless Setup Requirements
- HDMI dummy plug (~$5-10) — essential for GPU to run at full clocks
- Xorg virtual desktop on dummy display
- Sunshine captures X11 via NVENC
- Steam + Proton for Windows games
- Lutris for non-Steam games (GOG, Epic, Battle.net)
@@ -0,0 +1,84 @@
# Kernel 7.0 uinput regression: UI_DEV_SETUP required
## Summary
Kernel 7.0.0-22-generic broke the legacy uinput creation sequence.
Sunshine's Inputtino and libevdev both use the old method and fail silently.
## Pre-diagnostic: is uinput built-in or a module?
Before running the C or Python diagnostic below, check if uinput is a loadable module or built into the kernel:
```bash
modinfo uinput 2>&1 | grep filename
# "filename: (builtin)" → uinput is compiled into the kernel
# "filename: /lib/modules/..." → uinput is a loadable module
grep CONFIG_INPUT_UINPUT /boot/config-$(uname -r)
# "CONFIG_INPUT_UINPUT=y" → built-in (no modprobe needed)
# "CONFIG_INPUT_UINPUT=m" → module (modprobe uinput to load)
```
**If built-in:** `modprobe uinput` will succeed silently but `lsmod | grep uinput` will show nothing — this is expected. The device node at `/dev/uinput` exists regardless. Permissions (user in `input` group) are what matter. Skip any module-loading troubleshooting steps.
**If a module:** `sudo modprobe uinput` and verify with `lsmod | grep uinput`. To persist across reboots: `echo "uinput" | sudo tee /etc/modules-load.d/uinput.conf`.
## Root cause
The uinput subsystem on kernel 7.0+ REQUIRES `UI_DEV_SETUP` ioctl before
`UI_DEV_CREATE`. The old method (writing `struct uinput_user_dev` via write()
or calling `UI_DEV_CREATE` without prior setup) is rejected with EINVAL (-22).
Sunshine v2026.516.143833 links against libevdev 1.13.6, which uses the old method.
Inputtino (Sunshine's actual input backend, not libevdev) also uses the old method.
## Diagnostic (C test)
```c
#include <linux/uinput.h>
int fd = open("/dev/uinput", O_WRONLY);
ioctl(fd, UI_DEV_CREATE); // FAILS: EINVAL on kernel 7.0
// Correct kernel 7.0 sequence:
struct uinput_setup usetup;
memset(&usetup, 0, sizeof(usetup));
usetup.id.bustype = BUS_USB;
strcpy(usetup.name, "test");
ioctl(fd, UI_DEV_SETUP, &usetup); // NEW: Required on 7.0+
ioctl(fd, UI_DEV_CREATE); // SUCCESS
```
## Diagnostic (Python test)
```python
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)
uifd = ctypes.c_int(-1)
ret = libevdev.libevdev_uinput_create_from_device(dev, 3, ctypes.byref(uifd))
# Returns -25 (ENOTTY) on kernel 7.0 — uinput broken
# Returns 0 on kernel 6.x — uinput works
```
## Why the LD_PRELOAD shim may not work
The shim (`scripts/uinput-shim.c`) intercepts `libevdev_uinput_create_from_device`.
However, Sunshine's PRIMARY input backend is **Inputtino** (subproject inside Sunshine
binary), not libevdev. Inputtino creates keyboard/mouse devices through a separate
code path that may not call through libevdev's public API. If the shim is deployed
and input still doesn't work, Inputtino is bypassing it.
## Proven workarounds
1. **xdotool bridge** (`scripts/sunshine-input-bridge.py`) — tails Sunshine log,
injects via xdotool XTEST. Works on any kernel.
2. **Steam Remote Play** (`templates/remote_play.vdf`) — different protocol,
no uinput involved.
3. **Boot kernel 6.x** if available.
## Kernel version
Tested broken: 7.0.0-22-generic (Ubuntu 26.04)
Tested working: Linux 6.x series
@@ -0,0 +1,61 @@
# Locked Dummy Plug — BadMatch on xrandr Mode Switch
## Symptom
All `xrandr --output HDMI-0 --mode <res>` commands fail with:
```
xrandr: Configure crtc 0 failed
X Error of failed request: BadMatch (invalid parameter attributes)
Major opcode of failed request: 140 (RANDR)
Minor opcode of failed request: 7 (RRSetScreenSize)
```
Exit code 1. Occurs for every resolution/refresh combination — 4K@60, 1440p@120, 1080p@120 — even though EDID advertises all of them.
## Root Cause
Cheap HDMI dummy plugs ("FUEARAN 4K/120" and similar unbranded adapters) use a fixed-resolution video signal generator chip that cannot be reconfigured via CRTC/PHY. The EDID is static and lists many modes but the hardware can only output its single native resolution (typically 3840x2160@60). xrandr accepts the mode switch request, passes it to the NVIDIA driver, which tries to reconfigure the CRTC, and the hardware returns `BadMatch`.
## Hardware Details
- **Make/Model:** FUEARAN 4K/120 (labeled as HDMI 2.1 dummy plug)
- **EDID tag:** `0046554552414e20344b2f313230` (decoded: "FUEARAN 4K/120")
- **Physical connection:** HDMI-0 (NVIDIA port, X11 connector)
- **NVIDIA GPU:** RTX 2080 Ti, driver 580.159.03
- **Xorg mode:** X11 (not Wayland, not KMS capture)
- **Reports available via xrandr:** 3840x2160@60*, 2560x1440@120, 1920x1080@120, etc.
- **Actually works:** 3840x2160 at whatever rate it powers up (60Hz via xrandr)
## Diagnosis
```bash
# Test any mode switch — if it fails, plug is locked
DISPLAY=:0 xrandr --output HDMI-0 --mode 1920x1080 2>&1
# Check current actual mode
DISPLAY=:0 xrandr | grep "*"
# Verify Sunshine captures it correctly despite the lock
grep "Streaming display:" ~/.config/sunshine/sunshine.log
# Should show: HDMI-0 with res 3840x2160 offset by 0x0
```
## Fix
The `output_name = 2` fix in sunshine.conf is the primary black-screen fix (ensures Sunshine captures HDMI-0, not a disconnected display). Since the dummy plug can't switch modes, set all app prep-cmds to no-ops:
```json
"prep-cmd": [
{
"do": "",
"undo": "setsid steam steam://close/bigpicture"
}
]
```
## Alternatives
1. **Better dummy plug:** "Headless Ghost" or "Dr. HDMI" series — these support actual mode switching via hardware.
2. **Custom modeline:** Generate with `cvt 1920 1080 120` and add via `xrandr --newmode` + `xrandr --addmode` — may fail if the hardware PHY truly cannot drive different timings.
3. **NVIDIA Virtual Display (NVFBC-based):** `sudo nvidia-xconfig --virtual=1920x1080 --mode=1920x1080` — only works with `capture = nvfbc` which is broken on driver 580+.
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Emergency Sunshine pairing bypass — extract client cert from log and inject into state file.
Sunshine v2026.516.143833 has a broken PIN validation pipeline. The /api/pin endpoint
deadlocks internally even when IP addresses match. This script:
1. Reads the most recent /pair request's clientcert from sunshine.log
2. Decodes the hex-encoded PEM cert
3. Injects it directly into sunshine_state.json as a pre-paired named_device
4. Restarts Sunshine
After running, Moonlight must be FULLY CLOSED AND REOPENED — it will see the pre-paired
cert and skip the PIN screen.
Usage:
python3 pairing-bypass-inject-cert.py [--restart]
Requirements:
- Root not needed (Sunshine runs as the same user)
- sunshine_state.json must be writable
- Moonlight must have sent at least one /pair request (visible in sunshine.log)
"""
import json
import re
import uuid
import sys
import os
import subprocess
import time
SUNSHINE_LOG = os.path.expanduser("~/.config/sunshine/sunshine.log")
SUNSHINE_STATE = os.path.expanduser("~/.config/sunshine/sunshine_state.json")
SUNSHINE_CONF = os.path.expanduser("~/.config/sunshine/sunshine.conf")
def extract_latest_cert():
with open(SUNSHINE_LOG, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
matches = re.findall(r'clientcert -- ([0-9a-fA-F]+)', content)
if not matches:
print("ERROR: No clientcert found in sunshine.log")
print("Make sure Moonlight has tried to pair (it should show a PIN screen)")
sys.exit(1)
hex_cert = matches[-1]
pem_cert = bytes.fromhex(hex_cert).decode("utf-8")
print(f"Extracted certificate ({len(pem_cert)} bytes)")
print(f"Preview: {pem_cert[:60]}...")
return pem_cert
def inject_cert(pem_cert, client_name="RE-PC"):
try:
with open(SUNSHINE_STATE, "r") as f:
state = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
state = {}
if "root" not in state:
state["root"] = {"uniqueid": "0123456789ABCDEF", "named_devices": []}
if "named_devices" not in state["root"]:
state["root"]["named_devices"] = []
# Check if client already exists — update cert
updated = False
for dev in state["root"]["named_devices"]:
if dev.get("name") == client_name:
dev["cert"] = pem_cert
dev["enabled"] = "true"
updated = True
print(f"Updated existing client '{client_name}'")
break
if not updated:
state["root"]["named_devices"].append({
"name": client_name,
"cert": pem_cert,
"uuid": str(uuid.uuid4()).upper(),
"enabled": "true"
})
print(f"Added new client '{client_name}'")
# Ensure origin_pin_allowed = pc for easier future pairing
try:
with open(SUNSHINE_CONF, "r") as f:
conf = f.read()
if "origin_pin_allowed" not in conf:
with open(SUNSHINE_CONF, "a") as f:
f.write("\norigin_pin_allowed = pc\n")
print("Added origin_pin_allowed = pc to sunshine.conf")
except FileNotFoundError:
pass
with open(SUNSHINE_STATE, "w") as f:
json.dump(state, f, indent=4)
print(f"Injected into {SUNSHINE_STATE}")
def restart_sunshine():
print("Restarting Sunshine...")
subprocess.run(["systemctl", "--user", "restart", "sunshine"], check=False)
time.sleep(3)
result = subprocess.run(
["systemctl", "--user", "status", "sunshine", "--no-pager"],
capture_output=True, text=True
)
if "active (running)" in result.stdout:
print("Sunshine is running.")
else:
print("WARNING: Sunshine may not have started. Check: systemctl --user status sunshine")
def main():
restore = "--restart" in sys.argv
cert = extract_latest_cert()
inject_cert(cert, client_name=os.environ.get("MOONLIGHT_CLIENT_NAME", "RE-PC"))
if restore:
restart_sunshine()
else:
print("\nNow restart Sunshine: systemctl --user restart sunshine")
print("\nIMPORTANT: Fully CLOSE Moonlight on the client, then reopen it.")
print("It should connect directly to apps without asking for a PIN.")
if __name__ == "__main__":
main()
@@ -0,0 +1,77 @@
# Quick Browser-Based Remote Desktop (x11vnc + noVNC)
Lightweight alternative to Sunshine/Moonlight when you need to view the desktop in a laptop browser — no client install, no pairing, zero config beyond apt packages. Useful for quick admin tasks, one-off desktop viewing, or when Moonlight isn't available on the client.
## When to use
- View the desktop from a laptop browser (Chrome, Firefox, Safari)
- No client software allowed/available on the remote machine
- Quick one-off access — don't need persistent pairing or low-latency game streaming
- User says "I need to see my desktop in my browser"
## Setup (one-time)
```bash
sudo apt-get install -y x11vnc websockify novnc
```
## Launch (run each time)
```bash
# 1. Kill any stale processes from previous runs
pkill x11vnc 2>/dev/null
pkill websockify 2>/dev/null
sleep 1
# 2. Share the existing display via VNC (no password, shared)
# -noxdamage fixes black screen with noVNC
# -scale 1920x1080 downsamples 4K for browser performance
x11vnc -display :0 -forever -shared -nopw -noxdamage -scale 1920x1080 &
# 3. Use the official noVNC launcher (handles websockify bridge internally)
/usr/share/novnc/utils/novnc_proxy --listen 6800 --vnc localhost:5900 --web /usr/share/novnc &
```
**IMPORTANT:** Using the official `novnc_proxy` launcher is preferred over raw `websockify`. It starts its own internal websockify, serves noVNC files, and handles the WebSocket handshake correctly. It serves `vnc.html` (the full noVNC client) at the root, not just `vnc_lite.html`.
## Verify
```bash
# x11vnc listening (port 5900)
ss -tlnp | grep 5900
# novnc_proxy listening (port 6800, 0.0.0.0 = accessible from LAN)
ss -tlnp | grep 6800
# noVNC serving
curl -s -o /dev/null -w "%{http_code}" http://localhost:6800/vnc.html
# Expect: 200
```
## Client URL
Open in laptop browser:
```
http://<server-ip>:6800/vnc.html
```
The `novnc_proxy` launcher auto-fills the WebSocket connection parameters — no query params needed. Example: `http://192.168.50.98:6800/vnc.html`
## Teardown
```bash
pkill x11vnc
pkill websockify
pkill -f novnc_proxy
```
## Pitfalls
- **UFW blocks port 6800 by default.** If the client browser says "site can't be reached," open the port: `sudo ufw allow 6800/tcp`.
- **Black screen in noVNC.** If noVNC connects but shows only black (no desktop), first check whether it's a VNC encoding issue or the GPU framebuffer itself is black. Take an X11 screenshot to distinguish: `DISPLAY=:0 xwd -root -out /tmp/screen.xwd && convert /tmp/screen.xwd /tmp/screen.png`. If the screenshot is all-black (tiny file, ~1.3KB for any resolution), the GPU isn't rendering anything — check DRM connector state: `for c in /sys/class/drm/card*-HDMI-*/; do echo "$(basename $c): $(cat $c/enabled) dpms=$(cat $c/dpms)"; done`. If `enabled=disabled` and `dpms=Off`, the display is in a kernel-level DPMS lockup and only a reboot will fix it (see `references/debug-black-screen.md` for full workflow). If the screenshot shows the desktop but noVNC shows black, it's a VNC encoding issue — add `-noxdamage` to x11vnc. XDamage can fail to track on 4K displays and with compositing enabled.
- **4K display kills browser performance.** Streaming a native 4K (3840x2160) framebuffer over noVNC is extremely heavy for the browser. Always downsample with `-scale 1920x1080` (or `-scale 1280x720` for slower connections). The x11vnc `-scale` flag handles this server-side.
- **Use novnc_proxy, not raw websockify.** The Debian `novnc` package includes `/usr/share/novnc/utils/novnc_proxy` — a wrapper script that manages websockify internally, serves the full `vnc.html` client, and handles auto-connect better than the raw `websockify --web=...` command.
- **websockify binds 0.0.0.0 by default** — accessible from LAN. No auth on this setup; only use on trusted networks.
- **x11vnc -nopw means NO password.** Anyone on the LAN can connect to port 5900 directly. Acceptable for quick local use; don't leave running.
- **Contrasts with Sunshine/Moonlight:** This is pure software encode (no NVENC), higher latency, higher CPU usage. Fine for desktop viewing; terrible for gaming.
@@ -0,0 +1,168 @@
# Troubleshooting Sunshine Crashes
Diagnostic workflow for when Sunshine is offline, won't start, or crashes.
## Quick debug sequence
```bash
# 1. Is it running?
systemctl --user status sunshine --no-pager
# 2. Any recent crashes in systemd journal?
journalctl --user -u sunshine --no-pager -n 50
# 3. Check Sunshine's own log (⚠ overwritten on restart — copy first!)
cat ~/.config/sunshine/sunshine.log | grep -i "fatal\|error\|hang\|terminate"
# 4. Stale processes?
ps aux | grep -i sunshine | grep -v grep
# 5. Port conflicts?
ss -tlnp | grep -E "47984|47989|47990|48010"
# 6. Restart
systemctl --user restart sunshine
```
## Common crash patterns
### "Hang detected! Session failed to terminate in 10 seconds"
**Symptom:** Sunshine dies after a Moonlight client connects or disconnects. Log shows:
```
Fatal: Hang detected! Session failed to terminate in 10 seconds.
```
**Cause:** NVENC encoder or X11 capture pipeline hung during session teardown. The internal 10-second watchdog fires and kills the process.
**Common root cause — missing window manager:** Sunshine's X11 capture requires a running window manager or desktop environment on DISPLAY=:0. An X server alone (login screen, headless with no WM) allows capture to start but teardown of the X11 pipeline hangs without a WM managing the window resources. This produces a reproducible Hang detected crash every time a Moonlight client attempts a session.
**Diagnostic — check WM before restarting:**
```bash
# Is a window manager running on :0?
ps aux | grep -iE "openbox|xfce|gnome|kde|mutter|xfwm" | grep -v grep
# Is X11 alive?
DISPLAY=:0 xdpyinfo 2>&1 | head -5
# If X11 is running but no WM, start one:
DISPLAY=:0 openbox --replace &
# Then restart Sunshine
systemctl --user restart sunshine
```
**Fix:** If a WM is running, restart Sunshine — this may be a transient encoder hang. If it happens repeatedly and a WM is present:
- Check for GPU driver instability (`nvidia-smi` for errors)
- Stop co-resident LLM models to free VRAM before gaming (`ollama stop`)
- Consider increasing session timeout (Sunshine Web UI → Configuration → General)
- If on a headless server, ensure the dummy plug hasn't become unseated — no connected display causes similar hang behavior
### Silent death (log just stops, no error)
**Symptom:** Sunshine.log has startup messages then just ends. No Fatal, no Error, no "Terminate handler called."
**Possible causes:**
- OOM kill (check `dmesg | grep -i "out of memory"`)
- SIGKILL from systemd (check `journalctl --user -u sunshine` for signal)
- Second "Hang detected" crash where the log was overwritten before the Fatal line was flushed to disk
**Fix:** Restart and watch live: `tail -f ~/.config/sunshine/sunshine.log` in another terminal, then connect a Moonlight client and disconnect. Observe the teardown sequence.
### "Couldn't bind RTSP server to port [48010], Address already in use"
**Symptom:** Logged as `Fatal` during startup but Sunshine continues running.
**Cause:** A stale Flatpak wrapper process from a previous crash still holds port 48010.
**Impact:** Moonlight uses RTSP (port 48010) for streaming protocol negotiation. Without it, clients get "RTSP handshake failed" (error 10060/connection timeout) even though Sunshine appears running and the HTTPS ports (47984, 47989) and Web UI (47990) are up.
**Fix:**
```bash
# Find and kill the stale wrapper (not the new /usr/bin/sunshine)
ps aux | grep sunshine | grep -v grep
kill <stale-pid>
# Then restart so Sunshine rebinds all ports cleanly
systemctl --user restart sunshine
# Verify all four ports are listening
ss -tlnp | grep -E "47984|47989|47990|48010"
```
### Clean SIGTERM ("Terminate handler called")
**Symptom:** Log shows `Info: Terminate handler called` with no preceding error.
**Cause:** Something sent SIGTERM — manual stop (`systemctl --user stop`), systemd cleanup, or a Hermes agent session issuing restart.
**Fix:** Just restart. Not a crash — check system journal around the timestamp for what sent the signal.
### RTSP connection reset / "No pending session" (error 10054)
**Symptom:** Moonlight client reports "Starting RTSP handshake failed: error 10054" (Windows) or "Connection reset by peer." Sunshine log shows:
```
Debug: No pending session for incoming RTSP connection
```
Sunshine is running, all ports are listening (verified with `ss -tlnp | grep sunshine`), and HTTPS/HTTP serverinfo requests succeed — but every RTSP connection is immediately reset.
**Cause:** The Moonlight client was never paired, or its pairing was lost. Sunshine's RTSP server requires a pending session created by the HTTP pairing flow BEFORE accepting RTSP connections. With zero paired clients, there is never a pending session, so every RTSP connection gets RST immediately.
**Diagnostic — check pairing state:**
```bash
python3 -c "
import json
with open('$HOME/.config/sunshine/sunshine_state.json') as f:
state = json.load(f)
paired = state.get('paired_clients', [])
print(f'Paired clients: {len(paired)}')
"
```
If the count is 0, the client must be paired.
**Fix — pair the client:**
1. Open Moonlight, click "+", enter `192.168.50.98` (or let it auto-discover)
2. Moonlight prompts for a PIN
3. Open Sunshine Web UI at `https://<server-ip>:47990/pin`
4. Enter the PIN shown in Moonlight into the Web UI
5. After pairing, RTSP connections will be accepted
**Troubleshooting IP Mismatch during pairing:** If the Web UI says "Success!" but Moonlight still says "PC Offline" and `sunshine_state.json` still shows 0 paired clients, this is an **IP routing mismatch**. Sunshine drops the pairing if the client IP Moonlight is using (e.g., Tailscale `100.x.x.x`) doesn't match the IP you used in the browser to access the Web UI (e.g., LAN `192.168.x.x`). **Fix:** Make sure the IP address typed into Moonlight's "Add PC" dialog *exactly matches* the IP address in your browser's address bar when entering the PIN.
**Note on `origin_pin_allowed`:** With `origin_pin_allowed = pc` in sunshine.conf, only localhost connections auto-pair. All LAN/WAN clients must enter a PIN. If `origin_pin_allowed = lan`, Tailscale clients get "Request Timed out" during pairing because Sunshine rejects the non-LAN source address.
**Error code reference:**
| Error | Meaning | Likely cause |
|---|---|---|
| 10060 | Connection timeout | Port not listening, firewall, Sunshine not running, stale process holding port 48010, OR **Sunshine crashed instantly upon connection (check log for audio `pa_simple_new` errors)** |
| 10054 | Connection reset by peer | RTSP server rejecting — unpaired client, or RTSP server module broken |
### Instant Crash on Connection (Audio Init Failure)
**Symptom:** Moonlight connects successfully via HTTP, logs show `New streaming session started`, but Sunshine crashes immediately. Moonlight reports `Error 10060` (Timeout / PC Offline) because the server died before the RTSP streaming ports could be reached.
**Log shows:**
```
Error: pa_simple_new() failed: Invalid argument
Error: Unable to initialize audio capture. The stream will not have audio.
```
**Cause:** In Sunshine v2026+, hardcoded `audio_sink = auto_null` or `virtual_sink = auto_null` in `sunshine.conf` causes a fatal crash during PulseAudio/PipeWire initialization when the client attempts to start the stream.
**Fix:** Remove `audio_sink` and `virtual_sink` from `~/.config/sunshine/sunshine.conf` and restart Sunshine. Allow Sunshine to auto-detect the PipeWire virtual sinks.
## Log locations
| Source | Path | Persistence |
|---|---|---|
| Sunshine app log | `~/.config/sunshine/sunshine.log` | **Overwritten on restart** — copy before restarting |
| Systemd journal (user) | `journalctl --user -u sunshine` | Persistent across restarts |
| System journal | `journalctl --since "HH:MM" --until "HH:MM"` | Persistent, broader system context |
| Sunshine state | `~/.config/sunshine/sunshine_state.json` | Persists across restarts (client pairings, app list) |
## Preserving crash logs
Sunshine.log is the most useful for debugging but is wiped on restart. Always copy first:
```bash
cp ~/.config/sunshine/sunshine.log ~/sunshine-crash-$(date +%Y%m%d-%H%M).log
```
Then restart and compare the old crash log against the new startup log.
@@ -0,0 +1,66 @@
# Windows VM + GPU Passthrough for Gaming
Running a Windows VM alongside Linux services on the same machine so anti-cheat games and Proton-incompatible titles work. Requires both OSes to coexist without disrupting Linux services (Docker, HA, Immich, Ollama).
## Option matrix
### Option 1: Second GPU (best)
Buy a cheap low-power GPU (GT 1030 / GTX 1050 / RX 550 — $40-80 used) for Linux. Pass the primary gaming GPU (2080 Ti) to Windows VM via VFIO.
| | Linux host | Windows VM |
|---|---|---|
| GPU | Cheap second card | Gaming GPU (full power) |
| LLM inference | Stays on Ollama ✅ | — |
| NVENC | Sunshine on Windows ✅ | Native ✅ |
| Anti-cheat games | — | Full compatibility ✅ |
| All Docker services | Unchanged ✅ | — |
Requirements: motherboard with two PCIe x16 (or x16 + x4 physical) slots. PSU headroom negligible (GT 1030 draws ~30W).
### Option 2: Single GPU passthrough (free, painful)
Pass the only GPU to Windows. Linux becomes fully headless — no GPU at all.
- All GPU workloads die on Linux: no Ollama, no Immich ML, no NVENC, no Docker GPU acceleration
- Must bind GPU to VFIO at boot (kernel cmdline: `vfio-pci.ids=10de:1e07,10de:10f7,10de:1ad6,10de:1ad7`)
- When Windows VM shuts down, GPU can be rebound to nvidia driver on Linux — but fragile; a reboot is cleaner
- LLM must run inside Windows VM or via CPU-only on Linux (impractically slow)
### Option 3: Proton-only (free, zero complexity)
No Windows VM. Accept that ~5% of games with kernel anti-cheat (Valorant, CoD, Fortnite, Destiny 2) won't work. Everything else runs through Steam Proton or Lutris on native Linux.
### Option 4: Dual boot
Reboot between Linux and Windows. Not simultaneous — all Linux services are down while gaming.
## OEM motherboard pitfalls
### HP OMEN 30L (HP 8703 board)
- **iGPU is disabled when dGPU installed.** The i7-10700K has Intel UHD Graphics 630 but it's not exposed — no video output ports routed to back panel, not visible in lspci. You cannot use iGPU for Linux host + dGPU for Windows VM passthrough on this board.
- **Only one GPU works at a time.** Option 1 (second GPU) requires verifying the board has a second physical PCIe slot and that both GPUs fit physically.
- **BIOS may lack IOMMU/VT-d settings.** HP OEM BIOSes are stripped down. VT-d/IOMMU may be enabled by default but not configurable. Check with `dmesg | grep -iE "IOMMU|DMAR"` and `ls /sys/kernel/iommu_groups/`.
### General OEM board checks before GPU passthrough
```bash
# 1. GPU IOMMU group — must be isolated (no other devices in same group)
for d in /sys/kernel/iommu_groups/*/devices/*; do
n=${d#*/iommu_groups/*}; n=${n%%/*}
echo "Group $n: $(lspci -nns "${d##*/}" | cut -d' ' -f2-)"
done
# 2. VFIO kernel module available
modprobe vfio-pci && echo "vfio-pci available"
# 3. Second PCIe slot existence
lspci | grep -iE "VGA|3D|display"
```
## What this does NOT cover
- Full VFIO passthrough setup (libvirt XML, vfio-pci binding, Windows virtio drivers)
- Looking Glass (low-latency VM display on Linux host)
- SR-IOV (GPU virtualization — not supported on consumer NVIDIA cards)
@@ -0,0 +1,59 @@
#!/bin/bash
# Quick diagnostic: is uinput functional on this kernel?
# Sunshine uses libevdev/uinput for keyboard/mouse injection, NOT XTEST.
# If this script fails, all input will be silently broken regardless of
# Moonlight settings, admin rights, or WM choice.
set -e
echo "=== uinput diagnostic for Sunshine game streaming ==="
echo "Kernel: $(uname -r)"
echo ""
# Check 1: basic device node
if [ -e /dev/uinput ]; then
echo "[PASS] /dev/uinput exists"
else
echo "[FAIL] /dev/uinput does not exist"
exit 1
fi
# Check 2: writable
if [ -w /dev/uinput ]; then
echo "[PASS] /dev/uinput is writable"
else
echo "[FAIL] /dev/uinput is NOT writable"
echo " Fix: sudo usermod -a -G input $USER && newgrp input"
exit 1
fi
# Check 3: libevdev uinput create (the exact API Sunshine uses)
python3 -c '
import ctypes, sys
libevdev = ctypes.CDLL("libevdev.so.2")
dev = libevdev.libevdev_new()
if not dev:
print("[FAIL] libevdev_new() returned NULL")
sys.exit(1)
libevdev.libevdev_set_name(dev, b"diagnose-uinput")
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))
libevdev.libevdev_free(dev)
if ret == 0 and uinput_fd.value >= 0:
print(f"[PASS] uinput device created (fd={uinput_fd.value})")
import os; os.close(uinput_fd.value)
elif ret < 0:
import errno
errname = errno.errorcode.get(-ret, f"unknown({ret})")
print(f"[FAIL] uinput create returned {ret} (errno: {errname})")
if ret == -25:
print(" ENOTTY: uinput interface broken on this kernel (known on 7.0.0-22)")
print(" Workaround: Steam Remote Play or boot older kernel")
sys.exit(1)
else:
print(f"[FAIL] uinput create returned {ret}")
sys.exit(1)
'
echo ""
echo "=== Diagnostic complete ==="
@@ -0,0 +1,34 @@
#!/bin/bash
# Bootstraps Steam on a headless server without requiring X11 interaction.
# Bypasses the zenity license dialog that normally hangs headless installations.
set -e
# Get the current version expected by the Debian package
if [ -f "/usr/games/steam" ]; then
VERSION=$(grep -oP 'version="\K[^"]+' /usr/games/steam || echo "1.0.0.85")
else
VERSION="1.0.0.85"
fi
echo "Bootstrapping Steam headless installation (Version $VERSION)..."
STEAMDIR="$HOME/.steam/debian-installation"
mkdir -p "$STEAMDIR"
echo "Downloading Steam bootstrap tarball..."
curl -fsSL "https://repo.steampowered.com/steam/archive/beta/steam_${VERSION}.tar.gz" | tar -xz -C "$STEAMDIR"
echo "Extracting embedded Steam runtime..."
tar -xJf "$STEAMDIR/steam-launcher/bootstraplinux_ubuntu12_32.tar.xz" -C "$STEAMDIR"
echo "Marking installation version..."
mkdir -p "$STEAMDIR/deb-installer"
echo "$VERSION" > "$STEAMDIR/deb-installer/version"
echo "Creating required API symlinks..."
ln -fns "$STEAMDIR" "$HOME/.steam/steam"
ln -fns "$STEAMDIR" "$HOME/.steam/root"
echo "Steam is now bootstrapped."
echo "Run 'DISPLAY=:0 steam' to trigger the background update (takes ~3 minutes)."
@@ -0,0 +1,49 @@
#!/bin/bash
# Launch Steam Big Picture and force it fullscreen on headless X11
#
# On headless servers with an HDMI dummy plug, Steam Big Picture frequently
# renders as a 1280x800 window instead of going fullscreen. This means
# Sunshine captures the entire root window (3840x2160) and Moonlight shows
# mostly desktop background — user sees a "black screen."
#
# Additionally, the dummy plug enters DPMS Off after ~15 minutes of
# inactivity, causing X11 capture to return black frames. This script
# forces the display on before launching Steam.
#
# Deploy to ~/.config/sunshine/scripts/steam-bigpicture.sh
# Reference in apps.json:
# "detached": ["/home/ray/.config/sunshine/scripts/steam-bigpicture.sh"]
# Also add DPMS fix to prep-cmd (Sunshine runs commands directly, use sh -c):
# "do": "sh -c 'DISPLAY=:0 xset -dpms s off; DISPLAY=:0 xset dpms force on'"
export DISPLAY=:0
export STEAM_BIGPICTURE=1
# Force the display ON and disable DPMS/screensaver
xset -dpms s off 2>/dev/null
xset dpms force on 2>/dev/null
# Launch Steam into Big Picture mode
steam -fulldesktopres -bigpicture &
# Wait up to 30 seconds for the Big Picture window to appear
for i in $(seq 1 30); do
sleep 1
WIN_ID=$(xdotool search --name "Steam Big Picture Mode" 2>/dev/null | head -1)
if [ -n "$WIN_ID" ]; then
sleep 3
# Fullscreen via wmctrl (EWMH protocol — most reliable on XFWM4)
wmctrl -ir "$WIN_ID" -b add,fullscreen 2>/dev/null
sleep 1
# Fallback: xdotool resize + move + activate
xdotool windowsize "$WIN_ID" 3840 2160 2>/dev/null
xdotool windowmove "$WIN_ID" 0 0 2>/dev/null
xdotool windowactivate "$WIN_ID" 2>/dev/null
exit 0
fi
done
exit 1
@@ -0,0 +1,62 @@
/*
* LD_PRELOAD shim for Sunshine on kernel 7.0+.
*
* PROBLEM: Kernel 7.0 (confirmed: 7.0.0-22-generic) requires UI_DEV_SETUP ioctl
* BEFORE UI_DEV_CREATE on /dev/uinput. The old write()+UI_DEV_CREATE sequence
* and bare UI_DEV_CREATE both fail with EINVAL. Sunshine links against
* libevdev which uses the old method. Result: keyboard and mouse input silently
* fail (Sunshine receives Moonlight packets but can't inject into X11).
*
* FIX: This shim overrides libevdev_uinput_create_from_device() to use the
* correct UI_DEV_SETUP + UI_DEV_CREATE sequence.
*
* COMPILE: gcc -shared -fPIC -o uinput_fix.so uinput-shim.c -ldl
*
* DEPLOY:
* 1. cp uinput_fix.so ~/.config/sunshine/uinput_fix.so
* 2. Add to Sunshine systemd override:
* [Service]
* Environment=LD_PRELOAD=/home/ray/.config/sunshine/uinput_fix.so
* 3. systemctl --user daemon-reload
* 4. systemctl --user restart sunshine
*
* VERIFY: After connecting Moonlight, check that input events are created:
* grep "keyboard packet" ~/.config/sunshine/sunshine.log | tail -5
* # Should see packets arriving, AND keyboard/mouse now works on client.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <linux/uinput.h>
int libevdev_uinput_create_from_device(void *dev, int flags, int *uifd) {
int fd = open("/dev/uinput", O_WRONLY);
if (fd < 0) { return -errno; }
struct uinput_setup usetup;
memset(&usetup, 0, sizeof(usetup));
usetup.id.bustype = BUS_USB;
usetup.id.vendor = 0x1234;
usetup.id.product = 0x5678;
strncpy(usetup.name, "Sunshine Keyboard", UINPUT_MAX_NAME_SIZE-1);
if (ioctl(fd, UI_DEV_SETUP, &usetup) < 0) {
close(fd);
return -errno;
}
if (ioctl(fd, UI_DEV_CREATE) < 0) {
close(fd);
return -errno;
}
*uifd = fd;
return 0;
}
@@ -0,0 +1,11 @@
"remote_play"
{
"enabled" "1"
"autodiscover" "1"
"streaming_client" "1"
"host_audio" "1"
"host_input" "1"
"stream_packets" "1"
"prefer_hardware_encoding" "1"
"hardware_encoding_hevc" "1"
}
@@ -0,0 +1,187 @@
---
name: minecraft-modpack-server
description: "Host modded Minecraft servers (CurseForge, Modrinth)."
tags: [minecraft, gaming, server, neoforge, forge, modpack]
platforms: [linux, macos]
---
# Minecraft Modpack Server Setup
## When to use
- User wants to set up a modded Minecraft server from a server pack zip
- User needs help with NeoForge/Forge server configuration
- User asks about Minecraft server performance tuning or backups
## Gather User Preferences First
Before starting setup, ask the user for:
- **Server name / MOTD** — what should it say in the server list?
- **Seed** — specific seed or random?
- **Difficulty** — peaceful / easy / normal / hard?
- **Gamemode** — survival / creative / adventure?
- **Online mode** — true (Mojang auth, legit accounts) or false (LAN/cracked friendly)?
- **Player count** — how many players expected? (affects RAM & view distance tuning)
- **RAM allocation** — or let agent decide based on mod count & available RAM?
- **View distance / simulation distance** — or let agent pick based on player count & hardware?
- **PvP** — on or off?
- **Whitelist** — open server or whitelist only?
- **Backups** — want automated backups? How often?
Use sensible defaults if the user doesn't care, but always ask before generating the config.
## Steps
### 1. Download & Inspect the Pack
```bash
mkdir -p ~/minecraft-server
cd ~/minecraft-server
wget -O serverpack.zip "<URL>"
unzip -o serverpack.zip -d server
ls server/
```
Look for: `startserver.sh`, installer jar (neoforge/forge), `user_jvm_args.txt`, `mods/` folder.
Check the script to determine: mod loader type, version, and required Java version.
### 2. Install Java
- Minecraft 1.21+ → Java 21: `sudo apt install openjdk-21-jre-headless`
- Minecraft 1.18-1.20 → Java 17: `sudo apt install openjdk-17-jre-headless`
- Minecraft 1.16 and below → Java 8: `sudo apt install openjdk-8-jre-headless`
- Verify: `java -version`
### 3. Install the Mod Loader
Most server packs include an install script. Use the INSTALL_ONLY env var to install without launching:
```bash
cd ~/minecraft-server/server
ATM10_INSTALL_ONLY=true bash startserver.sh
# Or for generic Forge packs:
# java -jar forge-*-installer.jar --installServer
```
This downloads libraries, patches the server jar, etc.
### 4. Accept EULA
```bash
echo "eula=true" > ~/minecraft-server/server/eula.txt
```
### 5. Configure server.properties
Key settings for modded/LAN:
```properties
motd=\u00a7b\u00a7lServer Name \u00a7r\u00a78| \u00a7aModpack Name
server-port=25565
online-mode=true # false for LAN without Mojang auth
enforce-secure-profile=true # match online-mode
difficulty=hard # most modpacks balance around hard
allow-flight=true # REQUIRED for modded (flying mounts/items)
spawn-protection=0 # let everyone build at spawn
max-tick-time=180000 # modded needs longer tick timeout
enable-command-block=true
```
Performance settings (scale to hardware):
```properties
# 2 players, beefy machine:
view-distance=16
simulation-distance=10
# 4-6 players, moderate machine:
view-distance=10
simulation-distance=6
# 8+ players or weaker hardware:
view-distance=8
simulation-distance=4
```
### 6. Tune JVM Args (user_jvm_args.txt)
Scale RAM to player count and mod count. Rule of thumb for modded:
- 100-200 mods: 6-12GB
- 200-350+ mods: 12-24GB
- Leave at least 8GB free for the OS/other tasks
```
-Xms12G
-Xmx24G
-XX:+UseG1GC
-XX:+ParallelRefProcEnabled
-XX:MaxGCPauseMillis=200
-XX:+UnlockExperimentalVMOptions
-XX:+DisableExplicitGC
-XX:+AlwaysPreTouch
-XX:G1NewSizePercent=30
-XX:G1MaxNewSizePercent=40
-XX:G1HeapRegionSize=8M
-XX:G1ReservePercent=20
-XX:G1HeapWastePercent=5
-XX:G1MixedGCCountTarget=4
-XX:InitiatingHeapOccupancyPercent=15
-XX:G1MixedGCLiveThresholdPercent=90
-XX:G1RSetUpdatingPauseTimePercent=5
-XX:SurvivorRatio=32
-XX:+PerfDisableSharedMem
-XX:MaxTenuringThreshold=1
```
### 7. Open Firewall
```bash
sudo ufw allow 25565/tcp comment "Minecraft Server"
```
Check with: `sudo ufw status | grep 25565`
### 8. Create Launch Script
```bash
cat > ~/start-minecraft.sh << 'EOF'
#!/bin/bash
cd ~/minecraft-server/server
java @user_jvm_args.txt @libraries/net/neoforged/neoforge/<VERSION>/unix_args.txt nogui
EOF
chmod +x ~/start-minecraft.sh
```
Note: For Forge (not NeoForge), the args file path differs. Check `startserver.sh` for the exact path.
### 9. Set Up Automated Backups
Create backup script:
```bash
cat > ~/minecraft-server/backup.sh << 'SCRIPT'
#!/bin/bash
SERVER_DIR="$HOME/minecraft-server/server"
BACKUP_DIR="$HOME/minecraft-server/backups"
WORLD_DIR="$SERVER_DIR/world"
MAX_BACKUPS=24
mkdir -p "$BACKUP_DIR"
[ ! -d "$WORLD_DIR" ] && echo "[BACKUP] No world folder" && exit 0
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_FILE="$BACKUP_DIR/world_${TIMESTAMP}.tar.gz"
echo "[BACKUP] Starting at $(date)"
tar -czf "$BACKUP_FILE" -C "$SERVER_DIR" world
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "[BACKUP] Saved: $BACKUP_FILE ($SIZE)"
BACKUP_COUNT=$(ls -1t "$BACKUP_DIR"/world_*.tar.gz 2>/dev/null | wc -l)
if [ "$BACKUP_COUNT" -gt "$MAX_BACKUPS" ]; then
REMOVE=$((BACKUP_COUNT - MAX_BACKUPS))
ls -1t "$BACKUP_DIR"/world_*.tar.gz | tail -n "$REMOVE" | xargs rm -f
echo "[BACKUP] Pruned $REMOVE old backup(s)"
fi
echo "[BACKUP] Done at $(date)"
SCRIPT
chmod +x ~/minecraft-server/backup.sh
```
Add hourly cron:
```bash
(crontab -l 2>/dev/null | grep -v "minecraft/backup.sh"; echo "0 * * * * $HOME/minecraft-server/backup.sh >> $HOME/minecraft-server/backups/backup.log 2>&1") | crontab -
```
## Pitfalls
- ALWAYS set `allow-flight=true` for modded — mods with jetpacks/flight will kick players otherwise
- `max-tick-time=180000` or higher — modded servers often have long ticks during worldgen
- First startup is SLOW (several minutes for big packs) — don't panic
- "Can't keep up!" warnings on first launch are normal, settles after initial chunk gen
- If online-mode=false, set enforce-secure-profile=false too or clients get rejected
- The pack's startserver.sh often has an auto-restart loop — make a clean launch script without it
- Delete the world/ folder to regenerate with a new seed
- Some packs have env vars to control behavior (e.g., ATM10 uses ATM10_JAVA, ATM10_RESTART, ATM10_INSTALL_ONLY)
## Verification
- `pgrep -fa neoforge` or `pgrep -fa minecraft` to check if running
- Check logs: `tail -f ~/minecraft-server/server/logs/latest.log`
- Look for "Done (Xs)!" in the log = server is ready
- Test connection: player adds server IP in Multiplayer
+216
View File
@@ -0,0 +1,216 @@
---
name: pokemon-player
description: "Play Pokemon via headless emulator + RAM reads."
tags: [gaming, pokemon, emulator, pyboy, gameplay, gameboy]
platforms: [linux, macos, windows]
---
# Pokemon Player
Play Pokemon games via headless emulation using the `pokemon-agent` package.
## When to Use
- User says "play pokemon", "start pokemon", "pokemon game"
- User asks about Pokemon Red, Blue, Yellow, FireRed, etc.
- User wants to watch an AI play Pokemon
- User references a ROM file (.gb, .gbc, .gba)
## Startup Procedure
### 1. First-time setup (clone, venv, install)
The repo is NousResearch/pokemon-agent on GitHub. Clone it, then
set up a Python 3.10+ virtual environment. Use uv (preferred for speed)
to create the venv and install the package in editable mode with the
pyboy extra. If uv is not available, fall back to python3 -m venv + pip.
On this machine it is already set up at /home/teknium/pokemon-agent
with a venv ready — just cd there and source .venv/bin/activate.
You also need a ROM file. Ask the user for theirs. On this machine
one exists at roms/pokemon_red.gb inside that directory.
NEVER download or provide ROM files — always ask the user.
### 2. Start the game server
From inside the pokemon-agent directory with the venv activated, run
pokemon-agent serve with --rom pointing to the ROM and --port 9876.
Run it in the background with &.
To resume from a saved game, add --load-state with the save name.
Wait 4 seconds for startup, then verify with GET /health.
### 3. Set up live dashboard for user to watch
Use an SSH reverse tunnel via localhost.run so the user can view
the dashboard in their browser. Connect with ssh, forwarding local
port 9876 to remote port 80 on nokey@localhost.run. Redirect output
to a log file, wait 10 seconds, then grep the log for the .lhr.life
URL. Give the user the URL with /dashboard/ appended.
The tunnel URL changes each time — give the user the new one if restarted.
## Save and Load
### When to save
- Every 15-20 turns of gameplay
- ALWAYS before gym battles, rival encounters, or risky fights
- Before entering a new town or dungeon
- Before any action you are unsure about
### How to save
POST /save with a descriptive name. Good examples:
before_brock, route1_start, mt_moon_entrance, got_cut
### How to load
POST /load with the save name.
### List available saves
GET /saves returns all saved states.
### Loading on server startup
Use --load-state flag when starting the server to auto-load a save.
This is faster than loading via the API after startup.
## The Gameplay Loop
### Step 1: OBSERVE — check state AND take a screenshot
GET /state for position, HP, battle, dialog.
GET /screenshot and save to /tmp/pokemon.png, then use vision_analyze.
Always do BOTH — RAM state gives numbers, vision gives spatial awareness.
### Step 2: ORIENT
- Dialog/text on screen → advance it
- In battle → fight or run
- Party hurt → head to Pokemon Center
- Near objective → navigate carefully
### Step 3: DECIDE
Priority: dialog > battle > heal > story objective > training > explore
### Step 4: ACT — move 2-4 steps max, then re-check
POST /action with a SHORT action list (2-4 actions, not 10-15).
### Step 5: VERIFY — screenshot after every move sequence
Take a screenshot and use vision_analyze to confirm you moved where
intended. This is the MOST IMPORTANT step. Without vision you WILL get lost.
### Step 6: RECORD progress to memory with PKM: prefix
### Step 7: SAVE periodically
## Action Reference
- press_a — confirm, talk, select
- press_b — cancel, close menu
- press_start — open game menu
- walk_up/down/left/right — move one tile
- hold_b_N — hold B for N frames (use for speeding through text)
- wait_60 — wait about 1 second (60 frames)
- a_until_dialog_end — press A repeatedly until dialog clears
## Critical Tips from Experience
### USE VISION CONSTANTLY
- Take a screenshot every 2-4 movement steps
- The RAM state tells you position and HP but NOT what is around you
- Ledges, fences, signs, building doors, NPCs — only visible via screenshot
- Ask the vision model specific questions: "what is one tile north of me?"
- When stuck, always screenshot before trying random directions
### Warp Transitions Need Extra Wait Time
When walking through a door or stairs, the screen fades to black during
the map transition. You MUST wait for it to complete. Add 2-3 wait_60
actions after any door/stair warp. Without waiting, the position reads
as stale and you will think you are still in the old map.
### Building Exit Trap
When you exit a building, you appear directly IN FRONT of the door.
If you walk north, you go right back inside. ALWAYS sidestep first
by walking left or right 2 tiles, then proceed in your intended direction.
### Dialog Handling
Gen 1 text scrolls slowly letter-by-letter. To speed through dialog,
hold B for 120 frames then press A. Repeat as needed. Holding B makes
text display at max speed. Then press A to advance to the next line.
The a_until_dialog_end action checks the RAM dialog flag, but this flag
does not catch ALL text states. If dialog seems stuck, use the manual
hold_b + press_a pattern instead and verify via screenshot.
### Ledges Are One-Way
Ledges (small cliff edges) can only be jumped DOWN (south), never climbed
UP (north). If blocked by a ledge going north, you must go left or right
to find the gap around it. Use vision to identify which direction the
gap is. Ask the vision model explicitly.
### Navigation Strategy
- Move 2-4 steps at a time, then screenshot to check position
- When entering a new area, screenshot immediately to orient
- Ask the vision model "which direction to [destination]?"
- If stuck for 3+ attempts, screenshot and re-evaluate completely
- Do not spam 10-15 movements — you will overshoot or get stuck
### Running from Wild Battles
On the battle menu, RUN is bottom-right. To reach it from the default
cursor position (FIGHT, top-left): press down then right to move cursor
to RUN, then press A. Wrap with hold_b to speed through text/animations.
### Battling (FIGHT)
On the battle menu FIGHT is top-left (default cursor position).
Press A to enter move selection, A again to use the first move.
Then hold B to speed through attack animations and text.
## Battle Strategy
### Decision Tree
1. Want to catch? → Weaken then throw Poke Ball
2. Wild you don't need? → RUN
3. Type advantage? → Use super-effective move
4. No advantage? → Use strongest STAB move
5. Low HP? → Switch or use Potion
### Gen 1 Type Chart (key matchups)
- Water beats Fire, Ground, Rock
- Fire beats Grass, Bug, Ice
- Grass beats Water, Ground, Rock
- Electric beats Water, Flying
- Ground beats Fire, Electric, Rock, Poison
- Psychic beats Fighting, Poison (dominant in Gen 1!)
### Gen 1 Quirks
- Special stat = both offense AND defense for special moves
- Psychic type is overpowered (Ghost moves bugged)
- Critical hits based on Speed stat
- Wrap/Bind prevent opponent from acting
- Focus Energy bug: REDUCES crit rate instead of raising it
## Memory Conventions
| Prefix | Purpose | Example |
|--------|---------|---------|
| PKM:OBJECTIVE | Current goal | Get Parcel from Viridian Mart |
| PKM:MAP | Navigation knowledge | Viridian: mart is northeast |
| PKM:STRATEGY | Battle/team plans | Need Grass type before Misty |
| PKM:PROGRESS | Milestone tracker | Beat rival, heading to Viridian |
| PKM:STUCK | Stuck situations | Ledge at y=28 go right to bypass |
| PKM:TEAM | Team notes | Squirtle Lv6, Tackle + Tail Whip |
## Progression Milestones
- Choose starter
- Deliver Parcel from Viridian Mart, receive Pokedex
- Boulder Badge — Brock (Rock) → use Water/Grass
- Cascade Badge — Misty (Water) → use Grass/Electric
- Thunder Badge — Lt. Surge (Electric) → use Ground
- Rainbow Badge — Erika (Grass) → use Fire/Ice/Flying
- Soul Badge — Koga (Poison) → use Ground/Psychic
- Marsh Badge — Sabrina (Psychic) → hardest gym
- Volcano Badge — Blaine (Fire) → use Water/Ground
- Earth Badge — Giovanni (Ground) → use Water/Grass/Ice
- Elite Four → Champion!
## Stopping Play
1. Save the game with a descriptive name via POST /save
2. Update memory with PKM:PROGRESS
3. Tell user: "Game saved as [name]! Say 'play pokemon' to resume."
4. Kill the server and tunnel background processes
## Pitfalls
- NEVER download or provide ROM files
- Do NOT send more than 4-5 actions without checking vision
- Always sidestep after exiting buildings before going north
- Always add wait_60 x2-3 after door/stair warps
- Dialog detection via RAM is unreliable — verify with screenshots
- Save BEFORE risky encounters
- The tunnel URL changes each time you restart it