Portfolio›steam-streaming-configPython

steam-streaming-config

Steam / Moonlight streaming rig (CT 104, michelle-steam)

Sunshine + Moonlight game streaming with NVENC, plus an emulator stack that auto-imports ROMs into Steam. Host: Proxmox 192.168.1.96, CT IP 192.168.1.140.

Hard constraints - read before changing anything

The container MUST stay privileged (unprivileged: 0, lxc.cap.drop: empty). NVIDIA + NvFBC + uinput all depend on it. Converting it breaks the whole stack.

Do not remove picom. It adds latency, but Steam Big Picture's steamwebhelper (CEF) silently dies without a compositing manager and Steam does not respawn it - you get a black screen and must restart Steam entirely. The fix is /etc/xdg/picom.conf with vsync = false and unredir-if-possible = true, so fullscreen games bypass the compositor instead.

Device majors are dynamic. 104.conf allows 195, 510, 511; today nvidia-uvm is 511, 510 is stale, and nvidia-caps (236) is not listed at all - it only works because the container is privileged. Re-check grep nvidia /proc/devices after any driver change or host move.

NVIDIA userspace here is pinned to driver 595.84 (libnvidia-encode.so.595.84). The host driver must match exactly.

The ZFS dataset must mount at /georgia (mp0: /georgia/,mp=/mnt/). Every emulator path is relative to it.

csrf_allowed_origins hardcodes https://192.168.1.140:47990 - the Sunshine web UI breaks if the container IP changes.

Latency findings (2026-09-05)

The encoder was never the problem. In order of impact:

  1. min_log_level = verbose in sunshine.conf - several lines per frame to disk and journald inside the encode loop; log had reached 140 MB growing 62 KB/s. Now info.
  2. picom - see above, config fix not removal.
  3. Fragmented cpuset: cores: 8 gave 0,2-7,10 = 2 whole cores + 4 half-cores on a (0,6)(1,7)(2,8)... sibling map. Now cores: 12 / cpuset 0-11.
  4. RAM 8192 -> 16384 MB.

Not the cause, despite appearances: CPU quota (cpu.max is max, nr_throttled 0), software encoding (NVENC always used), WiFi (client is wired). Sent Frame ... Dupe spam is normal idle behaviour.

Operational gotchas

  • pct set 104 -cores N does NOT re-apply cpuset to a running container. Write /sys/fs/cgroup/lxc/104/cpuset.cpus directly or restart the CT.
  • Restarting sunshine.service kills any running game - Sunshine launches Steam.
  • Never pkill -f a steam pattern via pct exec 104 -- bash -c '...'; the pattern matches the pct exec cmdline and kills your own shell, taking Sunshine with it. Push a script in and run pct exec 104 -- bash /tmp/x.sh.
  • Relaunch Steam: runuser -u steam -- env DISPLAY=:1 XDG_RUNTIME_DIR=/run/user/1001 setsid /bin/steam -bigpicture
  • Black-screen health check: pgrep -a steamwebhelper must exist, and DISPLAY=:1 xwininfo -root -children | grep 1920x1080 must show full-size windows.
  • There is a DECOY /root/.config/sunshine/sunshine.conf that is not used. The service runs as steam; edit /home/steam/.config/sunshine/sunshine.conf.
  • Sunshine's virtual gamepad only exists WHILE a stream is active. Configure pads during a session. It is named after whatever the client reports, so hardcoded Device = lines break when the client controller changes.
  • Harmless log noise: nouveau driver load failure, glx dri3 screen failure, dbus-launch not found, av1_nvenc probe failure (Ampere has no AV1 encoder).

Emulator stack (/opt/emu)

Steam ROM Manager does not work here - do not retry the AppImage. Electron's renderer will not start in this container; eight invocation variants all hung or died with SIGTRAP. /opt/emu/emu-sync.py writes Steam's binary shortcuts.vdf directly and pulls artwork from SteamGridDB instead. No display, no browser engine.

  • emu-sync.py - idempotent; owns only ids tracked in state/managed.json. Returns 75 (EX_TEMPFAIL) if Steam is running, because Steam caches shortcuts.vdf in memory and overwrites it on exit. Units set SuccessExitStatus=0 75.
  • emu-update.py - weekly AppImage refresh. Supports asset_pattern (regex) for versioned filenames, prerelease, and source: rpcs3-api.
  • Units: emu-sync.path (inotify, works because the share is local ZFS), emu-sync.timer (15 min), emu-update.timer (Sun 04:00).
  • Web console: http://192.168.1.140:8090 (emu-console.service). Root, LAN-bound, no auth. Port 8090 because 8787 is taken.

Steam collections: the tags field in shortcuts.vdf is dead - modern Steam ignores it. Collections live in a Chromium LevelDB, mirrored into localconfig.vdf under user-collections. The collection NAME exists only in the LevelDB, so a collection invented in localconfig alone renders blank. emu-sync only ADDS to an existing collection (steam_collection_id, per-collection not global). Never write the LevelDB directly.

Per-emulator traps: see the individual wrappers in emu-bin/. Summary:

  • PCSX2 ships QT_QPA_PLATFORM="xcb,wayland" which its Qt build cannot parse - wrapper exports QT_QPA_PLATFORM=xcb.
  • RPCS3 Linux builds are in RPCS3/rpcs3-binaries-linux, and GitHub releases there are ordered by commit sha not date, so they are unusable for updates. Use https://update.rpcs3.net/?api=v2.
  • Dolphin has no official AppImage and flatpak cannot work here (/proc/sys is read-only under Proxmox AppArmor). Use pkgforge-dev/Dolphin-emu-AppImage. Its zenity update prompt BLOCKS launches - disabled via policy file plus a nozenity stub on PATH.
  • Never hand-write Dolphin input mappings; invalid expressions crash the Controllers dialog. Copy the bundled SDL Gamepad.ini and supply only Device =.
  • Never create/move/delete anything inside Dolphin's Wii NAND - it keeps fst.bin and unregistered folders cause "Could not write to/read from Wii system memory".
  • SteamGridDB sits behind Cloudflare and 403s the default Python-urllib User-Agent.

Secrets

/opt/emu/config.json holds the SteamGridDB API key (mode 600) - gitignored; emu/config.example.json is the redacted copy. sunshine_state.json holds the admin password hash and paired client certs - gitignored, never commit it.

Recent changes

  • 2026-09-20 15a49e8 Initial capture of Sunshine streaming + emulator stack