Merge pull request 'feat(hosts): add lyrathorpe-console, a living-room games machine' (#99) from feat/console-host into main
CI / flake (push) Successful in 4m54s

Reviewed-on: #99
This commit was merged in pull request #99.
This commit is contained in:
2026-08-29 19:23:01 +01:00
7 changed files with 781 additions and 10 deletions
+340
View File
@@ -0,0 +1,340 @@
# Console — living-room games machine
Flake host: `lyrathorpe-console`. Desktop (`portable = false`, imports
`../../modules/desktop.nix`). Files: `configuration.nix`, `nvidia.nix`,
`gaming.nix`, `hardware-configuration.nix`.
A 4th-generation Core i7 (Haswell) on a UEFI board with an NVIDIA GeForce GTX
1070 8 GB, wired to a television. It boots straight into Steam Big Picture and
is driven from the sofa with a Bluetooth controller; keyboard and mouse are
supported but secondary.
## Not installed yet
`hardware-configuration.nix` in this host directory is a **placeholder**, not a
hardware scan. It exists so the flake evaluates in CI and assumes the install
labels its partitions `nixos` (root, ext4) and `BOOT` (ESP, vfat). Replace the
whole file with the output of `nixos-generate-config` run on the machine and
commit that. If the labels do not match, the boot fails on a missing device
rather than touching the wrong disk.
Partition the disk GPT with an ESP (vfat, 512 MB is comfortable). Nothing else
in the host config depends on the disk layout.
## Bootloader
Ordinary PC UEFI firmware, so **systemd-boot** with
`canTouchEfiVariables = true` — unlike the Mac Pro, this board is trusted with
`efibootmgr` NVRAM writes.
`boot.loader.timeout = 0`: the boot menu is unreadable from a sofa and unusable
without a keyboard, so the default entry boots immediately. **Hold space at
power-on** to get the menu back and pick an older generation.
`configurationLimit = 10` stops the ESP filling up.
## Graphics — GTX 1070
Everything driver-related is in [`nvidia.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/hosts/Console/nvidia.nix).
The GTX 1070 is Pascal (GP104), so it is under the same driver constraint as the
Mac Pro's Quadro P400:
- **Driver branch 580** (`nvidiaPackages.legacy_580`), _not_ the nixpkgs default
(`production`, currently 595.x). 580 is the last branch supporting
Maxwell/Pascal/Volta, maintained as an LTS branch until Aug 2028; a newer
branch does not drive this card at all.
- `modesetting.enable = true` — mandatory for Wayland. Without
`nvidia-drm.modeset=1` neither gamescope nor wlroots gets a usable GBM device,
and both the Steam session and Sway fail to start.
- `open = false` — the open kernel modules require Turing or later.
- Sway runs with `--unsupported-gpu`; wlroots refuses the proprietary driver
otherwise. gamescope and cage/ReGreet need no such flag.
- `hardware.graphics.enable32Bit` pulls in the lib32 NVIDIA userspace that
32-bit Steam titles and Proton's 32-bit prefixes link against.
The driver is unfree, so it is **not in the binary cache**: the kernel module is
compiled on the machine. On a Haswell i7 that is a few minutes, not the Mac
Pro's ordeal, but it recurs on every kernel bump. The package names are
allowlisted in `unfreePackages` in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix).
Verify after a rebuild:
```sh
nvidia-smi
lsmod | grep nvidia # nvidia, nvidia_modeset, nvidia_drm
```
## Session model — autologin into Steam
[`gaming.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/hosts/Console/gaming.nix)
sets `programs.steam.gamescopeSession.enable`, which registers a `steam.desktop`
Wayland session (gamescope wrapping Steam in tenfoot mode) and installs a
`steam-gamescope` launcher. greetd — already present on every Sway host for
ReGreet, see [`../../modules/sway.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/sway.nix)
— gets an `initial_session` pointing at that launcher:
```
boot
└─ greetd initial_session (autologin as lyrathorpe)
└─ gamescope --steam -- steam -tenfoot -pipewire-dmabuf
├─ Steam library / Proton titles
├─ [non-Steam] RetroArch
└─ [non-Steam] Clone Hero
quit Steam → greetd default_session → ReGreet → pick Sway (keyboard + mouse)
```
So there is no greeter at boot and no keyboard needed. Quitting Steam drops back
to ReGreet, where the ordinary Sway session is available for everything else.
`services.greetd.restart` defaults to `false` once `initial_session` is set,
which is what stops a logout looping straight back into autologin.
Two details worth knowing:
- The launcher is referenced as `/run/current-system/sw/bin/steam-gamescope`.
The steam module builds that script privately and only adds it to
`environment.systemPackages`, so there is no package attribute to point at.
- `programs.gamescope.capSysNice = true` installs gamescope as a setcap wrapper
in `/run/wrappers/bin` instead of the system profile. That path precedes the
system profile on `PATH`, so `steam-gamescope` still resolves it.
The greeter is **Dvorak**, like every other host here (`modules/sway.nix` forces
`XKB_DEFAULT_VARIANT=dvorak` on cage). Only relevant if someone else has to type
a password.
### Adding RetroArch and Clone Hero to Big Picture
Both are installed system-wide but are not Steam titles. Add each once, from a
Sway session, via Steam's **Games → Add a Non-Steam Game**; they then appear in
Big Picture and inherit Steam Input, so the controller works in them without
further configuration.
## Emulation — RetroArch
`gaming.nix` builds RetroArch through `pkgs.retroarch-bare.wrapper`, which wires
up the packaged assets, core info and joypad autoconfig profiles. Cores:
| System | Core |
| -------------------------------------- | ------------------------ |
| NES | `nestopia` |
| SNES | `snes9x` |
| Game Boy / Color | `gambatte` |
| Game Boy Advance | `mgba` |
| Nintendo 64 | `mupen64plus` |
| Nintendo DS | `melonds` |
| GameCube / Wii | `dolphin` |
| Master System / Game Gear / Mega Drive | `genesis-plus-gx` |
| 32X / Mega CD | `picodrive` |
| Saturn | `beetle-saturn` |
| Dreamcast / NAOMI | `flycast` |
| PlayStation | `beetle-psx-hw` |
| PlayStation 2 | `pcsx2` (LRPS2) |
| PSP | `ppsspp` |
| Arcade | `fbneo`, `mame2003-plus` |
| DOS | `dosbox-pure` |
A handful of settings are applied on every launch via `--appendconfig`, so they
are fixed policy rather than saved preferences — changing them in the UI will
not stick. Everything else stays user-editable as normal.
- `menu_driver = ozone` — the controller-navigable menu.
- `video_fullscreen = true`.
- `input_menu_toggle_gamepad_combo = 2`**L3+R3 opens the RetroArch menu**
from inside a running core. Without a pad combo there is no way to exit a game
without a keyboard. No retro system emulated here has L3/R3 on its own
controller, so the binding cannot collide with a game.
- `system_directory`, `savefile_directory`, `savestate_directory`,
`playlist_directory`, `screenshot_directory`, `thumbnails_directory` and
`rgui_browser_directory` — all pointed at the shared library described below,
rather than scattered through `~/.config/retroarch`.
An unrecognised key in an appended config is ignored **silently**, so those key
names are worth keeping in step with upstream if RetroArch is ever bumped
across a major version.
### BIOS files and expectations
Several cores need BIOS or firmware images that are not redistributable and are
therefore not packaged. Drop them in `/srv/games/bios`, which is RetroArch's
system directory on this host:
- **PlayStation 2** (`pcsx2`) — a PS2 BIOS dump. The core will not boot anything
without one.
- **Saturn** (`beetle-saturn`) — region BIOS images.
- **Dreamcast** (`flycast`) — `dc_boot.bin` / `dc_flash.bin` for most titles.
- **Nintendo DS** (`melonds`) — optional, but DSi mode and some titles want the
real BIOS/firmware.
Be honest about the two heaviest cores. `dolphin` and `pcsx2` are libretro ports
of emulators whose upstream effort goes into their **standalone** builds; the
cores lag on compatibility and are the first place to look when a GameCube, Wii
or PS2 title misbehaves. If a game does not cooperate, add the standalone
emulators to `environment.systemPackages` in `gaming.nix`:
```nix
pkgs.dolphin-emu # GameCube / Wii
pkgs.pcsx2 # PlayStation 2
```
Both are controller-driven and can be added to Big Picture the same way as
RetroArch. The hardware is not the limit here — a GTX 1070 and a Haswell i7 run
PS2 and Wii comfortably.
Five cores (`snes9x`, `genesis-plus-gx`, `picodrive`, `fbneo`,
`mame2003-plus`) carry upstream licences with non-commercial or
no-redistribution-for-profit clauses, so their derivation names are in
`unfreePackages` in `flake.nix`. Nothing else in the core set needs it.
## Games library layout
Content lives under `/srv/games`, deliberately outside any home directory: it is
bulky, it is the thing most likely to move to its own disk, and it is shared
between Steam, RetroArch and Clone Hero rather than owned by one of them.
Mounting a second drive at `/srv/games` is the only change that move needs.
`gaming.nix` creates the tree with `systemd.tmpfiles.rules` at every boot:
```
/srv/games/
├── roms/ # RetroArch content browser opens here
│ ├── nes/ snes/ gb/ gbc/ gba/ n64/ nds/ gc/ wii/
│ ├── mastersystem/ gamegear/ megadrive/ sega32x/ segacd/
│ ├── saturn/ dreamcast/
│ ├── psx/ ps2/ psp/
│ └── arcade/ dos/
├── bios/ # RetroArch system dir: BIOS and firmware
├── saves/ # in-game saves
├── states/ # save states
├── playlists/
├── screenshots/
├── thumbnails/
├── steam/ # second Steam library folder
└── clonehero/
├── songs/
└── backgrounds/
```
Directory names under `roms/` follow the libretro/ES-DE convention, so a scraper
or a second frontend recognises them without anything being renamed.
Everything is `lyrathorpe:users` mode **2775**. The setgid bit matters: the
owning group is carried onto anything created inside, so a second account — or
an `rsync` from another machine — does not leave behind files the TV user cannot
write. The rules create directories if missing and otherwise leave them alone;
nothing here removes or rewrites content.
RetroArch is pointed at these paths declaratively. The other two have to be told
once, in their own UIs:
- **Steam** — Settings → Storage → the `+` control → add `/srv/games/steam` as a
library folder. Games installed there survive a reinstall of the OS.
- **Clone Hero** — set the song library path to `/srv/games/clonehero/songs` from
its settings screen. Clone Hero keeps its own config in `~/.clonehero`.
## Steam and Proton
`programs.steam.enable` already arranges most of what Proton needs, and it is
worth recording so it is not re-litigated:
- `hardware.graphics` with `enable32Bit` — the 32-bit GL/Vulkan userspace
Proton's 32-bit prefixes link against.
- Steam's udev rules (`hardware.steam-hardware.enable`) — controller and
hidraw access, which is also what lets `dualsensectl` talk to a DualSense.
- 32-bit PipeWire ALSA (`services.pipewire.alsa.support32Bit`), derived from
`alsa.enable`, which `gaming.nix` turns on for the older native titles that
talk to ALSA directly rather than through the Pulse shim.
- Wine's fonts — Liberation, DejaVu, FreeFont and the Noto set arrive with
`fonts.enableDefaultPackages` plus `modules/common-nixos.nix`. Liberation is
metric-compatible with the Microsoft core fonts, so text lays out correctly
without shipping unfree `corefonts`.
- `vm.max_map_count` is **1048576** in the nixpkgs default sysctls, above what
DX12 and Unreal titles need. No override required — this is the one people
usually copy from Arch wiki posts and it is already handled.
What is **not** covered by default, and is set explicitly in `gaming.nix`:
- `systemd.settings.Manager.DefaultLimitNOFILE = "1024:1048576"`. esync opens
one eventfd per Wine synchronisation object and runs out against systemd's
default 524288 hard limit in the heaviest titles. Only the hard limit is
raised; the soft limit stays at 1024, because lifting that breaks
`select()`-based programs elsewhere on the system.
- `extraCompatPackages = [ pkgs.proton-ge-bin ]`. The module puts its
`steamcompattool` output on `STEAM_EXTRA_COMPAT_TOOLS_PATHS`, which is what
makes **GE-Proton** appear in the client's compatibility list.
- `protontricks.enable` — winetricks against a Proton prefix, the standard
repair when a title needs a runtime (dotnet, vcrun, Media Foundation) Proton
does not ship.
- `programs.gamemode.enable` — applies the performance CPU governor around games
that request it.
One thing is **not declarative**: Steam Play must be switched on in the client,
once, per account — **Settings → Compatibility → Enable Steam Play for all other
titles**. Nix cannot set this; it lives in Steam's own config.
Verify the Proton side after installing:
```sh
vulkaninfo --summary # 64-bit ICD
nvidia-smi # driver up
ulimit -Hn # expect 1048576
# in a game's launch options, to confirm esync/fsync are active:
# PROTON_LOG=1 %command% → ~/steam-<appid>.log
```
## Controllers
- **Xbox One / Series over Bluetooth** — `hardware.xpadneo.enable`. The
out-of-tree driver; the in-kernel `xpad` handles these badly over Bluetooth
(wrong button mapping, no rumble). The module enables bluez itself.
- **Xbox 360, wired** — in-kernel `xpad`, autoloaded by udev on plug-in.
Nothing to configure. The kernel is built with `CONFIG_JOYSTICK_XPAD=m`,
`CONFIG_JOYSTICK_XPAD_FF=y` (rumble) and `CONFIG_JOYSTICK_XPAD_LEDS=y`. The
360 wireless PC receiver uses the same driver and works the same way.
- **DualSense / DualShock 4** — in-kernel `hid-playstation`, over both USB and
Bluetooth. No driver config. `dualsensectl` is installed for LED, battery and
microphone control; it works because Steam's udev rules grant hidraw access.
- **Clone Hero guitars** — plain USB HID gamepads, handled in-kernel. Nothing to
configure.
`hardware.bluetooth.powerOnBoot` is set so the adapter is up before the Steam
session starts and a pad can reconnect unattended.
`settings.General.Experimental = true` is what enables battery level reporting
for Bluetooth gamepads — it is still behind bluez's experimental flag.
Pair a new controller from Big Picture (**Settings → Controller**), or from a
Sway session with `bluetoothctl`. If a pad connects but no input arrives, check
`journalctl -b -u bluetooth` and confirm `hid_xpadneo` is loaded
(`lsmod | grep xpadneo`).
The Xbox One / Series USB **wireless dongle** is deliberately not configured. It
needs `hardware.xone.enable`, which **blacklists `xpad`** — that would break the
wired 360 pads — as well as `mt76x2u`, and pulls in proprietary dongle firmware.
Not worth the side effects unless that dongle is actually in use, and if it ever
is, the 360 pads have to be re-tested.
## Untested claims
This host has not been built or booted yet. Two things are worth watching on
first boot:
- **gamescope on the proprietary NVIDIA driver.** `gaming.nix` sets
`GBM_BACKEND=nvidia-drm` and `__GLX_VENDOR_LIBRARY_NAME=nvidia` for the
session, which is the standard fix, but the combination has a history of
needing tweaks. If the session dies at startup, switch the greeter back to
interactive by commenting out `services.greetd.settings.initial_session`, log
into Sway, and read `journalctl --user -b`.
- **Television resolution and refresh.** gamescope takes the output's native
mode by default. Add `gamescopeSession.args = [ "-W" "3840" "-H" "2160" "-r"
"60" ]` if a specific mode is wanted.
There is no HDMI-CEC configuration here, so the TV remote will not drive the
box; that needs a Pulse-Eight adapter or a working CEC bridge on the board.
Nothing boots to a splash screen either — Plymouth was left out deliberately, as
early KMS with the proprietary driver makes it unreliable.
## Networking
Wired NetworkManager from `../../modules/desktop.nix`; `modules/ssh.nix` adds
key-only sshd, which is the practical way to administer a machine with no
keyboard attached. The firewall is default-deny (`modules/workstation.nix`);
Steam Remote Play and local network game transfers open their own ports through
`programs.steam`.