💾 Archived View for perso.pw › blog › articles › nixos-thin-gaming-client.gmi captured on 2023-11-14 at 08:09:37. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-05-24)
-=-=-=-=-=-=-
This article will cover a use case I suppose very personal, but I love the way I solved it so let me share this story.
I'm a gamer, mostly on computer, but I have a big rig running Windows because many games still don't work well with Linux, but I also play video games on my Linux laptop. Unfortunately, my laptop only has an intel integrated graphic card, so many games won't run well enough to be played, so I'm using an external GPU for some games. But it's not ideal, the eGPU is big (think of it as a big shoes box), doesn't have mouse/keyboard/usb connectors, so I've put it into another room with a screen at a height to play while standing up, controller in hands. This doesn't solve everything, but I can play most games running on it and allowing a controller.
But if I install a game on both the big rig and the laptop, I have to manually sync the saves (I'm buying most of the games on GOG which doesn't have a Linux client to sync saves), it's highly boring and error-prone.
So, thanks to NixOS, I made a recipe to generate a USB live media to play on the big rig, using the data from the laptop, so it's acting as a thin client. The idea of a read only media to boot from is very nice, because USB memory sticks are terrible if you try to install Linux on them (I tried many times, it always ended with I/O errors quickly) and there is exactly what you need, generated from a declarative file.
What does it solve concretely? I can play some games on my laptop anywhere on the small screen, I can also play with my eGPU on the standing desk, but now I can also play all the installed games from the big rig with mouse/keyboard/144hz screen.
The generated ISO (USB capable) should come with a desktop environment like Xfce, Nvidia drivers, Steam, Lutris, Minigalaxy and some other programs I like to use, I keep the programs list minimal because I could still use nix-shell to run a program later.
For the system configuration, I declare the user "gaming" with the same uid as the user on my laptop, and use an NFS mount at boot time.
I'm not using Network Manager because I need the system to get an IP before connecting to a user account.
I'll be using flakes for this, it makes pinning so much easier.
I have two files, "flake.nix" and "iso.nix" in the same directory.
flake.nix file:
{ inputs = { nixpkgs.url = "nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs, ... }@inputs: let system = "x86_64-linux"; pkgs = import nixpkgs { inherit system; config = { allowUnfree = true; }; }; lib = nixpkgs.lib; in { nixosConfigurations.isoimage = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ ./iso.nix "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-base.nix" ]; }; }; }
And iso.nix file:
{ config, pkgs, ... }: { # compress 6x faster than default # but iso is 15% bigger # tradeoff acceptable because we don't want to distribute # default is xz which is very slow isoImage.squashfsCompression = "zstd -Xcompression-level 6"; # my azerty keyboard i18n.defaultLocale = "fr_FR.UTF-8"; services.xserver.layout = "fr"; console = { keyMap = "fr"; }; # xanmod kernel for better performance # see https://xanmod.org/ boot.kernelPackages = pkgs.linuxPackages_xanmod; # prevent GPU to stay at 100% performance hardware.nvidia.powerManagement.enable = true; # sound support hardware.pulseaudio.enable = true; # getting IP from dhcp # no network manager networking.dhcpcd.enable = true; networking.hostName = "biggy"; # Define your hostname. networking.wireless.enable = false; # many programs I use are under a non-free licence nixpkgs.config.allowUnfree = true; # enable steam programs.steam.enable = true; # enable ACPI services.acpid.enable = true; # thermal CPU management services.thermald.enable = true; # enable XFCE, nvidia driver and autologin services.xserver.desktopManager.xfce.enable = true; services.xserver.displayManager.lightdm.autoLogin.timeout = 10; services.xserver.displayManager.lightdm.enable = true; services.xserver.enable = true; services.xserver.libinput.enable = true; services.xserver.videoDrivers = [ "nvidia" ]; services.xserver.xkbOptions = "eurosign:e"; time.timeZone = "Europe/Paris"; # declare the gaming user and its fixed password users.mutableUsers = false; users.users.gaming.initialHashedPassword = "$6$bVayIA6aEVMCIGaX$FYkalbiet783049zEfpugGjZ167XxirQ19vk63t.GSRjzxw74rRi6IcpyEdeSuNTHSxi3q1xsaZkzy6clqBU4b0"; users.users.gaming = { isNormalUser = true; shell = pkgs.fish; uid = 1001; extraGroups = [ "networkmanager" "video" ]; }; services.xserver.displayManager.autoLogin = { enable = true; user = "gaming"; }; # mount the NFS before login systemd.services.mount-gaming = { path = with pkgs; [ nfs-utils ]; serviceConfig.Type = "oneshot"; script = '' mount.nfs -o fsc,nfsvers=4.2,wsize=1048576,rsize=1048576,async,noatime t470-eth.local:/home/jeux/ /home/jeux/ ''; before = [ "display-manager.service" ]; wantedBy = [ "display-manager.service" ]; after = [ "network-online.target" ]; }; # useful packages environment.systemPackages = with pkgs; [ bwm_ng chiaki dunst # for notify-send required in Dead Cells file fzf kakoune libstrangle lutris mangohud minigalaxy ncdu nfs-utils steam steam-run tmux unzip vlc xorg.libXcursor zip ]; }
Then I can update the sources using "nix flake lock --update-input nixpkgs", that will tell you the date of the nixpkgs repository image you are using, and you can compare the dates for updating. I recommend using a program like git to keep track of your files, if you see a failure with a more recent nixpkgs after the lock update, you can have fun pinpointing the issue and reporting it, or restoring the lock to the previous version and be able to continue building ISOs.
You can build the iso with the command "nix build .#nixosConfigurations.isoimage.config.system.build.isoImage", this will create a symlink "result" in the directory, containing the ISO that you can burn on a disk or copy to a memory stick using dd.
Of course, because I'm using NFS to share the data, I need to configure my laptop to serves the files over NFS, this is easy to achieve, just add the following code to your "configuration.nix" file and rebuild the system:
services.nfs.server.enable = true; services.nfs.server.exports = '' /home/gaming 10.42.42.141(rw,nohide,insecure,no_subtree_check) '';
If like me you are using the firewall, I'd recommend opening the NFS 4.2 port (TCP/2049) on the Ethernet interface only:
networking.firewall.enable = true; networking.firewall.allowedTCPPorts = [ ]; networking.firewall.allowedUDPPorts = [ ]; networking.firewall.interfaces.enp0s31f6.allowedTCPPorts = [ 2049 ];
In this case, you can see my NFS client is 10.42.42.141, and previously the NFS server was referred to as laptop-ethernet.local which I declare in my LAN unbound DNS server.
You could make a specialisation for the NFS server part, so it would only be enabled when you choose this option at boot.
If you have a few GB of spare memory on the gaming computer, you can enable cachefilesd, a service that will cache some NFS accesses to make the experience even smoother. You need memory because the cache will have to be stored in the tmpfs and it needs a few gigabytes to be useful.
If you want to enable it, just add the code to the iso.nix file, this will create a 10 MB * 300 cache disk. As tmpfs lacks user_xattr mount option, we need to create a raw disk on the tmpfs root partition and format it with ext4, then mount on the fscache directory used by cachefilesd.
services.cachefilesd.enable = true; services.cachefilesd.extraConfig = '' brun 6% bcull 3% bstop 1% frun 6% fcull 3% fstop 1% ''; # hints from http://www.indimon.co.uk/2016/cachefilesd-on-tmpfs/ systemd.services.tmpfs-cache = { path = with pkgs; [ e2fsprogs busybox ]; serviceConfig.Type = "oneshot"; script = '' if [ ! -f /disk0 ]; then dd if=/dev/zero of=/disk0 bs=10M count=600 echo 'y' | mkfs.ext4 /disk0 fi mkdir -p /var/cache/fscache mount | grep fscache || mount /disk0 /var/cache/fscache -t ext4 -o loop,user_xattr ''; before = [ "cachefilesd.service" ]; wantedBy = [ "cachefilesd.service" ]; };
Opening an NFS server on the network must be done only in a safe LAN, however I don't consider my gaming account to contain any important secret, but it would be bad if someone on the LAN mount it and delete all the files.
However, there are two NFS alternatives that could be used:
The generated ISO can be reduced in size by removing some packages.
for example Gnome comes with orca which will bring many dependencies for text-to-speech. You can easily exclude many Gnome packages.
environment.gnome.excludePackages = with pkgs.gnome; [ pkgs.orca epiphany yelp totem gnome-weather gnome-calendar gnome-contacts gnome-logs gnome-maps gnome-music pkgs.gnome-photos ];
I found that Wine came with the Windows compiler as a dependency, but yet it doesn't seem useful for running games in Lutris.
NixOS discourse: Wine installing mingw32 compiler?
It's possible to rebuild Wine used by Lutris without support for the mingw compiler, replace the lutris line in the "systemPackages" list with the following code:
(lutris-free.override { lutris-unwrapped = lutris-unwrapped.override { wine = wineWowPackages.staging.override { mingwSupport = false; }; }; })
Note that I'm using lutris-free which doesn't support Steam because it makes it a bit lighter and I don't need to manage my Steam games with Lutris.
It could be possible to try getting a package from the nix-store on the NFS server before trying cache.nixos.org which would improve bandwidth usage, it's easy to achieve but yet I need to try it in this context.
I found Steam games running with Proton are slow to start. I made a bug report on the Steam Linux client github.
Github: Proton games takes around 5 minutes to start from a network share
This can be solved partially by mounting ~/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var as tmpfs, it will uses less than 650MB.
I really love this setup, I can backup my games and saves from the laptop, play on the laptop, but now I can extend all this with a bigger and more comfortable setup. The USB live media doesn't take long to be copied to a USB memory stick, so in case one is defective, I can just recopy the image. The live media can be booted all in memory then be unplugged, this gives a crazy fast responsive desktop and can't be altered.
My previous attempts at installing Linux on an USB memory stick all gave bad results, it was extremely slow, i/o errors were common enough that the system became unusable after a few hours. I could add a small partition to one disk of the big rig or add a new disk, but this will increase the maintenance of a system that doesn't do much.