248 lines
10 KiB
Nix
248 lines
10 KiB
Nix
# 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
|
||
|
|
];
|
||
|
|
}
|