initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,40 @@
# Clean Reset: Remove All User Modifications
When Sunshine was modified with workarounds (uinput shim, input bridge, custom config) and you want to go back to a completely clean package state — removing everything created by the user, keeping only what the .deb package installed.
## What to remove
```bash
# Systemd override (all user-added drop-ins)
rm -rf ~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/
# Stale manual service file (user-created, not from .deb package)
rm -f ~/.config/systemd/user/sunshine.service
# Workaround files
rm -f ~/.config/sunshine/input_bridge.py
rm -f ~/.config/sunshine/uinput_fix.so
rm -f ~/.config/sunshine/sunshine_state.json.bak
# Custom config (let package defaults regenerate)
rm -f ~/.config/sunshine/sunshine.conf
rm -f ~/.config/sunshine/apps.json
# Reload systemd
systemctl --user daemon-reload
```
## What to keep
- `~/.config/sunshine/credentials/` — pairing certificates, needed for Moonlight clients to reconnect
- `~/.config/sunshine/sunshine.log` — runtime log artifact, harmless
## After reset
Sunshine regenerates `sunshine.conf` and `apps.json` with package defaults on next start. The web UI at `https://localhost:47990` prompts for first-time username/password setup. All previously paired Moonlight clients must be re-paired.
## Ubuntu 26.04 note
Kernel 7.0.0 has broken uinput. A clean install means NO input workarounds — keyboard/mouse injection will fail silently. The uinput diagnostic (see `scripts/diagnose-uinput.sh`) returns -25. To get input working, you must re-deploy either:
- The LD_PRELOAD shim (`scripts/uinput-shim.c`) — but this may not intercept Inputtino's internal code path
- The xdotool input bridge (`scripts/sunshine-input-bridge.py`) — reliable fallback
@@ -0,0 +1,35 @@
# Moonlight Client-Side Firewall/Routing Diagnostic Prompt
Deploy this prompt to a Hermes agent running on the Moonlight CLIENT PC
when the Sunshine server is confirmed working (paired, ports listening)
but Moonlight throws `Error 10060` (RTSP handshake timeout) on stream launch.
The prompt assumes:
- Sunshine server IP is known (replace `<SERVER_IP>` below)
- Sunshine is confirmed listening on TCP 48010, 47984, 47989, 47990
- Pairing completed successfully (client cert in sunshine_state.json)
- The block is on the client side (Windows Firewall, Tailscale routing, etc.)
---COPY BELOW THIS LINE---
Hey Hermes, I'm trying to stream from my Sunshine server (IP: <SERVER_IP>) to
this PC using Moonlight, but I keep getting "RTSP Handshake failed: Error 10060".
The host agent confirmed that the PC is successfully paired, and the Sunshine
server is running and listening on all required ports (TCP 47984, 47989, 47990,
48010). However, when Moonlight attempts to start the stream, the RTSP handshake
on TCP 48010 is timing out before it ever reaches the server logs.
Please perform a full network and firewall diagnostic on this client PC to fix
the connection to the host:
1. Verify if Windows Defender / Windows Firewall is blocking Moonlight.exe or
the required ports (TCP 48010, UDP 48000, 48010) and add allow rules for them.
2. Test network reachability to the host using
`Test-NetConnection -ComputerName <SERVER_IP> -Port 48010`.
3. Check if there are any stale UDP/TCP connections held open in TIME_WAIT state
that might be blocking Moonlight.
4. If Tailscale is installed on this PC, check if it's interfering with local
LAN routing to <SERVER_IP>.
Fix any firewall or routing blockers you find so Moonlight can successfully
complete the handshake.
@@ -0,0 +1,82 @@
# Black Screen Diagnostic Workflow
When Sunshine, x11vnc, or any display capture tool shows a black screen, use this
workflow to determine whether it's a capture/encoding problem or the GPU is
genuinely outputting black pixels.
## Step 1: Take a hardware screenshot
```bash
DISPLAY=:0 xwd -root -out /tmp/screen.xwd
convert /tmp/screen.xwd /tmp/screen.png
ls -la /tmp/screen.png
```
If the PNG is tiny (~1.3KB regardless of resolution), the framebuffer is all
black. Verify with Python:
```python
from PIL import Image
img = Image.open('/tmp/screen.png')
samples = [img.getpixel((x, y)) for y in range(0, img.height, 200)
for x in range(0, img.width, 200)]
unique = set(samples)
# {0} means completely black — GPU is not rendering
```
## Step 2: If framebuffer is black, check DRM connector state
```bash
for c in /sys/class/drm/card*-HDMI-*/; do
echo "$(basename $c): status=$(cat $c/status) enabled=$(cat $c/enabled) dpms=$(cat $c/dpms)"
done
```
Normal output:
```
card1-HDMI-A-1: status=connected enabled=enabled dpms=On
```
Problem output (DPMS lockup):
```
card1-HDMI-A-1: status=connected enabled=disabled dpms=Off
```
## Step 3: Cross-check with nvidia-settings
```bash
nvidia-settings -q EnabledDisplays -q ConnectedDisplays
```
Healthy: `EnabledDisplays` matches `ConnectedDisplays` bitmask.
Locked: `EnabledDisplays = 0x00000000` while `ConnectedDisplays` shows the port.
## Step 4: Cross-check X11 state
```bash
DISPLAY=:0 xrandr | grep "HDMI-0"
DISPLAY=:0 xset q | grep -i "monitor\|dpms"
```
X11 may report "Monitor is On" and show modes available even when the kernel
connector is disabled. This is a driver-level mismatch — X11's DPMS state
doesn't reflect the kernel's actual state.
## Resolution
If `enabled=disabled` and `dpms=Off` at the kernel level:
- `xset dpms force on` and `xrandr --output HDMI-0 --auto` will NOT fix it
- Writing to `/sys/class/drm/card*-HDMI-*/dpms` returns Permission denied even
as root (NVIDIA driver read-only)
- The GPU display controller needs a full power cycle
- Only a reboot reliably fixes this
After reboot, X11 authorization is broken (LightDM's Xauthority is invalid for
the user). See the main SKILL.md DPMS lockup pitfall for the Xauth merge fix.
## If framebuffer is NOT black (screenshot shows desktop)
The problem is in the capture/streaming layer:
- Sunshine: check `output_name`, `capture` method, `hw_cursor`
- x11vnc: add `-noxdamage`, use `novnc_proxy` launcher
- noVNC: check UFW allows the port, browser console for WebSocket errors
@@ -0,0 +1,189 @@
# Headless Server Setup for Game Streaming
Full pipeline for running Sunshine + games on a headless Linux server (no monitor attached).
## Required components
1. **HDMI dummy plug** ($5-10) — plugs into GPU HDMI port, emulates a 4K display. Without it, NVIDIA GPUs run in low-power mode with reduced clocks and may refuse to render at full speed. Essential for headless setups.
**EDID reality check:** Many "4K/120" dummy plugs (e.g., FUEARAN) advertise 4K@120Hz on the label but their EDID caps 4K at 60Hz. Actual high-refresh options are 1440p@120Hz and 1080p@120Hz. Check with:
```bash
DISPLAY=:0 xrandr --query | grep -A 20 "^HDMI-0" | grep -E "^[ ]+[0-9]"
```
If 4K@120Hz is absent despite the label, the plug hardware doesn't support it — plan your game resolutions accordingly.
2. **Xorg** — needs to run headless pointing at the dummy display, and persist across reboots.
**Verify the dummy display:**
```bash
for conn in /sys/class/drm/card0-*/status; do echo "$conn: $(cat $conn)"; done
# Look for "connected" — that's your dummy plug
cat /sys/class/drm/card0-HDMI-A-1/modes | head -1 # should show 3840x2160 or similar
```
**Start Xorg (manual test):**
```bash
sudo Xorg :0 -ac -nolisten tcp vt7 &
sleep 2
DISPLAY=:0 xdpyinfo | grep dimensions # verify
```
**Xorg persistence (systemd system service):**
Xorg needs root for VT access on headless servers. Create a system service so it survives reboots:
```ini
# /etc/systemd/system/xorg-headless.service
[Unit]
Description=Xorg display server for headless game streaming
After=multi-user.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/Xorg :0 -ac -nolisten tcp vt7
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
```
Then: `sudo systemctl daemon-reload && sudo systemctl enable --now xorg-headless`
3. **Sunshine** — game streaming server. See main SKILL.md for install.
4. **Game launchers:**
- **Steam (+ Proton):** Requires 32-bit architecture on Ubuntu 26.04+.
```bash
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y steam-installer
```
**Headless blocker — zenity license dialog:** The `steam-installer` package installs a `/usr/games/steam` wrapper script that, on first run, pops up a zenity license dialog (`--ok-label=Install --cancel-label=Cancel`). On a headless display, nobody can click "Install", so the Steam runtime never downloads. **Do not attempt debconf preseeding** — the zenity check (lines 113139 of `/usr/games/steam`) is hardcoded and ignores debconf.
**Workaround — bootstrap Steam manually:** Run the `scripts/setup-headless-steam.sh` script included in this skill. It automatically detects the required version, downloads the bootstrap tarball, unpacks the embedded runtime, and links the directories so the GUI license dialog is bypassed.
After these steps, `DISPLAY=:0 steam` launches without any dialog. First run downloads the full Steam client (~200-500 MB) and displays a self-updating progress bar (zenity `--progress --auto-close`). The client goes through a multi-stage bootstrap: update download → restart → runtime unpack → restart → login screen. This takes 3-5 minutes on a fast connection. Steam must be running and logged in for Sunshine's "Steam Big Picture" app to work.
Proton is built into Steam — enable in Settings → Steam Play. Handles ~85% of Windows games.
- **Lutris:** `sudo apt install lutris`. For non-Steam games: GOG, Epic Games Store, Battle.net, emulators.
## Pipeline
```
Headless server (no monitor)
↳ HDMI dummy plug → GPU sees "4K display" → full clocks enabled
↳ Xorg virtual desktop on dummy display
↳ Game launches (Steam/Lutris)
↳ Sunshine captures X11 display via NVENC
↳ Moonlight on client device streams it
```
## GPU requirements
- **NVIDIA GPU with NVENC** (Turing or newer: GTX 1650 Super+, RTX 20-series+, RTX 30-series+, RTX 40-series+)
- Consumer cards limited to 3 concurrent NVENC sessions
- NVENC is dedicated silicon — negligible FPS hit during encoding
## Linux game compatibility
| Type | How | Compatibility |
|---|---|---|
| Native Linux games | Steam → Install → Play | 100% |
| Windows games (Proton) | Steam → Enable Steam Play → Install | ~85% works out of box, ~10% needs tweaks, ~5% broken |
| Anti-cheat games | Valorant, CoD, Fortnite, Destiny 2 | 0% — kernel-level anti-cheat incompatible with Proton |
| Non-Steam games | Lutris (GOG, Epic, Battle.net) | Varies, mostly good |
ProtonDB (protondb.com) is the definitive compatibility reference for any specific game.
## Audio setup (headless)
Headless servers have no physical speakers and often no audio system at all. Sunshine needs an audio sink to capture game audio and stream it to Moonlight.
### Install PipeWire
```bash
sudo apt install -y pipewire pipewire-pulse wireplumber pulseaudio-utils
```
This gives you a PulseAudio-compatible interface over PipeWire 1.6+. The `auto_null` (Dummy Output) sink is created automatically — it accepts audio from games and makes it available for Sunshine capture without needing physical hardware.
### User must be in `audio` group
```bash
sudo usermod -a -G audio $USER
# Requires re-login or systemctl --user daemon-reexec to take effect
```
Without the audio group, `/dev/snd/*` devices are permission-denied and PipeWire can't enumerate ALSA hardware. The `auto_null` sink still works for capture, but NVIDIA HDMI audio (from the dummy plug's EDID) won't appear as a proper hardware sink until the group is active.
### Sunshine config for headless dummy plug
**Required config — tell Sunshine which display to capture:**
Without `output_name`, Sunshine defaults to display index 0 (DP-0), which may be disconnected even with a dummy plug in HDMI-0. The log shows:
```
Configuring selected display (0) to stream
Couldn't get requested display info, defaulting to recording entire virtual desktop
```
Fix: add to `~/.config/sunshine/sunshine.conf`:
```
output_name = 2
```
⚠️ **IMPORTANT:** `output_name` takes a **numeric display index**, NOT the connector name string. Passing `HDMI-0` as the value causes Sunshine to fail with `Could not stream display number, there are only [8] displays.`
Determine the correct index from Sunshine's own log output:
```bash
grep "Detected display" ~/.config/sunshine/sunshine.log
# Detected display: DP-0 (id: 0)DP-0 connected: false
# Detected display: DP-1 (id: 1)DP-1 connected: false
# Detected display: HDMI-0 (id: 2)HDMI-0 connected: true ← use output_name = 2
# Detected display: DP-2 (id: 3)DP-2 connected: false
```
The `(id: N)` value is the zero-based display index. Only one display should be `connected: true` on a headless system with a single dummy plug. After setting, verify Sunshine captures the correct display:
```
Streaming display: HDMI-0 with res 3840x2160 offset by 0x0
```
This log line confirms Sunshine is streaming the right display. Required alongside `capture = x11` for NVIDIA headless setups.
**Do NOT set audio sink config:**
`audio_sink` or `virtual_sink` in `sunshine.conf` causes an immediate fatal crash (`pa_simple_new() failed: Invalid argument`) on Sunshine v2026+. Let Sunshine auto-detect PipeWire instead.
**CRITICAL FIX FOR CRASHING STREAMS:** If Sunshine still crashes with `Invalid argument` upon stream start, PipeWire is rejecting Sunshine's attempt to dynamically create virtual surround sinks. You must pre-create them so Sunshine finds them instead of crashing:
```bash
pactl load-module module-null-sink sink_name=sink-sunshine-stereo
pactl load-module module-null-sink sink_name=sink-sunshine-surround51 channels=6 channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right
pactl load-module module-null-sink sink_name=sink-sunshine-surround71 channels=8 channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right
```
*(Run these commands before Sunshine starts, e.g. in a wrapper script or systemd ExecStartPre).*
### Verify audio
```bash
pactl info | grep "Default Sink" # should show auto_null
pactl list short sinks # should show auto_null, SUSPENDED
journalctl --user -u sunshine | grep -i audio # should have no warnings/errors
```
The earlier "Warning: There will be no audio" in sunshine logs should be gone after this config.
### Post-reboot: NVIDIA HDMI audio
After a reboot (once the `audio` group takes effect), WirePlumber discovers the NVIDIA HDMI audio device exposed by the dummy plug. An additional sink like `alsa_output.pci-0000_01_00.1.hdmi-stereo` appears. Either sink works — games output to the default sink and Sunshine captures whichever one is configured. The real HDMI sink has the advantage of proper EDID-negotiated sample rates (up to 96kHz on the FUERAN dummy plug).
## VRAM cohabitation strategy
When the server also runs LLM inference:
| Scenario | Solution |
|---|---|
| Model always loaded, game occasionally | Socket-activated llama-server (see llama-cpp skill, deployment-patterns.md). Model unloads when idle, 3-4s warm restart. |
| Gaming always, model occasionally | Stop llama-server before gaming. Start manually when needed. |
| Both simultaneously | Only works if VRAM fits both — e.g., 24 GB card with a 7B model (~4.7 GB) leaves ~19 GB for games. |
## Testing before buying hardware
- Verify `nvidia-smi` shows NVENC support (`nvidia-smi -q | grep -A5 Encoder`)
- Run Sunshine and confirm web UI at `https://localhost:47990`
- Install Moonlight on any device and verify auto-discovery works
@@ -0,0 +1,260 @@
# Headless Sunshine Troubleshooting
Real-world issues and fixes from a headless Sunshine + Moonlight setup on Ubuntu 26.04 with NVIDIA RTX 2080 Ti (driver 580.159.03).
## Symptom: "Video failed to find working encoder" / "Unable to find display or encoder"
Sunshine starts but immediately exits. Logs show it trying vulkan → vaapi → software, all failing, never reaching NVENC.
### Root cause: cascade of silent failures
Several things can cause this, often in combination:
1. **User not in `video` and `render` groups** — can't access `/dev/dri/card0` or `/dev/dri/renderD128`
2. **Systemd service missing `DISPLAY`** — X11 capture can't connect to Xorg
3. **CUDA/driver mismatch** — NVFBC capture fails, and if X11 fallback is also broken, nothing works
### Debugging technique: run sunshine directly
Systemd journal logs are terse — "nvenc failed" with no detail. Run sunshine from the terminal to see the full verbose output:
```bash
systemctl --user stop app-dev.lizardbyte.app.Sunshine
DISPLAY=:0 /usr/bin/sunshine ~/.config/sunshine/sunshine.conf
```
The verbose output shows every CUDA symbol load, every NVENC initialization call, and the exact failure point. In the working case, you'll see:
```
Found H.264 encoder: h264_nvenc [nvenc]
Found HEVC encoder: hevc_nvenc [nvenc]
```
**Note:** When checking DRM device permissions, use `stat /dev/dri/card0` — do NOT `cat` or `dd` from DRM devices as they can hang the shell when read without a proper DRM master context.
## Fix 1: Group permissions
```bash
# Check current groups
groups $USER | grep -oP '(video|render|input|audio)'
# Add missing groups
sudo usermod -a -G video,render,input,audio $USER
# Refresh groups in current session (no re-login needed for systemd user)
systemctl --user daemon-reexec
# Fix uinput for gamepad support
sudo chown $USER:input /dev/uinput
```
Without these:
- `/dev/dri/card0` → "Permission denied" → no KMS capture
- `/dev/dri/renderD128` → can't initialize NVENC CUDA context
- `/dev/uinput` → "Gamepad ds5 is disabled due to Permission denied"
- `/dev/snd/*` → PipeWire can't enumerate ALSA hardware (auto_null sink still works for capture, but NVIDIA HDMI audio sink won't appear until after reboot)
## Fix 1b: Give Sunshine effective `cap_sys_admin` for input injection
Sunshine has `cap_sys_admin=pe` (permitted only) by default and **drops it on startup** (log: `drop_elevated_privileges succeeded in dropping capabilities`). After dropping, `/dev/uinput` access may fail silently even when the user is in the `input` group — no visible error, just zero input events in the log.
```bash
# Check current capabilities
getcap /usr/bin/sunshine
# cap_sys_admin,cap_sys_nice=p ← 'p' = permitted only, dropped on startup
# Set to effective (keeps the capability active at runtime)
sudo setcap cap_sys_admin=ep /usr/bin/sunshine
getcap /usr/bin/sunshine
# cap_sys_admin=ep ← 'e' = effective, won't be dropped
```
Without `cap_sys_admin=ep`, Sunshine processes zero input events from Moonlight — the log shows stream activity but no `XTEST`, `keyboard`, or `mouse` entries. With it, you'll see `XTEST relative movement` and key events in the log as the client sends input.
### Check if uinput is available
On modern kernels (Ubuntu 7.0+), `uinput` is often built-in rather than a loadable module:
```bash
# Check if uinput is built-in or a module
modinfo uinput | head -3
# filename: (builtin) ← no need to modprobe, already available
# OR: filename: /lib/modules/.../uinput.ko ← loadable, may need modprobe
# Verify the device node exists with correct permissions
stat /dev/uinput
# crw-rw---- 1 root input 10, 223 ...
# Ray must be in the input group to access it
groups $USER | grep input
```
Do NOT waste time running `modprobe uinput` if it's built-in — it will silently succeed without loading anything. The key checks are: device node exists, user in `input` group, and `cap_sys_admin=ep` on the binary.
## Fix 2: Systemd DISPLAY override
The sunshine systemd service doesn't know about manually-started Xorg sessions. Create a drop-in:
```bash
mkdir -p ~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d
cat > ~/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/override.conf << 'EOF'
[Service]
Environment=DISPLAY=:0
# Clear the 5-second sleep delay (was waiting for desktop session)
ExecStartPre=
LimitNICE=-10
EOF
systemctl --user daemon-reload
systemctl --user restart app-dev.lizardbyte.app.Sunshine
```
Without this, X11 capture fails with "Unable to initialize capture method."
**Pitfall: Do NOT add `SupplementaryGroups=input` to this override.** If the user's login session doesn't have the `input` group (because group changes need a fresh login), systemd will fail to start the service with `status=216/GROUP` — "Changing group credentials failed: Operation not permitted." Adding supplementary groups only works if the user manager itself has those groups. For gamepad support, fix uinput permissions instead (see Fix 1), or wait until next login for group changes to propagate.
## Fix 2b: `output_name` must match the connected display
Sunshine's `output_name` is a zero-based index into the list of detected displays. If it points to a disconnected output, streaming will produce a black screen or fall back to "recording entire virtual desktop." Check which displays are connected:
```bash
# Method 1: xrandr
DISPLAY=:0 xrandr --listmonitors
# Shows: Monitors: 1
# 0: +*HDMI-0 3840/697x2160/392+0+0 HDMI-0
# The monitor name is HDMI-0, but output_name needs the INDEX (0 in this case)
# Method 2: Check sunshine log
grep "Detected display" ~/.config/sunshine/sunshine.log
# Shows: Detected display: DP-0 (id: 0)DP-0 connected: false
# Detected display: HDMI-0 (id: 2)HDMI-0 connected: true
# → Set output_name = 2
```
Sunshine logs each display as it detects them with `(id: N)` and `connected: true/false`. Match the index to the one that's `connected: true`. On headless systems with a dummy plug, only one display will be connected. Setting the wrong index produces: `Warning: Couldn't get requested display info, defaulting to recording entire virtual desktop.`
## Fix 3: CUDA/driver version mismatch (NVFBC → X11 fallback)
Sunshine v2026+ bundles CUDA 13.1.1 which requires NVIDIA driver ≥590.48.01. On systems with older drivers (e.g., 580.159.03), NVFBC capture fails because the CUDA runtime can't initialize. However, NVENC encoding works fine — the NVIDIA encode library (`libnvidia-encode.so.1`) is driver-versioned separately and doesn't need CUDA 13.
**Solution:** Force X11 capture instead of NVFBC:
```ini
# ~/.config/sunshine/sunshine.conf
capture = x11
encoder = nvenc
adapter_name = /dev/dri/renderD128
output_name = 0
```
X11 capture + NVENC works reliably. NVFBC is faster (direct GPU memory) but requires compatible CUDA. KMS capture also works but needs `video` group and `cap_sys_admin`.
### Verifying NVENC works independently
Even when Sunshine's NVFBC path fails, ffmpeg can confirm NVENC is functional:
```bash
DISPLAY=:0 ffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i :0.0+0,0 \
-c:v h264_nvenc -preset p1 -t 3 -f null /dev/null
```
If this works (shows encoding fps), NVENC is fine — the problem is Sunshine's capture/init path.
## Fix 4: CSRF protection for remote web UI access
Sunshine v2026 added CSRF protection. Accessing the web UI from anything other than localhost is blocked. Add to sunshine.conf:
```ini
origin_pin_allowed = pc
csrf_allowed_origins = https://100.93.253.36:47990,https://192.168.50.98:47990
```
Both `origin_pin_allowed = pc` and `csrf_allowed_origins` are required for LAN access. `origin_pin_allowed` relaxes the PIN/credential creation check for LAN origins; `csrf_allowed_origins` whitelists the specific origin URLs for CSRF token validation. Without `origin_pin_allowed`, credential creation is blocked even with the CSRF origin whitelisted.
Replace the IPs with your server's LAN and Tailscale IPs. Multiple origins are comma-separated. Restart sunshine after changing.
## Fix 5: Start-limit hit from repeated failures
After too many restart attempts, systemd blocks further starts:
```
app-dev.lizardbyte.app.Sunshine.service: Start request repeated too quickly.
Failed with result 'start-limit-hit'.
```
Reset with:
```bash
systemctl --user reset-failed app-dev.lizardbyte.app.Sunshine
systemctl --user start app-dev.lizardbyte.app.Sunshine
```
## Working sunshine.conf (headless X11 + NVENC)
```ini
sunshine_name = rayserver
capture = x11
encoder = nvenc
# adapter_name should NOT be set for NVENC — Sunshine auto-detects the GPU
# adapter_name = auto
output_name = 0
min_log_level = info
upnp = disabled
origin_pin_allowed = pc
csrf_allowed_origins = https://100.93.253.36:47990
audio_sink = auto_null
virtual_sink = auto_null
```
**IMPORTANT:** `output_name` is the zero-based index of the connected display. It varies per system. Check with `grep "Detected display" ~/.config/sunshine/sunshine.log` after starting Sunshine — use the index of the display marked `connected: true`. See Fix 2b above for details.
## Fix 6: Root process picks NvFBC (no input forwarding)
When Sunshine runs as root (via `sudo systemd-run`, a system-level service, or direct `sudo sunshine`), it auto-selects NvFBC capture because root has direct GPU access. NvFBC reads the framebuffer from GPU memory — it bypasses X11 entirely. Result: video streams fine, audio works, but **zero input events reach the desktop**. Sunshine logs show no `XTEST`, `keyboard`, or `mouse` activity at all.
The fix is simple — don't run as root. Use the user-level systemd service:
```bash
# STOP: never do this
sudo systemd-run --unit=sunshine-streamer sunshine # → NvFBC, no input
# DO: always use the user service
systemctl --user start app-dev.lizardbyte.app.Sunshine
# Verify it's running as the right user
ps aux | grep sunshine | grep -v grep
# Should show: ray ... /usr/bin/sunshine ← NOT root
```
If you accidentally started as root, kill it and restart:
```bash
sudo pkill -f "sunshine" # kills all sunshine processes (including root one)
sleep 2
systemctl --user start app-dev.lizardbyte.app.Sunshine
```
## Verification checklist
```bash
# 1. Xorg running on dummy display
DISPLAY=:0 xdpyinfo | grep dimensions # should show 3840x2160
# 2. Groups correct
groups $USER | grep -E 'video|render|input|audio'
# 3. NVENC encoders detected by ffmpeg
ffmpeg -encoders 2>/dev/null | grep nvenc
# 4. Sunshine running
systemctl --user status app-dev.lizardbyte.app.Sunshine
# 5. Web UI accessible
curl -sk https://localhost:47990/ | head -5
# 6. Avahi advertising
avahi-browse -at 2>/dev/null | grep sunshine
# 7. Audio running (no warnings in sunshine logs)
pactl info | grep "Default Sink"
journalctl --user -u app-dev.lizardbyte.app.Sunshine --no-pager -n 30 | grep -i "no audio" # should return nothing
```
@@ -0,0 +1,74 @@
# HP OMEN 30L (8703 motherboard) — GPU Passthrough & Game Streaming
Hardware-specific findings from a session analyzing game streaming feasibility on an HP OMEN 30L desktop with motherboard model 8703.
## Hardware
- **Motherboard:** HP 8703 (HP OMEN 30L prebuilt)
- **CPU:** Intel Core i7-10700K (Comet Lake, 10th gen) — has Intel UHD Graphics 630
- **GPU:** NVIDIA RTX 2080 Ti (11GB VRAM, Turing, NVENC H.264/HEVC/AV1)
- **RAM:** DDR4-3200, 4 DIMM slots
- **OS:** Ubuntu Server, headless (no monitor, no display server)
## iGPU Status: DISABLED
The i7-10700K's UHD Graphics 630 does NOT appear in `lspci` or `/sys/class/drm/`. HP OMEN 30L prebuilts disable the iGPU in BIOS when a discrete GPU is installed. The 8703 motherboard has no physical video outputs for the iGPU even if enabled. The only detected GPU is the RTX 2080 Ti at `card0`.
## IOMMU Group Layout
```
Group 1:
- NVIDIA TU102 [GeForce RTX 2080 Ti] (10de:1e07)
- NVIDIA TU102 HD Audio Controller (10de:10f7)
- NVIDIA TU102 USB 3.1 Host Controller (10de:1ad6)
- NVIDIA TU102 USB Type-C UCSI Controller (10de:1ad7)
```
The GPU is alone in its IOMMU group — no PCIe bridge, no root port, no other devices. This is ideal for VFIO passthrough. The USB controller on the GPU is the Type-C VirtualLink port — you'd pass it through with the GPU but it's rarely used.
## GPU Features
- NVENC: H.264, HEVC, AV1 (Turing encoder — excellent quality, ~x264 medium at 1/10th latency)
- 3 concurrent NVENC sessions (consumer cap, sufficient for single-stream gaming)
- 11 GB VRAM
## Simultaneous Windows + Linux Options
Without a second GPU or enabled iGPU, here are the real options:
| Option | Cost | Windows gaming | Linux GPU | Complexity |
|---|---|---|---|---|
| **Second GPU** (GT 1030 / GTX 1050) | ~$40-80 | Yes (2080 Ti passthrough) | Yes (second GPU) | Medium |
| **Single GPU passthrough** | Free | Yes | None | High |
| **Proton only** | Free | ~85% of games | Yes (native) | Low |
| **Dual boot** | Free | Yes (native) | N/A when in Windows | Low |
### Second GPU (recommended)
Cheap low-power GPU for Linux host (GT 1030 ~30W, $40-80 used). RTX 2080 Ti passed to Windows VM. Linux keeps dedicated GPU for Ollama/LLM, Docker GPU, Immich ML. Both OSes run simultaneously with dedicated hardware.
The HP 8703 has at least one PCIe x16 slot (occupied by 2080 Ti) and one x4 slot (free). Verify physical space and PSU headroom — a GT 1030 draws 30W max, negligible.
### Single GPU passthrough
Bind RTX 2080 Ti to VFIO at boot. Linux host loses ALL GPU: no Ollama, no Immich ML, no NVENC. When Windows VM shuts down, GPU can be rebound to nvidia driver. Fragile — reboot is cleaner. Use only if you're comfortable with kernel parameters and VFIO scripts.
### Proton only
Zero cost, zero complexity. Sunshine + Moonlight on Linux. ~85% of Windows games work via Proton. Kernel anti-cheat games (Valorant, CoD, Fortnite, Destiny 2) are 0% compatible — no workaround.
## VRAM Cohabitation
RTX 2080 Ti: 11 GB VRAM.
- qwen2.5:14b (Q4_K_M): ~9 GB
- AAA game (modern): 4-8 GB
- They don't fit simultaneously. Must stop Ollama before gaming, restart after.
- Socket-activated llama-server: stop model, warm-restart in 3-4 seconds when gaming ends.
## Headless Setup Requirements
- HDMI dummy plug (~$5-10) — essential for GPU to run at full clocks
- Xorg virtual desktop on dummy display
- Sunshine captures X11 via NVENC
- Steam + Proton for Windows games
- Lutris for non-Steam games (GOG, Epic, Battle.net)
@@ -0,0 +1,84 @@
# Kernel 7.0 uinput regression: UI_DEV_SETUP required
## Summary
Kernel 7.0.0-22-generic broke the legacy uinput creation sequence.
Sunshine's Inputtino and libevdev both use the old method and fail silently.
## Pre-diagnostic: is uinput built-in or a module?
Before running the C or Python diagnostic below, check if uinput is a loadable module or built into the kernel:
```bash
modinfo uinput 2>&1 | grep filename
# "filename: (builtin)" → uinput is compiled into the kernel
# "filename: /lib/modules/..." → uinput is a loadable module
grep CONFIG_INPUT_UINPUT /boot/config-$(uname -r)
# "CONFIG_INPUT_UINPUT=y" → built-in (no modprobe needed)
# "CONFIG_INPUT_UINPUT=m" → module (modprobe uinput to load)
```
**If built-in:** `modprobe uinput` will succeed silently but `lsmod | grep uinput` will show nothing — this is expected. The device node at `/dev/uinput` exists regardless. Permissions (user in `input` group) are what matter. Skip any module-loading troubleshooting steps.
**If a module:** `sudo modprobe uinput` and verify with `lsmod | grep uinput`. To persist across reboots: `echo "uinput" | sudo tee /etc/modules-load.d/uinput.conf`.
## Root cause
The uinput subsystem on kernel 7.0+ REQUIRES `UI_DEV_SETUP` ioctl before
`UI_DEV_CREATE`. The old method (writing `struct uinput_user_dev` via write()
or calling `UI_DEV_CREATE` without prior setup) is rejected with EINVAL (-22).
Sunshine v2026.516.143833 links against libevdev 1.13.6, which uses the old method.
Inputtino (Sunshine's actual input backend, not libevdev) also uses the old method.
## Diagnostic (C test)
```c
#include <linux/uinput.h>
int fd = open("/dev/uinput", O_WRONLY);
ioctl(fd, UI_DEV_CREATE); // FAILS: EINVAL on kernel 7.0
// Correct kernel 7.0 sequence:
struct uinput_setup usetup;
memset(&usetup, 0, sizeof(usetup));
usetup.id.bustype = BUS_USB;
strcpy(usetup.name, "test");
ioctl(fd, UI_DEV_SETUP, &usetup); // NEW: Required on 7.0+
ioctl(fd, UI_DEV_CREATE); // SUCCESS
```
## Diagnostic (Python test)
```python
import ctypes
libevdev = ctypes.CDLL('libevdev.so.2')
dev = libevdev.libevdev_new()
libevdev.libevdev_set_name(dev, b'test')
libevdev.libevdev_enable_event_type(dev, 1)
uifd = ctypes.c_int(-1)
ret = libevdev.libevdev_uinput_create_from_device(dev, 3, ctypes.byref(uifd))
# Returns -25 (ENOTTY) on kernel 7.0 — uinput broken
# Returns 0 on kernel 6.x — uinput works
```
## Why the LD_PRELOAD shim may not work
The shim (`scripts/uinput-shim.c`) intercepts `libevdev_uinput_create_from_device`.
However, Sunshine's PRIMARY input backend is **Inputtino** (subproject inside Sunshine
binary), not libevdev. Inputtino creates keyboard/mouse devices through a separate
code path that may not call through libevdev's public API. If the shim is deployed
and input still doesn't work, Inputtino is bypassing it.
## Proven workarounds
1. **xdotool bridge** (`scripts/sunshine-input-bridge.py`) — tails Sunshine log,
injects via xdotool XTEST. Works on any kernel.
2. **Steam Remote Play** (`templates/remote_play.vdf`) — different protocol,
no uinput involved.
3. **Boot kernel 6.x** if available.
## Kernel version
Tested broken: 7.0.0-22-generic (Ubuntu 26.04)
Tested working: Linux 6.x series
@@ -0,0 +1,61 @@
# Locked Dummy Plug — BadMatch on xrandr Mode Switch
## Symptom
All `xrandr --output HDMI-0 --mode <res>` commands fail with:
```
xrandr: Configure crtc 0 failed
X Error of failed request: BadMatch (invalid parameter attributes)
Major opcode of failed request: 140 (RANDR)
Minor opcode of failed request: 7 (RRSetScreenSize)
```
Exit code 1. Occurs for every resolution/refresh combination — 4K@60, 1440p@120, 1080p@120 — even though EDID advertises all of them.
## Root Cause
Cheap HDMI dummy plugs ("FUEARAN 4K/120" and similar unbranded adapters) use a fixed-resolution video signal generator chip that cannot be reconfigured via CRTC/PHY. The EDID is static and lists many modes but the hardware can only output its single native resolution (typically 3840x2160@60). xrandr accepts the mode switch request, passes it to the NVIDIA driver, which tries to reconfigure the CRTC, and the hardware returns `BadMatch`.
## Hardware Details
- **Make/Model:** FUEARAN 4K/120 (labeled as HDMI 2.1 dummy plug)
- **EDID tag:** `0046554552414e20344b2f313230` (decoded: "FUEARAN 4K/120")
- **Physical connection:** HDMI-0 (NVIDIA port, X11 connector)
- **NVIDIA GPU:** RTX 2080 Ti, driver 580.159.03
- **Xorg mode:** X11 (not Wayland, not KMS capture)
- **Reports available via xrandr:** 3840x2160@60*, 2560x1440@120, 1920x1080@120, etc.
- **Actually works:** 3840x2160 at whatever rate it powers up (60Hz via xrandr)
## Diagnosis
```bash
# Test any mode switch — if it fails, plug is locked
DISPLAY=:0 xrandr --output HDMI-0 --mode 1920x1080 2>&1
# Check current actual mode
DISPLAY=:0 xrandr | grep "*"
# Verify Sunshine captures it correctly despite the lock
grep "Streaming display:" ~/.config/sunshine/sunshine.log
# Should show: HDMI-0 with res 3840x2160 offset by 0x0
```
## Fix
The `output_name = 2` fix in sunshine.conf is the primary black-screen fix (ensures Sunshine captures HDMI-0, not a disconnected display). Since the dummy plug can't switch modes, set all app prep-cmds to no-ops:
```json
"prep-cmd": [
{
"do": "",
"undo": "setsid steam steam://close/bigpicture"
}
]
```
## Alternatives
1. **Better dummy plug:** "Headless Ghost" or "Dr. HDMI" series — these support actual mode switching via hardware.
2. **Custom modeline:** Generate with `cvt 1920 1080 120` and add via `xrandr --newmode` + `xrandr --addmode` — may fail if the hardware PHY truly cannot drive different timings.
3. **NVIDIA Virtual Display (NVFBC-based):** `sudo nvidia-xconfig --virtual=1920x1080 --mode=1920x1080` — only works with `capture = nvfbc` which is broken on driver 580+.
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Emergency Sunshine pairing bypass — extract client cert from log and inject into state file.
Sunshine v2026.516.143833 has a broken PIN validation pipeline. The /api/pin endpoint
deadlocks internally even when IP addresses match. This script:
1. Reads the most recent /pair request's clientcert from sunshine.log
2. Decodes the hex-encoded PEM cert
3. Injects it directly into sunshine_state.json as a pre-paired named_device
4. Restarts Sunshine
After running, Moonlight must be FULLY CLOSED AND REOPENED — it will see the pre-paired
cert and skip the PIN screen.
Usage:
python3 pairing-bypass-inject-cert.py [--restart]
Requirements:
- Root not needed (Sunshine runs as the same user)
- sunshine_state.json must be writable
- Moonlight must have sent at least one /pair request (visible in sunshine.log)
"""
import json
import re
import uuid
import sys
import os
import subprocess
import time
SUNSHINE_LOG = os.path.expanduser("~/.config/sunshine/sunshine.log")
SUNSHINE_STATE = os.path.expanduser("~/.config/sunshine/sunshine_state.json")
SUNSHINE_CONF = os.path.expanduser("~/.config/sunshine/sunshine.conf")
def extract_latest_cert():
with open(SUNSHINE_LOG, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
matches = re.findall(r'clientcert -- ([0-9a-fA-F]+)', content)
if not matches:
print("ERROR: No clientcert found in sunshine.log")
print("Make sure Moonlight has tried to pair (it should show a PIN screen)")
sys.exit(1)
hex_cert = matches[-1]
pem_cert = bytes.fromhex(hex_cert).decode("utf-8")
print(f"Extracted certificate ({len(pem_cert)} bytes)")
print(f"Preview: {pem_cert[:60]}...")
return pem_cert
def inject_cert(pem_cert, client_name="RE-PC"):
try:
with open(SUNSHINE_STATE, "r") as f:
state = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
state = {}
if "root" not in state:
state["root"] = {"uniqueid": "0123456789ABCDEF", "named_devices": []}
if "named_devices" not in state["root"]:
state["root"]["named_devices"] = []
# Check if client already exists — update cert
updated = False
for dev in state["root"]["named_devices"]:
if dev.get("name") == client_name:
dev["cert"] = pem_cert
dev["enabled"] = "true"
updated = True
print(f"Updated existing client '{client_name}'")
break
if not updated:
state["root"]["named_devices"].append({
"name": client_name,
"cert": pem_cert,
"uuid": str(uuid.uuid4()).upper(),
"enabled": "true"
})
print(f"Added new client '{client_name}'")
# Ensure origin_pin_allowed = pc for easier future pairing
try:
with open(SUNSHINE_CONF, "r") as f:
conf = f.read()
if "origin_pin_allowed" not in conf:
with open(SUNSHINE_CONF, "a") as f:
f.write("\norigin_pin_allowed = pc\n")
print("Added origin_pin_allowed = pc to sunshine.conf")
except FileNotFoundError:
pass
with open(SUNSHINE_STATE, "w") as f:
json.dump(state, f, indent=4)
print(f"Injected into {SUNSHINE_STATE}")
def restart_sunshine():
print("Restarting Sunshine...")
subprocess.run(["systemctl", "--user", "restart", "sunshine"], check=False)
time.sleep(3)
result = subprocess.run(
["systemctl", "--user", "status", "sunshine", "--no-pager"],
capture_output=True, text=True
)
if "active (running)" in result.stdout:
print("Sunshine is running.")
else:
print("WARNING: Sunshine may not have started. Check: systemctl --user status sunshine")
def main():
restore = "--restart" in sys.argv
cert = extract_latest_cert()
inject_cert(cert, client_name=os.environ.get("MOONLIGHT_CLIENT_NAME", "RE-PC"))
if restore:
restart_sunshine()
else:
print("\nNow restart Sunshine: systemctl --user restart sunshine")
print("\nIMPORTANT: Fully CLOSE Moonlight on the client, then reopen it.")
print("It should connect directly to apps without asking for a PIN.")
if __name__ == "__main__":
main()
@@ -0,0 +1,77 @@
# Quick Browser-Based Remote Desktop (x11vnc + noVNC)
Lightweight alternative to Sunshine/Moonlight when you need to view the desktop in a laptop browser — no client install, no pairing, zero config beyond apt packages. Useful for quick admin tasks, one-off desktop viewing, or when Moonlight isn't available on the client.
## When to use
- View the desktop from a laptop browser (Chrome, Firefox, Safari)
- No client software allowed/available on the remote machine
- Quick one-off access — don't need persistent pairing or low-latency game streaming
- User says "I need to see my desktop in my browser"
## Setup (one-time)
```bash
sudo apt-get install -y x11vnc websockify novnc
```
## Launch (run each time)
```bash
# 1. Kill any stale processes from previous runs
pkill x11vnc 2>/dev/null
pkill websockify 2>/dev/null
sleep 1
# 2. Share the existing display via VNC (no password, shared)
# -noxdamage fixes black screen with noVNC
# -scale 1920x1080 downsamples 4K for browser performance
x11vnc -display :0 -forever -shared -nopw -noxdamage -scale 1920x1080 &
# 3. Use the official noVNC launcher (handles websockify bridge internally)
/usr/share/novnc/utils/novnc_proxy --listen 6800 --vnc localhost:5900 --web /usr/share/novnc &
```
**IMPORTANT:** Using the official `novnc_proxy` launcher is preferred over raw `websockify`. It starts its own internal websockify, serves noVNC files, and handles the WebSocket handshake correctly. It serves `vnc.html` (the full noVNC client) at the root, not just `vnc_lite.html`.
## Verify
```bash
# x11vnc listening (port 5900)
ss -tlnp | grep 5900
# novnc_proxy listening (port 6800, 0.0.0.0 = accessible from LAN)
ss -tlnp | grep 6800
# noVNC serving
curl -s -o /dev/null -w "%{http_code}" http://localhost:6800/vnc.html
# Expect: 200
```
## Client URL
Open in laptop browser:
```
http://<server-ip>:6800/vnc.html
```
The `novnc_proxy` launcher auto-fills the WebSocket connection parameters — no query params needed. Example: `http://192.168.50.98:6800/vnc.html`
## Teardown
```bash
pkill x11vnc
pkill websockify
pkill -f novnc_proxy
```
## Pitfalls
- **UFW blocks port 6800 by default.** If the client browser says "site can't be reached," open the port: `sudo ufw allow 6800/tcp`.
- **Black screen in noVNC.** If noVNC connects but shows only black (no desktop), first check whether it's a VNC encoding issue or the GPU framebuffer itself is black. Take an X11 screenshot to distinguish: `DISPLAY=:0 xwd -root -out /tmp/screen.xwd && convert /tmp/screen.xwd /tmp/screen.png`. If the screenshot is all-black (tiny file, ~1.3KB for any resolution), the GPU isn't rendering anything — check DRM connector state: `for c in /sys/class/drm/card*-HDMI-*/; do echo "$(basename $c): $(cat $c/enabled) dpms=$(cat $c/dpms)"; done`. If `enabled=disabled` and `dpms=Off`, the display is in a kernel-level DPMS lockup and only a reboot will fix it (see `references/debug-black-screen.md` for full workflow). If the screenshot shows the desktop but noVNC shows black, it's a VNC encoding issue — add `-noxdamage` to x11vnc. XDamage can fail to track on 4K displays and with compositing enabled.
- **4K display kills browser performance.** Streaming a native 4K (3840x2160) framebuffer over noVNC is extremely heavy for the browser. Always downsample with `-scale 1920x1080` (or `-scale 1280x720` for slower connections). The x11vnc `-scale` flag handles this server-side.
- **Use novnc_proxy, not raw websockify.** The Debian `novnc` package includes `/usr/share/novnc/utils/novnc_proxy` — a wrapper script that manages websockify internally, serves the full `vnc.html` client, and handles auto-connect better than the raw `websockify --web=...` command.
- **websockify binds 0.0.0.0 by default** — accessible from LAN. No auth on this setup; only use on trusted networks.
- **x11vnc -nopw means NO password.** Anyone on the LAN can connect to port 5900 directly. Acceptable for quick local use; don't leave running.
- **Contrasts with Sunshine/Moonlight:** This is pure software encode (no NVENC), higher latency, higher CPU usage. Fine for desktop viewing; terrible for gaming.
@@ -0,0 +1,168 @@
# Troubleshooting Sunshine Crashes
Diagnostic workflow for when Sunshine is offline, won't start, or crashes.
## Quick debug sequence
```bash
# 1. Is it running?
systemctl --user status sunshine --no-pager
# 2. Any recent crashes in systemd journal?
journalctl --user -u sunshine --no-pager -n 50
# 3. Check Sunshine's own log (⚠ overwritten on restart — copy first!)
cat ~/.config/sunshine/sunshine.log | grep -i "fatal\|error\|hang\|terminate"
# 4. Stale processes?
ps aux | grep -i sunshine | grep -v grep
# 5. Port conflicts?
ss -tlnp | grep -E "47984|47989|47990|48010"
# 6. Restart
systemctl --user restart sunshine
```
## Common crash patterns
### "Hang detected! Session failed to terminate in 10 seconds"
**Symptom:** Sunshine dies after a Moonlight client connects or disconnects. Log shows:
```
Fatal: Hang detected! Session failed to terminate in 10 seconds.
```
**Cause:** NVENC encoder or X11 capture pipeline hung during session teardown. The internal 10-second watchdog fires and kills the process.
**Common root cause — missing window manager:** Sunshine's X11 capture requires a running window manager or desktop environment on DISPLAY=:0. An X server alone (login screen, headless with no WM) allows capture to start but teardown of the X11 pipeline hangs without a WM managing the window resources. This produces a reproducible Hang detected crash every time a Moonlight client attempts a session.
**Diagnostic — check WM before restarting:**
```bash
# Is a window manager running on :0?
ps aux | grep -iE "openbox|xfce|gnome|kde|mutter|xfwm" | grep -v grep
# Is X11 alive?
DISPLAY=:0 xdpyinfo 2>&1 | head -5
# If X11 is running but no WM, start one:
DISPLAY=:0 openbox --replace &
# Then restart Sunshine
systemctl --user restart sunshine
```
**Fix:** If a WM is running, restart Sunshine — this may be a transient encoder hang. If it happens repeatedly and a WM is present:
- Check for GPU driver instability (`nvidia-smi` for errors)
- Stop co-resident LLM models to free VRAM before gaming (`ollama stop`)
- Consider increasing session timeout (Sunshine Web UI → Configuration → General)
- If on a headless server, ensure the dummy plug hasn't become unseated — no connected display causes similar hang behavior
### Silent death (log just stops, no error)
**Symptom:** Sunshine.log has startup messages then just ends. No Fatal, no Error, no "Terminate handler called."
**Possible causes:**
- OOM kill (check `dmesg | grep -i "out of memory"`)
- SIGKILL from systemd (check `journalctl --user -u sunshine` for signal)
- Second "Hang detected" crash where the log was overwritten before the Fatal line was flushed to disk
**Fix:** Restart and watch live: `tail -f ~/.config/sunshine/sunshine.log` in another terminal, then connect a Moonlight client and disconnect. Observe the teardown sequence.
### "Couldn't bind RTSP server to port [48010], Address already in use"
**Symptom:** Logged as `Fatal` during startup but Sunshine continues running.
**Cause:** A stale Flatpak wrapper process from a previous crash still holds port 48010.
**Impact:** Moonlight uses RTSP (port 48010) for streaming protocol negotiation. Without it, clients get "RTSP handshake failed" (error 10060/connection timeout) even though Sunshine appears running and the HTTPS ports (47984, 47989) and Web UI (47990) are up.
**Fix:**
```bash
# Find and kill the stale wrapper (not the new /usr/bin/sunshine)
ps aux | grep sunshine | grep -v grep
kill <stale-pid>
# Then restart so Sunshine rebinds all ports cleanly
systemctl --user restart sunshine
# Verify all four ports are listening
ss -tlnp | grep -E "47984|47989|47990|48010"
```
### Clean SIGTERM ("Terminate handler called")
**Symptom:** Log shows `Info: Terminate handler called` with no preceding error.
**Cause:** Something sent SIGTERM — manual stop (`systemctl --user stop`), systemd cleanup, or a Hermes agent session issuing restart.
**Fix:** Just restart. Not a crash — check system journal around the timestamp for what sent the signal.
### RTSP connection reset / "No pending session" (error 10054)
**Symptom:** Moonlight client reports "Starting RTSP handshake failed: error 10054" (Windows) or "Connection reset by peer." Sunshine log shows:
```
Debug: No pending session for incoming RTSP connection
```
Sunshine is running, all ports are listening (verified with `ss -tlnp | grep sunshine`), and HTTPS/HTTP serverinfo requests succeed — but every RTSP connection is immediately reset.
**Cause:** The Moonlight client was never paired, or its pairing was lost. Sunshine's RTSP server requires a pending session created by the HTTP pairing flow BEFORE accepting RTSP connections. With zero paired clients, there is never a pending session, so every RTSP connection gets RST immediately.
**Diagnostic — check pairing state:**
```bash
python3 -c "
import json
with open('$HOME/.config/sunshine/sunshine_state.json') as f:
state = json.load(f)
paired = state.get('paired_clients', [])
print(f'Paired clients: {len(paired)}')
"
```
If the count is 0, the client must be paired.
**Fix — pair the client:**
1. Open Moonlight, click "+", enter `192.168.50.98` (or let it auto-discover)
2. Moonlight prompts for a PIN
3. Open Sunshine Web UI at `https://<server-ip>:47990/pin`
4. Enter the PIN shown in Moonlight into the Web UI
5. After pairing, RTSP connections will be accepted
**Troubleshooting IP Mismatch during pairing:** If the Web UI says "Success!" but Moonlight still says "PC Offline" and `sunshine_state.json` still shows 0 paired clients, this is an **IP routing mismatch**. Sunshine drops the pairing if the client IP Moonlight is using (e.g., Tailscale `100.x.x.x`) doesn't match the IP you used in the browser to access the Web UI (e.g., LAN `192.168.x.x`). **Fix:** Make sure the IP address typed into Moonlight's "Add PC" dialog *exactly matches* the IP address in your browser's address bar when entering the PIN.
**Note on `origin_pin_allowed`:** With `origin_pin_allowed = pc` in sunshine.conf, only localhost connections auto-pair. All LAN/WAN clients must enter a PIN. If `origin_pin_allowed = lan`, Tailscale clients get "Request Timed out" during pairing because Sunshine rejects the non-LAN source address.
**Error code reference:**
| Error | Meaning | Likely cause |
|---|---|---|
| 10060 | Connection timeout | Port not listening, firewall, Sunshine not running, stale process holding port 48010, OR **Sunshine crashed instantly upon connection (check log for audio `pa_simple_new` errors)** |
| 10054 | Connection reset by peer | RTSP server rejecting — unpaired client, or RTSP server module broken |
### Instant Crash on Connection (Audio Init Failure)
**Symptom:** Moonlight connects successfully via HTTP, logs show `New streaming session started`, but Sunshine crashes immediately. Moonlight reports `Error 10060` (Timeout / PC Offline) because the server died before the RTSP streaming ports could be reached.
**Log shows:**
```
Error: pa_simple_new() failed: Invalid argument
Error: Unable to initialize audio capture. The stream will not have audio.
```
**Cause:** In Sunshine v2026+, hardcoded `audio_sink = auto_null` or `virtual_sink = auto_null` in `sunshine.conf` causes a fatal crash during PulseAudio/PipeWire initialization when the client attempts to start the stream.
**Fix:** Remove `audio_sink` and `virtual_sink` from `~/.config/sunshine/sunshine.conf` and restart Sunshine. Allow Sunshine to auto-detect the PipeWire virtual sinks.
## Log locations
| Source | Path | Persistence |
|---|---|---|
| Sunshine app log | `~/.config/sunshine/sunshine.log` | **Overwritten on restart** — copy before restarting |
| Systemd journal (user) | `journalctl --user -u sunshine` | Persistent across restarts |
| System journal | `journalctl --since "HH:MM" --until "HH:MM"` | Persistent, broader system context |
| Sunshine state | `~/.config/sunshine/sunshine_state.json` | Persists across restarts (client pairings, app list) |
## Preserving crash logs
Sunshine.log is the most useful for debugging but is wiped on restart. Always copy first:
```bash
cp ~/.config/sunshine/sunshine.log ~/sunshine-crash-$(date +%Y%m%d-%H%M).log
```
Then restart and compare the old crash log against the new startup log.
@@ -0,0 +1,66 @@
# Windows VM + GPU Passthrough for Gaming
Running a Windows VM alongside Linux services on the same machine so anti-cheat games and Proton-incompatible titles work. Requires both OSes to coexist without disrupting Linux services (Docker, HA, Immich, Ollama).
## Option matrix
### Option 1: Second GPU (best)
Buy a cheap low-power GPU (GT 1030 / GTX 1050 / RX 550 — $40-80 used) for Linux. Pass the primary gaming GPU (2080 Ti) to Windows VM via VFIO.
| | Linux host | Windows VM |
|---|---|---|
| GPU | Cheap second card | Gaming GPU (full power) |
| LLM inference | Stays on Ollama ✅ | — |
| NVENC | Sunshine on Windows ✅ | Native ✅ |
| Anti-cheat games | — | Full compatibility ✅ |
| All Docker services | Unchanged ✅ | — |
Requirements: motherboard with two PCIe x16 (or x16 + x4 physical) slots. PSU headroom negligible (GT 1030 draws ~30W).
### Option 2: Single GPU passthrough (free, painful)
Pass the only GPU to Windows. Linux becomes fully headless — no GPU at all.
- All GPU workloads die on Linux: no Ollama, no Immich ML, no NVENC, no Docker GPU acceleration
- Must bind GPU to VFIO at boot (kernel cmdline: `vfio-pci.ids=10de:1e07,10de:10f7,10de:1ad6,10de:1ad7`)
- When Windows VM shuts down, GPU can be rebound to nvidia driver on Linux — but fragile; a reboot is cleaner
- LLM must run inside Windows VM or via CPU-only on Linux (impractically slow)
### Option 3: Proton-only (free, zero complexity)
No Windows VM. Accept that ~5% of games with kernel anti-cheat (Valorant, CoD, Fortnite, Destiny 2) won't work. Everything else runs through Steam Proton or Lutris on native Linux.
### Option 4: Dual boot
Reboot between Linux and Windows. Not simultaneous — all Linux services are down while gaming.
## OEM motherboard pitfalls
### HP OMEN 30L (HP 8703 board)
- **iGPU is disabled when dGPU installed.** The i7-10700K has Intel UHD Graphics 630 but it's not exposed — no video output ports routed to back panel, not visible in lspci. You cannot use iGPU for Linux host + dGPU for Windows VM passthrough on this board.
- **Only one GPU works at a time.** Option 1 (second GPU) requires verifying the board has a second physical PCIe slot and that both GPUs fit physically.
- **BIOS may lack IOMMU/VT-d settings.** HP OEM BIOSes are stripped down. VT-d/IOMMU may be enabled by default but not configurable. Check with `dmesg | grep -iE "IOMMU|DMAR"` and `ls /sys/kernel/iommu_groups/`.
### General OEM board checks before GPU passthrough
```bash
# 1. GPU IOMMU group — must be isolated (no other devices in same group)
for d in /sys/kernel/iommu_groups/*/devices/*; do
n=${d#*/iommu_groups/*}; n=${n%%/*}
echo "Group $n: $(lspci -nns "${d##*/}" | cut -d' ' -f2-)"
done
# 2. VFIO kernel module available
modprobe vfio-pci && echo "vfio-pci available"
# 3. Second PCIe slot existence
lspci | grep -iE "VGA|3D|display"
```
## What this does NOT cover
- Full VFIO passthrough setup (libvirt XML, vfio-pci binding, Windows virtio drivers)
- Looking Glass (low-latency VM display on Linux host)
- SR-IOV (GPU virtualization — not supported on consumer NVIDIA cards)