initial commit
This commit is contained in:
@@ -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 ≥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 113–139 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"
|
||||
}
|
||||
Reference in New Issue
Block a user