This document records the implementation and initial verification of an always-on Steam streaming session on a monitorless NixOS host.
The host renders a 1920×1200 display at 120 Hz on an RTX 4070 Ti. Sunshine captures and encodes that display. Moonlight on a NixOS laptop decodes the stream and returns keyboard, mouse, and controller input. XREAL glasses can display the laptop’s output without connecting directly to the server.
Outcome
The deployed system provides:
- A virtual 1920×1200@120 display on a physically disconnected NVIDIA output.
- An automatically logged-in gamescope session running Steam Big Picture.
- Sunshine KMS capture with Vulkan Video hardware encoding.
- H.264, HEVC, and AV1 encoder support on the RTX 4070 Ti.
- PipeWire audio capture and Opus transport.
- Remote input through Linux
uinput. - An official Moonlight client on the NixOS laptop.
- A LAN-only administration endpoint at
sunshine.kuhree.com. - No public-edge route and no game/inference mode-switch service.
The initial operator test covered several games for roughly five to ten minutes. The stream was reported as smooth, with barely perceptible input lag. The XREAL glasses were a good fit for the single-screen gaming use case. Longer sessions and more games remain untested.
Constraints
The implementation was designed around these constraints:
| Constraint | Decision |
|---|---|
| No physical monitor | Generate an EDID and force one DRM connector active at boot. |
| Existing AI workloads | Keep Sunshine always available; avoid simultaneous gaming and inference manually. |
| Fixed client target | Use 1920×1200@120 for the laptop and XREAL display path. |
| Existing user data | Run Steam and Sunshine as the inventory-defined primary user. |
| LAN-only use | Bind Sunshine to the host’s LAN address and publish only a LAN service endpoint. |
| Minimal custom code | Use the NixOS Sunshine and Steam gamescope modules. |
| Credentials | Keep Steam, Sunshine, and pairing state in the user’s home directory. |
Architecture
flowchart LR subgraph Host["Headless NixOS host"] EDID["Generated EDID<br/>1920×1200 at 120 Hz"] Steam["gamescope<br/>Steam Big Picture"] Capture["Sunshine<br/>KMS capture"] Encode["RTX 4070 Ti<br/>Vulkan Video"] Audio["PipeWire audio"] Input["Virtual input<br/>uinput"] end subgraph Client["NixOS laptop"] Moonlight["Moonlight"] Controls["Keyboard, mouse, controller"] Display["Laptop display or XREAL glasses"] Speakers["Audio output"] end EDID --> Steam Steam --> Capture Capture --> Encode Encode -->|video over LAN| Moonlight Audio -->|Opus audio| Moonlight Moonlight --> Display Moonlight --> Speakers Controls --> Moonlight Moonlight -->|input over LAN| Input Input --> Steam
The laptop does not run the game. It decodes video, plays audio, and transports input. Game execution, Steam state, Proton prefixes, saves, and shader caches remain on the host.
Host design
Virtual display
A display must exist before gamescope and Sunshine have a framebuffer to use. SSH access does not create a graphical session.
A generated EDID declares one mode:
connector: DP-1
resolution: 1920 × 1200
refresh rate: 120 Hz
physical display: noneNixOS installs the EDID as firmware and adds two kernel parameters:
drm.edid_firmware=DP-1:edid/xreal120.bin
video=DP-1:1920x1200@120eflowchart LR E["Generated EDID"] --> K["Kernel firmware loader"] K --> D["NVIDIA DRM driver"] D --> O["Connected DP-1 output"]
This part requires a reboot. After deployment, the kernel reported DP-1 as connected. DRM state showed an active 1920×1200 mode at 120 Hz, a 1920×1200 framebuffer, and an assigned cursor plane.
The DRM card number changed across boots, so operational checks identify DP-1 dynamically instead of assuming card0.
Graphical session
The host does not run KDE, GNOME, or a general-purpose desktop. Greetd logs the primary user into the NixOS steam-gamescope session.
flowchart LR G["greetd"] --> GS["gamescope"] GS --> S["Steam Big Picture"] S --> Game["Selected game"]
The deployed gamescope process uses:
gamescope --steam
-W 1920 -H 1200
-w 1920 -h 1200
-r 120
-O DP-1
--generate-drm-mode cvt
--force-composition
--prefer-vk-device 10de:2782
-- steam -tenfoot -pipewire-dmabuf--steam enables gamescope’s Steam integration. --prefer-vk-device selects the RTX 4070 Ti instead of the AMD integrated GPU.
--force-composition was added after the first test because Sunshine KMS capture did not show the Steam overlay reliably. Gamescope can promote a game to direct scanout while composing the overlay separately. Forced composition produces one frame containing both. The setting is deployed; visual confirmation remains pending.
Capture and encoding
Sunshine runs as the primary user’s systemd user service and starts with the graphical session. A ConditionUser guard prevents the globally installed unit from also starting under root’s user manager.
Sunshine uses KMS capture. The service has the capability required to inspect the DRM framebuffer and is restricted to the host’s LAN address.
The stock nixpkgs Sunshine package was built without CUDA support. Forcing nvenc therefore failed with Cannot load libcuda.so.1. Sunshine then found a working Vulkan Video path on the same RTX 4070 Ti. The deployed configuration selects vulkan directly.
Startup verification found:
Found H.264 encoder: h264_vulkan
Found HEVC encoder: hevc_vulkan
Found AV1 encoder: av1_vulkan
Vulkan encode using GPU: NVIDIA GeForce RTX 4070 TiThis is hardware encoding. It does not use the CPU software encoder.
Audio
PipeWire runs on the host with ALSA and PulseAudio compatibility. During a live stream, Sunshine created and selected a stereo sink monitor, then initialized Opus at 48 kHz and two channels.
flowchart LR Game["Game audio"] --> PipeWire["PipeWire sink monitor"] PipeWire --> Sunshine["Sunshine<br/>Opus encoding"] Sunshine --> Moonlight["Moonlight audio output"]
Input
Moonlight sends client input back to Sunshine. Sunshine creates host-side virtual input devices through /dev/uinput. The primary user belongs to the input, render, and uinput groups.
flowchart RL Device["Client input device"] --> Moonlight["Moonlight"] Moonlight --> Sunshine["Sunshine"] Sunshine --> UInput["Linux uinput"] UInput --> Game["Steam or game"]
The first deployment omitted uinput group membership. Sunshine reported a permission error. Adding the group fixed the host-side virtual-input boundary.
Client design
The official moonlight-qt package is installed through the laptop’s standalone Home Manager profile.
The client performs four jobs:
- Discover or connect to the Sunshine host.
- Decode the selected video codec in hardware.
- Play the streamed audio.
- Send local input events to the host.
The XREAL glasses remain a display attached to the laptop. They do not pair with Sunshine and do not decode the stream independently.
flowchart LR Host["Streaming host"] -->|encoded video and audio| Laptop["Moonlight on laptop"] Laptop --> Glasses["XREAL glasses"] Controls["Local controls"] --> Laptop Laptop -->|input events| Host
Administration path
The administration path is separate from the game stream.
Platform declares sunshine.kuhree.com as a LAN endpoint on the streaming host. CoreDNS derives its A record from Platform host inventory. The existing nginx instance terminates TLS and proxies the Web UI to Sunshine’s native HTTPS listener on port 47990.
flowchart LR Browser["LAN browser"] --> DNS["CoreDNS"] DNS --> Address["Streaming host LAN address"] Address --> Nginx["nginx on port 443"] Nginx --> UI["Sunshine UI on port 47990"]
Sunshine explicitly trusts both the native LAN URL and https://sunshine.kuhree.com as CSRF origins. The initial setup failed until the LAN origin was declared.
Moonlight does not send gameplay through nginx. Video, audio, and input use Sunshine’s native LAN ports directly.
Deployment and verification
The implementation was merged through Platform issue #1404 and its associated pull requests.
Configuration checks covered:
- Sunshine enabled only on the intended host.
- LAN address binding.
- The Vulkan encoder selection.
- Primary-user
uinputaccess. - Primary-user systemd service ownership.
- The LAN endpoint and nginx upstream.
- Gamescope forced composition.
- Full NixOS evaluation for the streaming host.
- Full NixOS evaluation for both authoritative DNS hosts.
- Moonlight presence in the evaluated laptop Home Manager package set.
Runtime verification covered:
- The booted NixOS generation.
- Kernel EDID parameters.
- Connected
DP-1state. - Active DRM mode and framebuffer size.
- A live gamescope process on the RTX 4070 Ti.
- A single Sunshine process owned by the primary user.
- H.264, HEVC, and AV1 Vulkan encoders.
- Sunshine TCP reachability from the laptop.
- Valid TLS and DNS for
sunshine.kuhree.com. - PipeWire audio capture during a stream.
- A successful Moonlight pairing.
- Steam Big Picture visible through Moonlight.
- Several games launched during an initial five-to-ten-minute session.
Problems found during deployment
| Problem | Cause | Correction |
|---|---|---|
| Headless connector remained disconnected | No EDID or forced mode existed at boot | Generate EDID, install it as firmware, and force DP-1 at boot |
| NVENC initialization failed | Stock nixpkgs package lacked CUDA support | Select the verified Vulkan Video encoder |
| Controller emulation reported permission errors | Primary user lacked the uinput group | Add uinput to the primary user’s groups |
| Sunshine started twice | Global user unit also started under root’s user manager | Add ConditionUser for the primary user |
| Web UI rejected credential setup | LAN URL was absent from allowed CSRF origins | Declare native and friendly LAN origins |
| Friendly URL returned 502 | Wrong Sunshine instance held the native streaming ports | Stop the duplicate instance and enforce service ownership |
| Steam overlay was absent from the captured game | Direct scanout separated the game plane from composed UI | Enable gamescope forced composition |
Observed result
The initial live session produced these observations:
- Steam Big Picture was available immediately after connecting.
- Several games launched and remained responsive.
- The operator barely perceived input lag during the short test.
- Video and audio were stable enough for normal play.
- The XREAL glasses worked well as the laptop’s private display.
- Steam overlay capture was the only reported graphical defect.
These observations are qualitative. No frame-time, encode-latency, decode-latency, packet-loss, or end-to-end input measurements have been collected.
Desktop and non-Steam software
The host has a real graphical session, but it does not have a conventional desktop environment. Gamescope owns the display and Steam owns the interface.
The current session supports:
- Native Steam games.
- Proton compatibility selection per game.
- Custom Proton versions installed on the host.
- Non-Steam executables added to the Steam library.
- Host-side editing of Steam shortcuts, game files, and Proton prefixes.
Desktop-oriented tools remain awkward because the session lacks a general file manager, terminal window, and graphical file picker. Steam ROM Manager is an example: it must run on the host because it edits the host’s Steam library, but its configuration interface does not fit the current console-style session well.
A possible follow-up is a separate Sunshine application named Desktop. It would start a lightweight maintenance session while preserving Steam as the default experience. This is not implemented.
Current limitations and follow-up tests
- Confirm that forced composition makes the Steam overlay visible in Moonlight.
- Measure performance during longer sessions and more demanding games.
- Verify controller behavior across several controller types.
- Record Moonlight’s decode, network, and render latency statistics.
- Evaluate whether full composition changes latency, frame pacing, or power use.
- Decide whether a host-maintenance desktop is worth its extra session complexity.
- Continue avoiding simultaneous gaming and local AI inference unless measured behavior supports it.
Implementation locations
Platform owns the declarative implementation:
services/sunshine/default.nix
services/sunshine/module.nix
services/sunshine/tests.nix
services/sunshine/README.md
hosts/srv-bldr-nv/default.nix
hosts/nixpad/default.nix
modules/home/desktop/moonlight.nixThe monitorless-display experiment and operator observations remain in the unpublished project record at Projects/monitorless-display-experiment.md.
Sources
Primary documentation
- Linux kernel EDID documentation
- ValveSoftware/gamescope
- Sunshine configuration reference
- Moonlight setup guide for Sunshine
- NixOS Sunshine module
- NixOS Steam module
Relevant upstream issues
- nixpkgs #559144: Sunshine NVENC is compiled out by default
- Sunshine #3860: Steam overlay is absent from Gamescope streams
- gamescope #1418: Steam overlay not loading under gamescope
- gamescope #835: Gamescope breaks Steam overlay