# 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. ## Disk `hardware-configuration.nix` is a real `nixos-generate-config` scan from the installed machine: GPT with a vfat ESP at `/boot` and an ext4 root, both addressed by UUID, and no swap device. 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. Three 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. - greetd gives the session the VT for its standard streams, so anything the Steam session prints as it dies is erased the moment the greeter repaints. `initial_session` therefore runs the launcher under `systemd-cat`, and the output is readable after the fact with `journalctl -b -t steam-session`. ### When the television lands on ReGreet instead of Steam greetd starts `initial_session` **once per boot** — the check is the presence of `/run/greetd.run` — and if the session process starts and then exits, greetd falls through to `default_session`, i.e. ReGreet. A gamescope that dies on startup is therefore indistinguishable from autologin never having run, unless the logs are read: ```sh journalctl -b -t steam-session # what the session itself said journalctl -b -u greetd # whether greetd started it at all ls -l /run/greetd.run # exists => greetd has already had its one go ``` If greetd never even reaches the session, it prints `unable to start greeter` and **exits** — there is no greeter to fall back to and the screen stays blank. Recover over SSH (`modules/ssh.nix`), or hold space at power-on and boot the previous generation. To reproduce a session failure by hand, switch to a free VT and run `steam-gamescope` there. Running it from inside Sway is not the same test: gamescope picks its nested Wayland backend when `WAYLAND_DISPLAY` is set, and it is the DRM backend — the one the boot session uses — that is in question. 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, PCSX2 and Clone Hero to Big Picture All three are installed system-wide but are not Steam titles. Added as Steam shortcuts they appear in Big Picture and inherit Steam Input, so the controller works in them without further configuration. Steam's own **Games → Add a Non-Steam Game** is the documented route and is also the one that tends not to work. Both that dialog's **Browse** button and the library-folder picker below go through the XDG desktop portal `FileChooser` interface; when the portal is unreachable the client silently falls back to a legacy chooser that is blank or inert — Valve [#9447](https://github.com/ValveSoftware/steam-for-linux/issues/9447) and [#10415](https://github.com/ValveSoftware/steam-for-linux/issues/10415), neither fixed. Check the portal side before blaming the client: ```sh busctl --user introspect org.freedesktop.portal.Desktop \ /org/freedesktop/portal/desktop | grep FileChooser ``` `modules/sway.nix` enables `xdg-desktop-portal-gtk`, which is the backend that implements `FileChooser` (`xdg-desktop-portal-wlr` implements only screenshot and screencast), and the generated `sway-portals.conf` routes everything but those two to `gtk`. If the interface is missing from the introspection above, the backend is not running and that is the fault to chase. Independent of the dialog, `steamtinkerlaunch` writes `shortcuts.vdf` directly: ```sh # Steam must be closed -- it rewrites shortcuts.vdf on exit. steamtinkerlaunch addnonsteamgame -an="RetroArch" -ep="$(command -v retroarch)" steamtinkerlaunch addnonsteamgame -an="PCSX2" -ep="$(command -v pcsx2-qt)" steamtinkerlaunch addnonsteamgame -an="Clone Hero" -ep="$(command -v clonehero)" ``` ## 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. That has already happened for PS2: the LRPS2 core was not good enough in practice, so **standalone `pkgs.pcsx2` is installed** in `gaming.nix` alongside the core and is what PS2 titles should be run through. `pkgs.dolphin-emu` is not installed; add it the same way if GameCube or Wii turns out to need it. 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. That picker is the broken one described under [Adding RetroArch, PCSX2 and Clone Hero to Big Picture](#adding-retroarch-pcsx2-and-clone-hero-to-big-picture); when it comes up blank, use Steam's own console instead — open Steam with `steam -console`, pick the **CONSOLE** tab and run: ``` library_folder_add /srv/games/steam ``` The directory must already exist and be writable by the user Steam runs as, which `systemd.tmpfiles` guarantees. The command does not create it. - **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-.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. ## Open problems The machine is installed and Sway works. Still outstanding: - **Autologin into Steam does not happen** — the television comes up on ReGreet. Not yet root-caused; the config side is correct (`initial_session` is present in the generated `greetd.toml` and `services.greetd.restart` evaluates to `false`), so the suspicion is that gamescope starts and dies on the proprietary NVIDIA driver, which greetd treats as a finished session and answers by starting the greeter. `gaming.nix` sets `GBM_BACKEND=nvidia-drm` and `__GLX_VENDOR_LIBRARY_NAME=nvidia`, the standard fix, but the combination has a history of needing more. Read `journalctl -b -t steam-session` first — see [When the television lands on ReGreet instead of Steam](#when-the-television-lands-on-regreet-instead-of-steam). - **Steam's file pickers do not open** — both the non-Steam-game **Browse** dialog and the library-folder picker. Upstream client bugs with usable workarounds; see the two sections above. - **Boot into Big Picture has not been exercised**, since autologin has not run. - **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`.