feat(hosts): add lyrathorpe-console, a living-room games machine
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 7m46s
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 7m46s
New x86_64 host for a 4th-gen Core i7 (Haswell) with a GTX 1070 8 GB, wired to a television and driven with a Bluetooth controller. Session: greetd gets an initial_session, so the machine autologins into the gamescope Steam session at boot with no greeter and no keyboard. Quitting Steam falls back to greetd's default_session (ReGreet), where the ordinary Sway session is available for keyboard-and-mouse work. Graphics: the GTX 1070 is Pascal, so it needs driver branch 580 (nvidiaPackages.legacy_580) like the Mac Pro's Quadro P400 -- the nixpkgs default (production, 595.x) dropped Pascal support. Games: RetroArch built through retroarch-bare.wrapper with 17 cores covering NES through PS2, GameCube and Wii; Clone Hero; and Steam with Proton-GE and protontricks. RetroArch's menu toggle is bound to L3+R3, without which there is no way to exit a running core on a machine with no keyboard. Proton prerequisites beyond what programs.steam already arranges: the esync file-descriptor hard limit is raised from systemd's default 524288 to 1048576 (soft limit untouched), and Proton-GE is wired in via extraCompatPackages. vm.max_map_count already defaults high enough in nixpkgs and needs no override. Content lives in a shared /srv/games tree created by systemd.tmpfiles, laid out one directory per system under roms/ using the libretro/ES-DE naming convention, plus bios/, saves/, states/, a Steam library folder and Clone Hero's songs. RetroArch is pointed at it declaratively. hardware-configuration.nix is a placeholder, not a hardware scan: the machine is not installed yet. It assumes partition labels `nixos` and `BOOT` and must be replaced with nixos-generate-config output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1ff333a896
commit
39434b3929
@@ -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`.
|
||||
Reference in New Issue
Block a user