diff --git a/README.md b/README.md index bb8fc52..3a88ef5 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ single flake. Defined in the host table in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix): -| Configuration | System | Machine | -| --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | -| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) | -| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) | -| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) | -| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) | -| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) | +| Configuration | System | Machine | +| --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | +| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) | +| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) | +| `lyrathorpe-console` | `x86_64-linux` | Living-room games machine (Haswell i7 + GTX 1070) on a television — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/console/) | +| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) | +| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) | +| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) | Shared layers: `home` (home-manager: shell, git, editor), `modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches), diff --git a/docs/hosts/console.md b/docs/hosts/console.md new file mode 100644 index 0000000..adb490a --- /dev/null +++ b/docs/hosts/console.md @@ -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-.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`. diff --git a/flake.nix b/flake.nix index d8109f4..91cd422 100644 --- a/flake.nix +++ b/flake.nix @@ -112,13 +112,27 @@ # Unfree packages permitted to be built (replaces blanket allowUnfree). # The NVIDIA entries are for the Mac Pro's Quadro P400 (hosts/MacPro31/ - # nvidia.nix); unfree packages are not in the binary cache, so the - # kernel module is compiled on the host. + # nvidia.nix) and the Console host's GTX 1070 (hosts/Console/nvidia.nix); + # unfree packages are not in the binary cache, so the kernel module is + # compiled on the host. The steam/clonehero entries are the Console + # host's games stack (hosts/Console/gaming.nix). unfreePackages = [ "claude-code" "nvidia-x11" "nvidia-kernel-modules" "nvidia-settings" + "steam" + "steam-unwrapped" + "steam-run" + "clonehero" + # RetroArch cores whose upstream licences carry a non-commercial or + # no-redistribution-for-profit clause. Everything else in the core set + # is plain free software. + "libretro-snes9x" + "libretro-genesis-plus-gx" + "libretro-picodrive" + "libretro-fbneo" + "libretro-mame2003-plus" ]; # Per-user identity, keyed by username. See README "Users". @@ -293,6 +307,28 @@ ]; }; + lyrathorpe-console = { + system = "x86_64-linux"; + portable = false; + # Living-room games machine on a television: autologins into the + # gamescope Steam session (hosts/Console/gaming.nix). sway.nix is + # still imported -- greetd/ReGreet is what the Steam session falls + # back to, and Sway is the keyboard-and-mouse session behind it. + modules = [ + ./hosts/Console/configuration.nix + ./modules/desktop.nix + ./modules/ssh.nix + inputs.nixos-hardware.nixosModules.common-pc-ssd + inputs.nixos-hardware.nixosModules.common-cpu-intel + ./modules/sway.nix + ]; + users.lyrathorpe.homeModules = [ + ./home + ./users/lyrathorpe/home.nix + ./home/desktop.nix + ]; + }; + emmathorpe-edaas = { system = "x86_64-linux"; modules = [ diff --git a/hosts/Console/configuration.nix b/hosts/Console/configuration.nix new file mode 100644 index 0000000..fbceb7b --- /dev/null +++ b/hosts/Console/configuration.nix @@ -0,0 +1,36 @@ +# Living-room games machine: 4th-gen Core i7 (Haswell) on a UEFI board, wired to +# a television and driven from the sofa with a Bluetooth controller. Desktop host +# -- shared graphical/wired options live in ../../modules/desktop.nix; only +# host-specific settings are here. The games stack (Steam session, RetroArch, +# controllers) is in ./gaming.nix and the GPU in ./nvidia.nix. Install notes: +# see ../../docs/hosts/console.md. +{ ... }: + +{ + imports = [ + ./hardware-configuration.nix + ./nvidia.nix + ./gaming.nix + ]; + + # Haswell: AVX2/FMA/BMI2, i.e. x86-64-v3. Above the fleet default (2), so this + # only records the fact -- no feature flag currently keys off level 3. + features.cpu.microarchLevel = 3; + + # Ordinary PC UEFI firmware: systemd-boot, and NVRAM writes are safe here + # (unlike the Mac Pro's Apple EFI, which cannot be trusted with efibootmgr). + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + # The boot menu is unreadable from a sofa and unusable without a keyboard. + # Boot the default immediately; hold space at power-on to get the menu back. + boot.loader.timeout = 0; + # Bound the entry list so the ESP does not fill up with old generations. + boot.loader.systemd-boot.configurationLimit = 10; + + networking.hostName = "Console-NixOS"; + + hardware.cpu.intel.updateMicrocode = true; + + # See `man configuration.nix` / the stateVersion docs before changing. + system.stateVersion = "26.05"; +} diff --git a/hosts/Console/gaming.nix b/hosts/Console/gaming.nix new file mode 100644 index 0000000..635ece3 --- /dev/null +++ b/hosts/Console/gaming.nix @@ -0,0 +1,247 @@ +# The games stack for the living-room machine: the Steam session that the TV +# boots into, RetroArch with its cores, Clone Hero, and the controller plumbing. +# +# Session model. greetd (from ../../modules/sway.nix, which enables it for +# ReGreet) gets an `initial_session`, so the machine autologins into the +# gamescope Steam session at boot -- no keyboard, no greeter, straight to Big +# Picture. Quitting Steam drops back to greetd's `default_session`, i.e. ReGreet, +# where the ordinary Sway session can be picked for keyboard-and-mouse work. +{ pkgs, ... }: + +let + # The account the television autologins as. Must match the user declared for + # this host in the flake host table. + tvUser = "lyrathorpe"; + + # Games library root. Deliberately outside any home directory: content is + # bulky, is the thing most likely to move to its own disk, and is shared + # between Steam, RetroArch and Clone Hero rather than owned by one of them. + # Mounting a second drive at this path is the only change that needs. + gamesRoot = "/srv/games"; + + # One ROM directory per emulated system. Names follow the libretro/ES-DE + # convention so a scraper or a second frontend recognises them without + # renaming anything. + romSystems = [ + "nes" + "snes" + "gb" + "gbc" + "gba" + "n64" + "nds" + "gc" + "wii" + "mastersystem" + "gamegear" + "megadrive" + "sega32x" + "segacd" + "saturn" + "dreamcast" + "psx" + "ps2" + "psp" + "arcade" + "dos" + ]; + + # Everything under the root that is not a ROM directory. RetroArch is pointed + # at these below; Steam and Clone Hero have to be told about theirs in their + # own UIs (see docs/hosts/console.md). + libraryDirs = [ + "bios" # RetroArch system directory: BIOS and firmware images + "saves" # in-game saves + "states" # save states + "playlists" + "screenshots" + "thumbnails" + "steam" # add as a Steam library folder from the client + "clonehero/songs" + "clonehero/backgrounds" + ]; + + # RetroArch and the cores this machine is expected to run. The wrapper already + # points RetroArch at the packaged assets, core info and joypad autoconfig + # profiles; `settings` here is merged on top of those. + retroarch = pkgs.retroarch-bare.wrapper { + cores = with pkgs.libretro; [ + # Nintendo + nestopia # NES + snes9x # SNES + gambatte # Game Boy / Color + mgba # Game Boy Advance + mupen64plus # Nintendo 64 + melonds # Nintendo DS + dolphin # GameCube / Wii + # Sega + genesis-plus-gx # Master System / Game Gear / Mega Drive + picodrive # 32X / Mega CD + beetle-saturn # Saturn + flycast # Dreamcast / NAOMI + # Sony + beetle-psx-hw # PlayStation, hardware renderer + pcsx2 # PlayStation 2 (LRPS2); needs a PS2 BIOS in RetroArch's system dir + ppsspp # PSP + # Arcade and PC + fbneo + mame2003-plus + dosbox-pure + ]; + settings = { + # Applied on every launch via --appendconfig, so these three are fixed + # policy rather than saved preferences: changing them in the UI will not + # stick. Everything else stays user-editable as usual. + # + # Ozone is the controller-navigable menu; the TV has no keyboard. + menu_driver = "ozone"; + video_fullscreen = "true"; + # L3+R3 opens the RetroArch menu from inside a running core + # (INPUT_COMBO_L3_R3). Without a pad combo there is no way to exit a game + # without a keyboard, and no retro system this box emulates has L3/R3 on + # its own controller, so the binding cannot collide with a game. + input_menu_toggle_gamepad_combo = "2"; + + # Point RetroArch at the shared library instead of scattering content and + # state through ~/.config/retroarch. Key names are RetroArch's own; an + # unrecognised key in an appended config is ignored silently, so they are + # worth keeping in step with upstream. + system_directory = "${gamesRoot}/bios"; + savefile_directory = "${gamesRoot}/saves"; + savestate_directory = "${gamesRoot}/states"; + playlist_directory = "${gamesRoot}/playlists"; + screenshot_directory = "${gamesRoot}/screenshots"; + thumbnails_directory = "${gamesRoot}/thumbnails"; + # Where the content browser opens, so loading a game is a couple of + # D-pad presses rather than a walk up from the filesystem root. + rgui_browser_directory = "${gamesRoot}/roms"; + }; + }; +in +{ + programs.steam = { + enable = true; + + # Registers the "Steam" wayland session (gamescope wrapping Steam in tenfoot + # mode) with the display manager and installs the steam-gamescope launcher. + gamescopeSession.enable = true; + gamescopeSession.env = { + # gamescope has to be pointed at NVIDIA's GBM implementation and GLX + # vendor explicitly; on the proprietary driver it otherwise fails to get a + # usable device and the session dies at startup. + GBM_BACKEND = "nvidia-drm"; + __GLX_VENDOR_LIBRARY_NAME = "nvidia"; + }; + + # Remote Play and local network game transfers are the point of a TV box on + # the same LAN as a desktop; both need their ports open. + remotePlay.openFirewall = true; + localNetworkGameTransfers.openFirewall = true; + + # Proton-GE, selectable per title in Steam's compatibility settings. Covers + # the titles where Valve's Proton lags on codecs and anti-cheat shims. The + # module puts its steamcompattool output on STEAM_EXTRA_COMPAT_TOOLS_PATHS, + # which is what makes it appear in the client's Proton version list. + extraCompatPackages = [ pkgs.proton-ge-bin ]; + + # Winetricks against a Proton prefix: the standard repair tool when a title + # needs a runtime (dotnet, vcrun, Media Foundation) that Proton does not ship. + protontricks.enable = true; + }; + + # Proton prerequisites beyond what programs.steam already arranges. + # + # Already covered by the steam module, recorded here so it is not re-litigated: + # hardware.graphics 32-bit (the lib32 NVIDIA userspace Proton's 32-bit prefixes + # need), Steam's udev rules, 32-bit PipeWire, and the system fonts Wine renders + # with (Liberation and DejaVu arrive with fonts.enableDefaultPackages). + # vm.max_map_count is 1048576 in the nixpkgs default sysctls, which is above + # what DX12/Unreal titles need -- no override required. + # + # What is not covered: esync opens one eventfd per Wine sync object and runs + # out against systemd's default 524288 hard limit in the heaviest titles. + # Raise the hard limit only; the soft limit stays at the default, because + # lifting that breaks select()-based programs elsewhere on the system. + systemd.settings.Manager.DefaultLimitNOFILE = "1024:1048576"; + + # capSysNice lets gamescope raise its own scheduling priority, which is what + # keeps the compositor smooth while a game saturates the GPU. It installs + # gamescope as a setcap wrapper instead of a plain systemPackages entry; + # /run/wrappers/bin precedes the system profile on PATH, so steam-gamescope + # still resolves it. + programs.gamescope = { + enable = true; + capSysNice = true; + }; + + # Applies the performance CPU governor (and drops it again) around games that + # ask for it; Steam's Proton builds and most native titles do. + programs.gamemode.enable = true; + + # Autologin into the Steam session. The launcher is not exposed as a package + # by the steam module -- it is built inside it and added to + # environment.systemPackages -- so reference it through the system profile. + # greetd's `restart` option defaults to false once initial_session is set, + # which is what stops a logout from looping straight back into autologin. + services.greetd.settings.initial_session = { + command = "/run/current-system/sw/bin/steam-gamescope"; + user = tvUser; + }; + + # Controllers. + # + # Xbox One/Series pads over Bluetooth need xpadneo: the in-kernel xpad driver + # does not handle them well over BT (wrong button mapping, no rumble). The + # module turns on bluez itself; powerOnBoot is set below so the adapter is up + # before the Steam session starts and a pad can reconnect unattended. + # + # Everything else is in-kernel and needs no configuration: wired Xbox 360 pads + # (and the 360 wireless receiver) via xpad, DualSense/DualShock 4 via + # hid-playstation over USB and Bluetooth, and Clone Hero guitars as plain USB + # HID gamepads. xpadneo does not contend with xpad -- it binds Bluetooth HID + # devices, and the 360 pad is not HID-compliant. hardware.xone is deliberately + # left off: it blacklists xpad, which would break the 360 pads. + # + # hidraw access for the PlayStation pads (LED, battery, dualsensectl) comes + # from Steam's udev rules, which programs.steam enables via + # hardware.steam-hardware. + hardware.xpadneo.enable = true; + hardware.bluetooth = { + enable = true; + powerOnBoot = true; + # Battery level reporting for Bluetooth gamepads is still behind bluez's + # experimental flag. + settings.General.Experimental = true; + }; + + # The games library, created at boot so the directories exist before anything + # tries to write into them. Mode 2775 is setgid: the owning group is carried + # onto anything created inside, so a second account (or an rsync from another + # machine) does not end up with files the TV user cannot write. Directories + # are created if missing and otherwise left alone -- nothing here removes or + # rewrites content. + systemd.tmpfiles.rules = + let + dir = path: "d ${path} 2775 ${tvUser} users -"; + in + [ + (dir gamesRoot) + (dir "${gamesRoot}/roms") + ] + ++ map (system: dir "${gamesRoot}/roms/${system}") romSystems + ++ map (sub: dir "${gamesRoot}/${sub}") libraryDirs; + + # 32-bit ALSA for the older native titles that talk to ALSA directly rather + # than through the PulseAudio shim; programs.steam derives + # pipewire.alsa.support32Bit from this. PipeWire itself and the Pulse shim + # come from ../../modules/workstation.nix. + services.pipewire.alsa.enable = true; + + environment.systemPackages = [ + retroarch + pkgs.clonehero + pkgs.dualsensectl # DualSense LED/battery/mic control from the shell + pkgs.mangohud # FPS/frametime overlay; use `mangohud %command%` in Steam + pkgs.vulkan-tools # vulkaninfo, for checking the 32/64-bit ICDs Proton needs + ]; +} diff --git a/hosts/Console/hardware-configuration.nix b/hosts/Console/hardware-configuration.nix new file mode 100644 index 0000000..b935792 --- /dev/null +++ b/hosts/Console/hardware-configuration.nix @@ -0,0 +1,57 @@ +# PLACEHOLDER -- not generated by nixos-generate-config. +# +# This host has not been installed yet, so there is no real hardware scan to +# commit. The values below are the conventional defaults for a Haswell UEFI +# desktop and assume the install labels its partitions `nixos` (root, ext4) and +# `BOOT` (ESP, vfat) -- see docs/hosts/console.md. They exist so the flake +# evaluates in CI; they are not a description of the actual machine. +# +# Replace this 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 loudly on +# a missing device rather than touching the wrong disk. +{ + config, + lib, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "xhci_pci" + "ehci_pci" + "ahci" + "nvme" + "usb_storage" + "usbhid" + "sd_mod" + "sr_mod" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ ]; + + networking.useDHCP = lib.mkDefault true; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/hosts/Console/nvidia.nix b/hosts/Console/nvidia.nix new file mode 100644 index 0000000..9b7e834 --- /dev/null +++ b/hosts/Console/nvidia.nix @@ -0,0 +1,54 @@ +# NVIDIA GeForce GTX 1070 8 GB (Pascal, GP104): proprietary driver for the +# gamescope Steam session and the Sway desktop. +# +# Driver branch: 580 (nvidiaPackages.legacy_580), NOT the nixpkgs default +# (`production`, currently 595.x). 580 is the last branch that supports +# Maxwell/Pascal/Volta -- NVIDIA keeps it as an LTS branch to Aug 2028 -- and a +# newer branch simply will not drive this card. Same constraint as the Mac Pro's +# Quadro P400; see hosts/MacPro31/nvidia.nix. +# +# The driver is unfree, so it is not in the binary cache: the kernel module is +# compiled locally on every kernel bump. +{ config, ... }: + +{ + # Selects the proprietary driver; the module blacklists nouveau/nvidiafb and + # loads nvidia-uvm via a modprobe softdep. Naming is historical -- this option + # drives the kernel/driver choice on Wayland hosts too, which is why it is set + # on a machine that runs no X server. + services.xserver.videoDrivers = [ "nvidia" ]; + + hardware.nvidia = { + package = config.boot.kernelPackages.nvidiaPackages.legacy_580; + # Required for Wayland: sets nvidia-drm.modeset=1 (and fbdev=1), without + # which neither gamescope nor wlroots gets a usable GBM device and both the + # Steam session and Sway fail to start. + modesetting.enable = true; + # The open kernel modules need Turing or later; Pascal must use the closed + # ones. Explicit because the option has no default on driver >= 560. + open = false; + }; + + # The NVIDIA module only puts these in boot.kernelModules when + # services.xserver.enable is true, which is false on this Wayland-only host -- + # so load them explicitly rather than relying on udev modalias autoloading. + # nvidia_uvm is deliberately absent: the module's modprobe softdep pulls it in + # after the GPU device exists, which is the supported ordering. + boot.kernelModules = [ + "nvidia" + "nvidia_modeset" + "nvidia_drm" + ]; + + # wlroots refuses the proprietary NVIDIA driver unless told to proceed. The + # greeter's compositor (cage) and gamescope have no such check; only Sway + # needs the flag, which the module bakes into the wrapper the session's + # .desktop file runs. + programs.sway.extraOptions = [ "--unsupported-gpu" ]; + + # 32-bit driver libraries for 32-bit Steam titles and Proton's 32-bit + # prefixes: hardware.graphics.enable32Bit pulls in the matching lib32 NVIDIA + # userspace. programs.steam (./gaming.nix) sets it too; stated here as well so + # the GPU's 32-bit story lives with the rest of the GPU config. + hardware.graphics.enable32Bit = true; +}