Gaming session: gamescope + Sunshine/Moonlight + ES-DE¶
How the living-room gaming/streaming box on av (hostname ascertain-verse) is
wired end to end: autologin into a gamescope session as guest, playable
locally on the TV via Steam Big Picture, and remotely via Sunshine →
Moonlight (native clients and the browser moonlight-web stack). ES-DE is
the front-end hub; Steam and individual Steam games hand off to and from it.
This lives entirely in the gaming stack, separate from the homelab — nothing
here runs as root against homelab services. It is the "sacred, don't touch"
setup the top-level docs warn around (CONTEXT.md, docs/architecture.md,
README.md, docs/host-port-map.md). This file documents it so it's
recoverable; it is not a change surface.
Admin access:
ssh guest@localhost(skill-lava's key is inguest'sauthorized_keys).guestis UID 1001, groupsvideo render input gamers, shell bash. The ROM library, per-system emulators, and RetroArch/MAME/Xbox wiring live inrom-library.md; PS3 specifics inrpcs3.md.
The pieces at a glance¶
| Layer | What | Where it's defined |
|---|---|---|
| Display session | CachyOS gamescope-session (SteamOS-style), autologin guest |
/usr/share/wayland-sessions/gamescope-session.desktop, /usr/lib/systemd/user/gamescope-session.{service,target}, cachyos-gamescope-autologin.service |
| Local entry | gamescope → Steam Big Picture (-gamepadui -steamos3) |
gamescope-steam.service (user) |
| Remote host | Sunshine (LizardByte) as a guest user service | /usr/lib/systemd/user/app-dev.lizardbyte.app.Sunshine.service (symlinked ~/.config/systemd/user/sunshine.service) |
| Remote clients | Native Moonlight + browser moonlight-web | repo: stacks/moonlight-web/, stacks/traefik/dynamic/sunshine.yaml |
| Front-end | ES-DE in its own gamescope | gamescope-esde.service (user) |
| Steam↔ES-DE glue | Session handoff units + Steam→ES-DE sync | gamescope-steam*.service, sync-steam-games (timer) |
| Input | makima gamepad remapper + watchdog | 50-makima.rules, bigscreen-input-watchdog.service |
| HA hook | Controller-plug → HA webhook | 90-controller-ha.rules, ha-game-mode.service |
| Audio | sendspin-player (Music Assistant client) | sendspin-player.service (user) |
Boot → session¶
SDDM is installed but disabled; cachyos-gamescope-autologin.service sets
the session and autologins guest into gamescope-session.desktop, which runs
start-gamescope-session → gamescope-session.target. The base target starts
Steam Big Picture in gamescope (gamescope -f -e -- steam -gamepadui -steamos3).
Full desktop (Plasma / plasma-bigscreen) sessions also exist for maintenance.
/usr/bin/start-gamescope.sh is a manual fallback that launches
gamescope -f -e -- steam -gamepadui -steamos if nothing is already running.
Remote host: Sunshine¶
Runs as a guest user service, WantedBy=graphical-session.target (so it only
exists while the guest desktop is up), with a 5 s ExecStartPre sleep so the
compositor is ready first. Alias sunshine.service.
~/.config/sunshine/sunshine.conf:
csrf_allowed_origins = https://192.168.86.49:47990,https://sunshine.fmm.house
system_tray = disabled
hevc_mode = 2 # allow HEVC
av1_mode = 0 # AV1 off
~/.config/sunshine/apps.json exposes exactly one app — everything routes
through ES-DE, not per-game Sunshine entries:
{ "apps": [ { "name": "Games", "cmd": "/home/guest/.local/bin/tile-games", "working-dir": "/home/guest" } ] }
Paired clients: the Moonlight clients that have paired are recorded in
sunshine_state.json on the host (self-chosen device names + certs) — kept
out of the repo, since the names are personal. Credentials/cert likewise
live in ~/.config/sunshine/ (credentials/, portal_token), never committed.
Ports are reserved and off-limits to homelab services: 47984–48010 (+ 1716)
— see docs/host-port-map.md. Sunshine web UI is on 47990.
Remote clients (in the repo)¶
stacks/traefik/dynamic/sunshine.yaml— proxiessunshine.fmm.house→https://172.30.0.1:47990(Sunshine's web UI) withinsecureSkipVerify.stacks/moonlight-web/— browser Moonlight atgame.fmm.house. WebRTC media on UDP 40000–40100 published to the host;WEBRTC_NAT_1TO1_HOSTis the Tailscale IP; connects to Sunshine at172.30.0.1:47989; pairs asroth.
The session state machine (ES-DE as hub)¶
tile-games (the Sunshine "Games" cmd) is the keep-alive that ties a Moonlight
session to the whole gamescope chain:
- Starts
gamescope-esde.service(ES-DE in gamescope,-W 1920 -H 1080 -r 60 --backend wayland). - Polls
systemctl --user list-units 'gamescope-*'every 2 s; exits once nogamescope-*unit has been active for ~6 s (3 misses). Whentile-gamesexits, Sunshine ends the stream automatically.
Handoffs between ES-DE, Steam, and a specific Steam game — each conflicts ES-DE and restarts it on exit, so you always land back in ES-DE:
| Unit | Launches | On stop |
|---|---|---|
gamescope-esde.service |
ES-DE | — |
gamescope-steam.service |
Steam Big Picture (-steamos3) |
handoff.conf: ExecStopPost → start gamescope-esde.service |
gamescope-steam-game@<appid>.service |
steam -applaunch <appid> |
ExecStopPost → start gamescope-esde.service |
gamescope-steam.service has an ExecCondition refusing to start if gamescope is
already running, plus drop-ins: notify.conf (wants steam-launch-notify),
path.conf (prepends ~/.local/bin), relabel.conf (steam-relabel-exit).
Steam → ES-DE sync¶
~/.local/bin/sync-steam-games (Python) runs via sync-steam-games.timer
(2 min after boot, then every 5 min). It:
- Discovers installed titles from
appmanifest_*.acfin/opt/steam-library/steamappsand~/.local/share/Steam/steamapps(skips Proton / Steam Linux Runtime / Steamworks). - Writes a launcher
<Game>.shper title into/opt/games/roms-flat/steam/, each runningsystemctl --user start --no-block gamescope-steam-game@<appid>.service(plus a "Steam Big Picture" launcher). Stale.shfiles are pruned. - Scrapes metadata (Steam
appdetails), trailers (HLS→mp4), and artwork (SteamGridDB — API key embedded in the script) into~/ES-DE/downloaded_media/steam/and~/ES-DE/gamelists/steam/gamelist.xml, preserving user fields (favorite/playcount/lastplayed/hidden). Incremental: metadata cached in~/.cache/steam-meta, media fetched only when missing.
ES-DE custom systems¶
~/ES-DE/custom_systems/es_systems.xml overrides two systems (everything else is
ES-DE's bundled defaults):
steam—.sh/.SHlaunched via/usr/bin/bash %ROM%(the sync'd launchers above).ps3— override so disc ISOs launch directly via standalone RPCS3 (%EMULATOR_RPCS3% --no-gui %ROM%). Full detail inrpcs3.md.
ROMs live at /opt/games/roms-flat/<system>/ (group gamers): nes snes gb gbc
gba n64 gc genesis psx ps3 steam.
Input¶
- makima gamepad→kbd/mouse remapper —
/etc/udev/rules.d/50-makima.rulesgrantsuinputaccess (GROUP=input,uaccess). No config present in~/.config/makima/— it runs on defaults. bigscreen-input-watchdog.service(user,Restart=always) clears stuck input suppression and enforces guide-button policy in Big Picture.- Steam runs in
-gamepadui(Big Picture) so the whole session is controller-navigable.
Home Assistant "game mode" hook¶
/etc/udev/rules.d/90-controller-ha.rules: when any joystick (js* /
ID_INPUT_JOYSTICK) is added, systemd starts the system oneshot
ha-game-mode.service → /usr/local/bin/ha-game-mode.sh, which POSTs
https://ha.fillmemore.house/api/webhook/game-mode. This is how HA knows a
controller woke up (e.g. lighting/scene changes).
⚠️ Dead wiring: the same udev file also declares
ENV{SYSTEMD_USER_WANTS}="controller-plugged.service", but that user unit does not exist in~/.config/systemd/user/. The system webhook still fires; the per-user half is a no-op. Either create the unit or drop the line.
Audio: sendspin-player (Music Assistant)¶
The guest session also runs sendspin-player.service (user):
/usr/local/bin/sendspin-player --daemon --no-tui --name "Living Room"
--client-id av-living-room — a Music Assistant player exposing the living-room
audio, with sendspin-watchdog.timer (every 2 min) reconnecting it. Related to
the music stack, not to Moonlight, but part of the same guest AV box.
Recovery / operations¶
All user units are under ~guest/.config/systemd/user/ (run
systemctl --user … as guest). Key knobs:
ssh guest@localhost
systemctl --user status sunshine.service # Sunshine host
systemctl --user restart gamescope-esde.service # kick ES-DE
systemctl --user start sync-steam-games.service # force a Steam→ES-DE sync
journalctl --user -u app-dev.lizardbyte.app.Sunshine.service -e
If a rebuild is ever needed, the reconstruction inputs are: the gamescope-session
package (system units under /usr/lib/systemd/user/, autologin), the guest user
units above, ~/.config/sunshine/ (config + pairings + cert), ~/ES-DE/
(custom_systems, gamelists, downloaded_media), the ~/.local/bin/ scripts
(tile-games, sync-steam-games, steam-relabel-*, steam-launch-notify,
bigscreen-input-watchdog), and the two udev rules. None of these are backed up
in the repo today — see follow-ups.
Follow-ups¶
controller-plugged.servicereferenced by udev doesn't exist — resolve (create or remove the reference).- makima has no pinned config — captures nothing if the box is rebuilt.
- Guest-home config (Sunshine, ES-DE custom_systems,
~/.local/binscripts, user units) is not version-controlled. Consider a tracked mirror like the ESPHome one, or at least a documented backup, given how much bespoke glue lives here.