dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

4ec10ba

Match CachyOS's NVIDIA driver; add the benchmark comparison

By Gustaf Granström

The kernel's package set comes from the nixpkgs the CachyOS kernel flake pins,
which still had 615.71.09. Build 615.78.08, the driver CachyOS runs, from the
system nixpkgs' definition against the same kernel until the pinned set
catches up.

tools/bench-compare reads MangoHud logs from both systems and compares
average FPS, 1% and 0.1% lows. The backup steps in MIGRATION.md can now be run
ahead of time and repeated before the wipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

4 files changed +159 −8

modifiedMIGRATION.md
@@ -18,12 +18,18 @@
1818 - [ ] Optional, but this is the last chance: record WoW's performance on
1919 CachyOS, to compare with NixOS later. In Faugus, edit Battle.net, turn
2020 on MangoHud and add this line to the launch arguments:
21- `MANGOHUD_CONFIG=read_cfg,output_folder=/home/vizie/benchmarks,log_duration=60`.
21+ `MANGOHUD_CONFIG=read_cfg,output_folder=/home/vizie/benchmarks,log_duration=60,log_interval=0`.
2222 In game, stand somewhere you can return to (a busy city), keep the camera
2323 still and press Left Shift+F2. Do it three times. The logs land in
24- `~/benchmarks`, which the backup includes.
24+ `~/benchmarks`, which the backup includes. Do the same on NixOS later,
25+ then compare the two with `~/git/dot/tools/bench-compare`.
26+
27+The backup can be made ahead of time, days before the install if you like.
28+Right before the wipe you run the two rsync commands below again (and the
29+"save the built system" step, if the config changed since); they only copy
30+what changed, so that takes a minute or two.
2531
26-**Prepare the backup drive.** This erases `sda`, the 112 GB Kingston, which
32+**Prepare the backup drive** (once). This erases `sda`, the 112 GB Kingston, which
2733 is empty now. Check that the name points at `sda` first:
2834
2935 ```bash
@@ -51,7 +57,7 @@
5157 settings, `Interface` is the addons):
5258
5359 ```bash
54-cp -a ~/Games/battlenet/drive_c/'Program Files (x86)'/'World of Warcraft'/_retail_/{WTF,Interface} /mnt/backup/wow/
60+rsync -a --delete ~/Games/battlenet/drive_c/'Program Files (x86)'/'World of Warcraft'/_retail_/{WTF,Interface} /mnt/backup/wow/
5561 ```
5662
5763 **Back up your home folder.** Quit Signal, Discord, Firefox, Claude and the
@@ -62,11 +68,13 @@
6268 16 GB) with an extra `--exclude=/git/skate3/`:
6369
6470 ```bash
65-sudo rsync -aHAXx --info=progress2 --filter='merge /home/vizie/git/dot/backup-filter' /home/vizie/ /mnt/backup/vizie/
71+sudo rsync -aHAXx --delete --info=progress2 --filter='merge /home/vizie/git/dot/backup-filter' /home/vizie/ /mnt/backup/vizie/
6672 ```
6773
6874 Check that it looks complete (`ls -a /mnt/backup/vizie`, `du -sh /mnt/backup/*`),
69-then `sudo umount /mnt/backup`.
75+then `sudo umount /mnt/backup`. To update the backup later, mount it again
76+(`sudo mount -o compress=zstd /dev/disk/by-label/backup /mnt/backup`) and rerun
77+the commands above.
7078
7179 ## 2. Install
7280
modifiedREADME.md
@@ -31,6 +31,7 @@
3131 packages.nix tools the scripts call; imsh-clients, philly
3232 config/ the dotfiles themselves
3333 pkgs/ packages not in nixpkgs, or patched
34+tools/bench-compare compares MangoHud logs from CachyOS and NixOS
3435 ```
3536
3637 ## Everyday use
@@ -8,6 +8,26 @@
88 }:
99 let
1010 cfg = config.dot.nvidia;
11+
12+ # The driver CachyOS runs: 615.78.08. The kernel's package set comes from the
13+ # nixpkgs the CachyOS kernel flake pins (cachyos.nix), which can be a driver
14+ # release behind the system's nixpkgs. Then the driver is built from the
15+ # system nixpkgs' definition instead (pkgs/os-specific/linux/nvidia-x11),
16+ # against the same kernel. Once the pinned set catches up, its own driver is
17+ # used again and this can go.
18+ pinnedDriver = config.boot.kernelPackages.nvidiaPackages.latest;
19+ nvidiaDriver =
20+ if lib.versionAtLeast pinnedDriver.version "615.78.08" then
21+ pinnedDriver
22+ else
23+ config.boot.kernelPackages.nvidiaPackages.mkDriver {
24+ version = "615.78.08";
25+ sha256_64bit = "sha256-Pj9t3cLudnoIGFMAr3vjyyhuznZpjS3eNSRZl4LQf/4=";
26+ sha256_aarch64 = "sha256-UHYBE1AYHoGgMfD9AS/jX+S+N8eCDWUft6qolK9Eejc=";
27+ openSha256 = "sha256-HBINiOjL0ZJLIAJeNIBYHBnwgUXtNwPPtnFpAI1YwF4=";
28+ settingsSha256 = "sha256-inDRpG02sdDgHmlqgu/DsgK8OFdOt1fZIYyXdhlGC/c=";
29+ persistencedSha256 = "sha256-RzeR6Ldct6MUxjnXRyThdh5Y3jjMehTVg85MtwuWNX4=";
30+ };
1131 in
1232 {
1333 options.dot.nvidia.enable = lib.mkEnableOption "the NVIDIA driver" // {
@@ -27,8 +47,7 @@
2747
2848 hardware.nvidia = {
2949 open = true;
30- # 615.x, the same branch CachyOS has.
31- package = config.boot.kernelPackages.nvidiaPackages.latest;
50+ package = nvidiaDriver;
3251 modesetting.enable = true;
3352 nvidiaSettings = true;
3453 # Not set up on CachyOS either (no nvidia-suspend services enabled).
@@ -1,0 +1,123 @@
1+#!/usr/bin/env python3
2+"""Compare MangoHud frame logs from CachyOS and NixOS (see MIGRATION.md).
3+
4+ tools/bench-compare every log in ~/benchmarks
5+ tools/bench-compare DIR|FILE ... the logs given
6+
7+Logs are grouped by the OS MangoHud wrote into each file's header, so logs
8+from both systems can sit in one folder. For each run it shows the average
9+FPS, the 1% and 0.1% lows (the FPS of the slowest 1% and 0.1% of frames) and
10+the longest frame; then each system's average over its runs, and how far
11+NixOS is from CachyOS.
12+
13+Record with log_interval=0 so every frame is logged; with sampled logs the
14+lows are only estimates.
15+"""
16+import csv
17+import statistics
18+import sys
19+from pathlib import Path
20+
21+
22+def read_log(path):
23+ """Return (os name, frame times in ms) from one MangoHud CSV log."""
24+ rows = list(csv.reader(path.open(newline="", errors="replace")))
25+ os_name = "unknown"
26+ frametimes = None
27+ for i, row in enumerate(rows):
28+ cols = [c.strip().lower() for c in row]
29+ if cols[:1] == ["os"] and i + 1 < len(rows) and rows[i + 1]:
30+ os_name = rows[i + 1][0].strip() or os_name
31+ elif "frametime" in cols:
32+ col = cols.index("frametime")
33+ frametimes = []
34+ for data in rows[i + 1 :]:
35+ try:
36+ frametimes.append(float(data[col]))
37+ except (IndexError, ValueError):
38+ continue
39+ break
40+ if not frametimes:
41+ return os_name, []
42+ # Older MangoHud versions log microseconds, newer ones milliseconds.
43+ if statistics.median(frametimes) > 200:
44+ frametimes = [t / 1000 for t in frametimes]
45+ return os_name, [t for t in frametimes if t > 0]
46+
47+
48+def low(frametimes, fraction):
49+ """FPS of the slowest `fraction` of frames (averaged, as MangoHud does)."""
50+ worst = sorted(frametimes, reverse=True)
51+ n = max(1, int(len(worst) * fraction))
52+ return 1000 / statistics.fmean(worst[:n])
53+
54+
55+def stats(frametimes):
56+ return {
57+ "avg": 1000 / statistics.fmean(frametimes),
58+ "1%": low(frametimes, 0.01),
59+ "0.1%": low(frametimes, 0.001),
60+ "max ms": max(frametimes),
61+ }
62+
63+
64+def short_os(name):
65+ lower = name.lower()
66+ if "cachyos" in lower:
67+ return "CachyOS"
68+ if "nixos" in lower:
69+ return "NixOS"
70+ return name
71+
72+
73+def main(args):
74+ targets = [Path(a) for a in args] or [Path.home() / "benchmarks"]
75+ files = []
76+ for target in targets:
77+ if target.is_dir():
78+ files += sorted(target.rglob("*.csv"))
79+ elif target.exists():
80+ files.append(target)
81+ else:
82+ sys.exit(f"bench-compare: {target} doesn't exist")
83+ files = [f for f in files if not f.stem.endswith("_summary")]
84+ if not files:
85+ sys.exit("bench-compare: no MangoHud logs found")
86+
87+ keys = ["avg", "1%", "0.1%", "max ms"]
88+ header = f"{'':38}" + "".join(f"{k:>9}" for k in keys)
89+ groups = {}
90+ print(header)
91+ for f in files:
92+ os_name, frametimes = read_log(f)
93+ if len(frametimes) < 100:
94+ print(f"{f.name[:38]:38} skipped: {len(frametimes)} frames")
95+ continue
96+ s = stats(frametimes)
97+ system = short_os(os_name)
98+ groups.setdefault(system, []).append(s)
99+ label = f"{system} {f.name}"[:38]
100+ print(f"{label:38}" + "".join(f"{s[k]:9.1f}" for k in keys))
101+
102+ print()
103+ print(f"{'average of runs':38}" + "".join(f"{k:>9}" for k in keys))
104+ means = {}
105+ for system, runs in groups.items():
106+ means[system] = {k: statistics.fmean(r[k] for r in runs) for k in keys}
107+ label = f"{system} ({len(runs)} runs)"
108+ print(f"{label:38}" + "".join(f"{means[system][k]:9.1f}" for k in keys))
109+
110+ if "CachyOS" in means and "NixOS" in means:
111+ a, b = means["CachyOS"], means["NixOS"]
112+ diff = "".join(f"{(b[k] - a[k]) / a[k] * 100:+8.1f}%" for k in keys)
113+ print(f"{'NixOS vs CachyOS':38}{diff}")
114+ print("(higher is better for FPS columns, lower is better for max ms)")
115+ # Run-to-run noise on the same system, to judge whether a gap is real.
116+ for system, runs in groups.items():
117+ if len(runs) > 1:
118+ spread = statistics.pstdev(r["avg"] for r in runs)
119+ print(f"{system}: average FPS varies ±{spread:.1f} between runs")
120+
121+
122+if __name__ == "__main__":
123+ main(sys.argv[1:])