Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8552bc7bb | ||
|
|
29707ba0f6 | ||
|
|
67bafd4f73 | ||
|
|
b7e6418fa6 | ||
|
|
fc1772e5db | ||
|
|
42c805f430 |
+23
@@ -1 +1,24 @@
|
||||
.session
|
||||
|
||||
# `nix build` / `nixos-rebuild build` output symlink. Points into /nix/store and
|
||||
# churns on every build, so it must not be tracked.
|
||||
result
|
||||
result-*
|
||||
|
||||
# git-secret. `git secret init` writes the random_seed and the `!*.secret`
|
||||
# negation; the rest is hardening.
|
||||
#
|
||||
# WARNING: `!*.secret` only un-ignores the *encrypted* blobs. It does nothing to
|
||||
# protect the plaintext originals -- that relies entirely on `git secret add`
|
||||
# appending each plaintext path below. Always run `git secret add <file>` from
|
||||
# the repo root (never hand-edit .gitsecret/paths/mapping.cfg) and verify with
|
||||
# `git status` before committing.
|
||||
.gitsecret/keys/random_seed
|
||||
# .gitsecret/keys is git-secret's GPG homedir. pubring.kbx is meant to be
|
||||
# committed; private key material and sockets never are.
|
||||
.gitsecret/keys/private-keys-v1.d/
|
||||
.gitsecret/keys/openpgp-revocs.d/
|
||||
.gitsecret/keys/*.gpg~
|
||||
.gitsecret/keys/.#*
|
||||
.gitsecret/keys/S.*
|
||||
!*.secret
|
||||
|
||||
@@ -37,11 +37,33 @@
|
||||
# are declared in the host's disks.nix instead.
|
||||
services.udisks2.enable = true;
|
||||
|
||||
# Run udiskie as a user unit rather than from the hypr autostart — it was
|
||||
# previously only installed, never actually launched. Binding it to
|
||||
# graphical-session.target (which UWSM manages properly) means it starts and
|
||||
# stops with the session instead of leaking across logins.
|
||||
systemd.user.services.udiskie = {
|
||||
# Session daemons run as systemd user units bound to graphical-session.target
|
||||
# (which UWSM manages properly) rather than from the hypr autostart, so they
|
||||
# start and stop with the session instead of leaking across logins.
|
||||
#
|
||||
# hypridle and hyprsunset both ship their own unit, but in
|
||||
# $out/share/systemd/user — NOT $out/lib/systemd/user. That distinction is
|
||||
# why they need redeclaring rather than just enabling:
|
||||
#
|
||||
# * `systemd.packages = [pkgs.hypridle]` would collect nothing. nixpkgs'
|
||||
# generateUnits globs only $pkg/etc/systemd/user and $pkg/lib/systemd/user.
|
||||
# * The units are nonetheless *visible* to the user manager, via
|
||||
# environment.pathsToLink -> /run/current-system/sw/share/systemd/user,
|
||||
# which is on the search path because XDG_DATA_DIRS ends with
|
||||
# /run/current-system/sw/share. But visible is not enabled: nothing
|
||||
# creates the WantedBy symlink, so they sit unstarted.
|
||||
# * `systemd.user.services.<n>.wantedBy` alone would not help either — with
|
||||
# no other keys NixOS synthesises a *replacement* unit with an empty
|
||||
# [Service] section, shadowing upstream's via /etc/systemd/user (which
|
||||
# takes precedence over $XDG_DATA_DIRS).
|
||||
#
|
||||
# So the full definition below is deliberate. It is close to upstream's but
|
||||
# not identical: hyprsunset's `Requires=graphical-session.target` is dropped
|
||||
# on purpose (PartOf already propagates the stop, while Requires would also
|
||||
# make starting hyprsunset by hand pull up the whole session target), and
|
||||
# hypridle gains an explicit `path` because its config shells out.
|
||||
systemd.user.services = {
|
||||
udiskie = {
|
||||
description = "udiskie removable media automounter";
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
@@ -54,6 +76,58 @@
|
||||
};
|
||||
};
|
||||
|
||||
hypridle = {
|
||||
description = "Hyprland idle daemon";
|
||||
documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hypridle"];
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
wantedBy = ["graphical-session.target"];
|
||||
# ~/.config/hypr/hypridle.conf calls out to these: hyprlock (lock_cmd),
|
||||
# pidof (the `pidof hyprlock ||` guard), brightnessctl (backlight
|
||||
# listener), hyprctl (dpms), loginctl + systemctl (sleep/lock),
|
||||
# notify-send (unlock_cmd).
|
||||
#
|
||||
# MAINTENANCE HAZARD: that config lives in a *different* repo (the stow'd
|
||||
# `hypr` package in the dotfiles repo), so this list can silently drift
|
||||
# out of sync with it. A binary used there but missing here fails as a
|
||||
# bare "command not found" inside hypridle, and Restart=on-failure will
|
||||
# NOT catch it because hypridle itself stays up — the idle action just
|
||||
# quietly stops working. Add any new binary to both places.
|
||||
path = with pkgs; [
|
||||
hyprlock
|
||||
hyprland
|
||||
brightnessctl
|
||||
libnotify
|
||||
procps
|
||||
systemd
|
||||
];
|
||||
unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.hypridle}/bin/hypridle";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
hyprsunset = {
|
||||
description = "Hyprland blue-light filter";
|
||||
documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hyprsunset"];
|
||||
partOf = ["graphical-session.target"];
|
||||
after = ["graphical-session.target"];
|
||||
wantedBy = ["graphical-session.target"];
|
||||
unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.hyprsunset}/bin/hyprsunset";
|
||||
Restart = "on-failure";
|
||||
# Intentionally only on hyprsunset, not hypridle/udiskie: upstream's
|
||||
# hyprsunset.service sets Slice=session.slice and upstream's
|
||||
# hypridle.service does not. Not an oversight — don't "fix" it.
|
||||
Slice = "session.slice";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
# hypr* ecosystem (keybinds / hypridle / autostart)
|
||||
hyprlock
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# (`nix run .#sync-hardware`) safe: re-running it can never clobber these
|
||||
# definitions, nor produce duplicates for whichever data drives happened to
|
||||
# be mounted at the time it ran.
|
||||
flake.nixosModules.shadowDisks = {...}: let
|
||||
flake.nixosModules.shadowDisks = {pkgs, ...}: let
|
||||
# Secondary data drives, addressed by filesystem label.
|
||||
#
|
||||
# nofail on all of them: it stops a missing or dying disk from becoming a
|
||||
@@ -74,6 +74,101 @@
|
||||
"/mnt/games" = eagerDrive "GAMES";
|
||||
};
|
||||
|
||||
swapDevices = [];
|
||||
# Swap exists for hibernation, not for memory pressure — 31GiB of RAM on a
|
||||
# desktop rarely needs paging. The hibernation image must fit in swap, so the
|
||||
# floor is "as big as RAM"; 40GiB leaves headroom above that.
|
||||
#
|
||||
# NVIDIA video memory does NOT land here: with
|
||||
# NVreg_PreserveVideoMemoryAllocations=1 (set by
|
||||
# hardware.nvidia.powerManagement.enable) the driver spools the 3090's VRAM
|
||||
# to NVreg_TemporaryFilePath, which is unset and so defaults to /tmp. That is
|
||||
# only safe because /tmp is on this ext4 root — if /tmp ever becomes a tmpfs
|
||||
# (boot.tmp.useTmpfs) that VRAM dump goes into RAM and hibernation breaks.
|
||||
#
|
||||
# On non-btrfs filesystems NixOS creates this with `dd if=/dev/zero`, so the
|
||||
# file is fully allocated rather than sparse. That matters because the kernel
|
||||
# writes swap through the block layer using the extent map swapon built,
|
||||
# bypassing the filesystem — so the mapping has to be stable and complete.
|
||||
#
|
||||
# The failure is loud, not silent, though: swapon refuses a file with holes
|
||||
# outright ("skipping - it appears to have holes"), so a sparse file would
|
||||
# simply leave the system with no swap rather than corrupt anything. Genuine
|
||||
# silent corruption is the copy-on-write case, which is why btrfs needs
|
||||
# chattr +C and a NOCOW-from-creation file.
|
||||
swapDevices = [
|
||||
{
|
||||
device = "/var/lib/swapfile";
|
||||
size = 40 * 1024; # MiB
|
||||
}
|
||||
];
|
||||
|
||||
# Resuming from hibernation needs the kernel to be told where the image is,
|
||||
# and for a swap *file* that means both halves below.
|
||||
#
|
||||
# boot.initrd.systemd.enable is true on this host, which makes this
|
||||
# mandatory rather than optional: under systemd initrd the `resume=` kernel
|
||||
# param is emitted ONLY from boot.resumeDevice. The legacy stage-1 script
|
||||
# that scans swapDevices to find a resume device is not in play. Without
|
||||
# this, hibernate succeeds and the machine then cold-boots, silently
|
||||
# discarding the session.
|
||||
#
|
||||
# resume_offset is the physical block offset of the swapfile's first extent,
|
||||
# because the kernel addresses the file by raw block, not by path. Recompute
|
||||
# it with the following if the swapfile is ever recreated, resized, or moved
|
||||
# — a stale value resumes from the wrong blocks:
|
||||
#
|
||||
# sudo filefrag -v /var/lib/swapfile | awk '$1=="0:" {gsub(/\.\./,"",$4); print $4}'
|
||||
#
|
||||
# filefrag reports in 4096-byte filesystem blocks and the kernel wants
|
||||
# PAGE_SIZE units; on x86_64 both are 4096, so the number transfers directly.
|
||||
boot.resumeDevice = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a";
|
||||
boot.kernelParams = ["resume_offset=499355648"];
|
||||
|
||||
# A stale resume_offset fails *silently*: the kernel finds no swsusp
|
||||
# signature at the given block, logs `PM: Image not found (code -22)`, and
|
||||
# cold-boots — the hibernated session is simply gone, with no error at
|
||||
# hibernate time. Since the value above is hand-copied, check it on every
|
||||
# activation so drift surfaces as a warning instead of as a lost session.
|
||||
#
|
||||
# Two known limits, both acceptable:
|
||||
# * nixpkgs only recreates the swapfile when `size` changes, so on the one
|
||||
# rebuild that resizes it this check still sees the old file and passes;
|
||||
# it fires on the next rebuild instead. It is a drift detector, not a
|
||||
# pre-flight gate.
|
||||
# * Warn rather than fail: activation must not be blocked over this, and it
|
||||
# is expected to read nothing at all on a fresh install where
|
||||
# mkswap-var-lib-swapfile.service has not run yet.
|
||||
system.activationScripts.checkResumeOffset.text = ''
|
||||
swapfile=/var/lib/swapfile
|
||||
want=499355648
|
||||
if [ -f "$swapfile" ]; then
|
||||
got=$(${pkgs.e2fsprogs}/bin/filefrag -v "$swapfile" 2>/dev/null \
|
||||
| ${pkgs.gawk}/bin/awk '$1=="0:" {gsub(/\.\./,"",$4); print $4; exit}') || got=""
|
||||
if [ -n "$got" ] && [ "$got" != "$want" ]; then
|
||||
echo "WARNING: resume_offset is pinned to $want in modules/hosts/shadow/disks.nix," >&2
|
||||
echo " but $swapfile now starts at block $got. Hibernation will cold-boot" >&2
|
||||
echo " and lose the session until boot.kernelParams is updated to $got." >&2
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# This host sleeps via suspend-then-hibernate (hypridle in the dotfiles repo
|
||||
# is what invokes it): S3 first so a quick return is instant, then a real
|
||||
# suspend-to-disk once the machine has clearly been abandoned.
|
||||
#
|
||||
# Deliberately NOT hybrid-sleep, which writes the disk image up front on
|
||||
# every single sleep. Measured here that cost ~59s of filesystem sync plus an
|
||||
# 11GiB write before the screens even went off, and because an S3 wake
|
||||
# invalidates the image the next boot always reported `PM: Image not found
|
||||
# (code -22)` — full hibernation price paid every time, redeemed only on a
|
||||
# power cut. suspend-then-hibernate defers that write until it is actually
|
||||
# worth doing, and unlike hybrid-sleep it is a mode NVIDIA explicitly
|
||||
# supports (see the system-sleep hook installed by the gpuNvidia module).
|
||||
#
|
||||
# HibernateDelaySec is set explicitly because systemd otherwise derives the
|
||||
# handoff from battery discharge estimation, and this desktop has no system
|
||||
# battery — the sole /sys/class/power_supply entry is a Logitech mouse
|
||||
# reporting scope=Device, which is ignored for that purpose.
|
||||
systemd.sleep.settings.Sleep.HibernateDelaySec = "30min";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,93 @@
|
||||
networking = {
|
||||
hostName = "shadow";
|
||||
};
|
||||
|
||||
# Windows lives on the 476G NVMe -- the one with three partitions: 16M
|
||||
# "Microsoft reserved" MSR, 476G NTFS (fs-uuid 70E4E841E4E80AE4, the C:
|
||||
# volume), 832M WinRE. Identified by layout rather than by nvmeXnY on
|
||||
# purpose; see the enumeration note below.
|
||||
#
|
||||
# That disk has NO EFI System Partition of its own -- its 16M partition is a
|
||||
# "Microsoft reserved" MSR with no filesystem. Windows was installed against
|
||||
# the ESP that NixOS now owns: the 1G vfat partition fs-uuid 5873-AD05 on the
|
||||
# 1.9T NVMe (that disk holds only two partitions, the ESP and the ext4 root
|
||||
# deff63fc-1738-4cfc-a79c-06e58c78471a). So both OSes boot from one shared
|
||||
# ESP; there is no separate Windows boot drive.
|
||||
#
|
||||
# The NixOS install reformatted that ESP, which wiped
|
||||
# EFI/Microsoft/Boot/{bootmgfw.efi,BCD} and left Windows unbootable. That has
|
||||
# since been repaired from WinRE and both files are back in place, so the
|
||||
# menuentry below is live. Keep the recovery command to hand in case the ESP
|
||||
# is ever reformatted again -- it has to be run from Windows install media or
|
||||
# WinRE, because the BCD store cannot be generated from Linux:
|
||||
#
|
||||
# bcdboot C:\Windows /s S: /f UEFI (S: = the ESP, fs-uuid 5873-AD05)
|
||||
#
|
||||
# Copying bootmgfw.efi alone is not enough; without BCD it fails 0xc000000f.
|
||||
# Note bcdboot also drops a copy of the Windows boot manager at
|
||||
# EFI/BOOT/bootx64.efi -- see the fallback-path note in core/boot.nix.
|
||||
#
|
||||
# Chainloaded explicitly rather than via grub.useOSProber: deterministic,
|
||||
# pulls in no extra tooling, and avoids probing every disk on each bootloader
|
||||
# install. The `-f` guard makes the entry self-disabling: it renders only
|
||||
# while bootmgfw.efi is actually present, so a future ESP reformat degrades
|
||||
# to a missing menu item rather than one that boots into an error.
|
||||
#
|
||||
# fs-uuid is shadow's ESP, matching the "/boot" entry in disks.nix. Searching
|
||||
# by UUID rather than (hdX,gptN) matters here: NVMe enumeration is not stable
|
||||
# on this board -- /boot has been seen as both nvme0n1p1 and nvme1n1p1. For
|
||||
# the same reason, re-derive device names before running anything
|
||||
# destructive:
|
||||
#
|
||||
# lsblk -o NAME,SIZE,FSTYPE,PARTTYPENAME,UUID,MOUNTPOINT
|
||||
#
|
||||
# Windows' boot files now occupy ~30-60MiB of that shared 1G ESP and Windows
|
||||
# Update rewrites them periodically, which is part of why
|
||||
# grub.configurationLimit is held at 10 in core/boot.nix.
|
||||
#
|
||||
# Before dual-booting, turn off Windows Fast Startup. hiberfil.sys is present
|
||||
# on the 476G NTFS volume, and Fast Startup plus this host's hibernation
|
||||
# setup will corrupt any NTFS volume mounted read-write from both sides.
|
||||
#
|
||||
# On the GRUB script itself:
|
||||
#
|
||||
# The outer `search` + `-f` test run at *boot*, while GRUB parses grub.cfg —
|
||||
# not at Nix eval time. That is why repairing Windows needed no rebuild: the
|
||||
# entry started rendering as soon as bcdboot put bootmgfw.efi back, and it
|
||||
# will stop again by itself if the file disappears.
|
||||
#
|
||||
# The `-n "$esp"` test is a *separate,
|
||||
# nested* if rather than `[ -n "$esp" -a -f ... ]`: a failed search leaves
|
||||
# $esp unset, and GRUB's `test` builtin evaluates both operands of `-a`
|
||||
# rather than short-circuiting, so the combined form would still expand the
|
||||
# `-f` to `()/...` and could complain about a bad device on every boot.
|
||||
# Nesting guarantees the path test is not reached at all.
|
||||
#
|
||||
# No `insmod` is needed for the outer search: GRUB's EFI core image already
|
||||
# embeds part_gpt/fat/search_fs_uuid, since it must find its own $prefix on
|
||||
# the FAT ESP before reading this file. NixOS' own install-grub.pl relies on
|
||||
# exactly that for the search it emits. The insmods inside the menuentry are
|
||||
# kept only because `chain` genuinely is a separate module.
|
||||
#
|
||||
# The inner `search` deliberately repeats the outer one rather than reusing
|
||||
# `set root=$esp`. It costs one extra disk scan, and only when the entry is
|
||||
# actually selected, in exchange for a menuentry that is self-contained and
|
||||
# cannot be broken by anything that clobbers $esp in between. This also
|
||||
# matches the per-entry search install-grub.pl generates.
|
||||
boot.loader.grub.extraEntries = ''
|
||||
search --no-floppy --set=esp --fs-uuid 5873-AD05
|
||||
if [ -n "$esp" ]; then
|
||||
if [ -f ($esp)/EFI/Microsoft/Boot/bootmgfw.efi ]; then
|
||||
menuentry "Windows Boot Manager" --class windows --class os {
|
||||
insmod part_gpt
|
||||
insmod fat
|
||||
insmod chain
|
||||
search --no-floppy --set=root --fs-uuid 5873-AD05
|
||||
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
|
||||
}
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,67 @@
|
||||
efi = {
|
||||
canTouchEfiVariables = true;
|
||||
};
|
||||
# Pinned explicitly. grub.enable defaults to true, so this host silently
|
||||
# switched from systemd-boot (used at install time) to GRUB when this
|
||||
# block was added. NixOS never removes the previous bootloader's files
|
||||
# or its NVRAM entry, so both lived on the ESP and the firmware kept
|
||||
# booting the frozen systemd-boot menu. Keep this explicit, and never
|
||||
# set systemd-boot.enable alongside it.
|
||||
#
|
||||
# Note what this does and does not do. Both of these lines are no-ops
|
||||
# against nixpkgs' defaults (grub.enable is already true,
|
||||
# systemd-boot.enable already false); their value is that a conflicting
|
||||
# definition elsewhere now fails loudly instead of silently winning. They
|
||||
# delete nothing — stale EFI/systemd/, loader/ and the leftover NVRAM
|
||||
# entry have to be removed by hand on any host installed with
|
||||
# systemd-boot. Done on shadow; NOT yet verified on t480.
|
||||
#
|
||||
# This is stock UEFI GRUB: NixOS installs EFI/<distro><esp>/grubx64.efi
|
||||
# (here EFI/NixOS-boot/grubx64.efi) and registers a matching NVRAM entry
|
||||
# via efibootmgr, because canTouchEfiVariables is true. That NVRAM entry
|
||||
# is what makes GRUB the default; NixOS does not manage the removable
|
||||
# fallback path EFI/BOOT/BOOTX64.EFI and we deliberately do not ask it to
|
||||
# (grub.efiInstallAsRemovable, which would need canTouchEfiVariables=false,
|
||||
# is only for firmware that ignores NVRAM entries — neither host needs it).
|
||||
#
|
||||
# On shadow that fallback path is NOT empty, though: bcdboot left a copy of
|
||||
# the *Windows* boot manager at EFI/BOOT/bootx64.efi (byte-identical to
|
||||
# EFI/Microsoft/Boot/bootmgfw.efi). Normal boots are unaffected because the
|
||||
# firmware follows its NVRAM entry to GRUB. But it does mean that if NVRAM
|
||||
# is ever cleared — CMOS reset, battery, firmware update — the fallback
|
||||
# lands on Windows rather than GRUB. Left as-is by choice; recover by
|
||||
# rerunning `nixos-rebuild boot` to reinstate the NVRAM entry.
|
||||
systemd-boot.enable = false;
|
||||
grub = {
|
||||
enable = true;
|
||||
efiSupport = true;
|
||||
device = "nodev";
|
||||
theme = pkgs.catppuccin-grub;
|
||||
# Bound the menu so the ESP cannot fill up with old kernels.
|
||||
# install-grub.pl unlinks any $boot/kernels/* not referenced by a kept
|
||||
# generation, so this genuinely caps ESP usage rather than just
|
||||
# trimming the menu.
|
||||
#
|
||||
# Sizing, measured on shadow rather than assumed. Generations share
|
||||
# aggressively in practice: 10 kept generations referenced 1 bzImage
|
||||
# (13.3MiB) and 3 initrds (~33.4MiB each) for 114MiB total, i.e. ~11MiB
|
||||
# per generation, not ~47MiB. Only a kernel or initrd change mints a new
|
||||
# file — and the initrd does change more often than you'd expect, since
|
||||
# nixpkgs embeds /etc/modprobe.d/nixos.conf inside it, so every kernel
|
||||
# module parameter edit produces a fresh one.
|
||||
#
|
||||
# So the real bound is between ~11MiB and ~47MiB per generation
|
||||
# depending on churn: 10 generations costs ~115MiB typically and ~470MiB
|
||||
# worst case, against a 1GiB ESP that also carries grub/x86_64-efi, the
|
||||
# catppuccin theme, and Windows' own boot files (bootmgfw.efi + BCD,
|
||||
# ~30-60MiB) since Windows was bcdboot'd onto this same ESP. 10 keeps
|
||||
# the worst case under half the partition.
|
||||
#
|
||||
# Keep this conservative: overflow fails as `copy ... or die` in
|
||||
# install-grub.pl, which runs *after* the system profile has already
|
||||
# been switched, so the result is an activated but unbootable
|
||||
# generation. t480 reaches this module too and its ESP may be smaller.
|
||||
configurationLimit = 10;
|
||||
};
|
||||
};
|
||||
plymouth = {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# For Intel+NVIDIA laptops (e.g. the T480), use gpuNvidiaPrime instead.
|
||||
flake.nixosModules.gpuNvidia = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
@@ -15,15 +16,147 @@
|
||||
|
||||
hardware.nvidia = {
|
||||
modesetting.enable = true;
|
||||
# Proprietary kernel modules. Set to true only for Turing+ (RTX 20xx and
|
||||
# newer) where the open modules are well supported.
|
||||
open = false;
|
||||
# Open kernel modules. Mandatory to set explicitly on driver >= 560 —
|
||||
# the option defaults to null there and nixpkgs asserts against null, so
|
||||
# this line cannot simply be dropped.
|
||||
#
|
||||
# true because every card this module targets is Turing or later (RTX 20xx
|
||||
# and newer), which is exactly what upstream recommends open modules for.
|
||||
# Set false only for a genuinely pre-Turing card, which would want the
|
||||
# gpuNvidia module pointed at a legacy package branch anyway.
|
||||
#
|
||||
# This is NOT cosmetic and NOT a version switch: `open` selects between two
|
||||
# real builds of the same release — nvidia-open (~12MB) vs the proprietary
|
||||
# nvidia-kernel-modules (~85MB). It has no effect on driver version, which
|
||||
# comes solely from `package` below.
|
||||
#
|
||||
# Kernel modules only change on reboot, so `cat /proc/driver/nvidia/version`
|
||||
# describes the RUNNING module and can disagree with this setting until the
|
||||
# next boot. Compare /run/booted-system against /run/current-system to see
|
||||
# a pending swap.
|
||||
#
|
||||
# mkDefault because "Turing or later" is an assumption about the hosts that
|
||||
# happen to import this module today, not a property of the module itself.
|
||||
# A future pre-Turing card can then override it at the host level instead of
|
||||
# having to edit this file (and note that without mkDefault, importing both
|
||||
# gpuNvidia and gpuNvidiaPrime would be an outright merge conflict rather
|
||||
# than something resolvable).
|
||||
open = lib.mkDefault true;
|
||||
nvidiaSettings = true;
|
||||
# Fixes suspend/resume corruption on many setups.
|
||||
# Required for suspend/hibernate to come back with an intact framebuffer:
|
||||
# this sets NVreg_PreserveVideoMemoryAllocations=1, so VRAM contents are
|
||||
# spooled out across sleep instead of being discarded.
|
||||
#
|
||||
# The driver spools VRAM to NVreg_TemporaryFilePath, which nixpkgs leaves
|
||||
# unset so it defaults to /tmp. Keep /tmp on a real filesystem — setting
|
||||
# boot.tmp.useTmpfs would route a full VRAM dump into RAM, which on a
|
||||
# large card is enough to break hibernation outright.
|
||||
powerManagement.enable = true;
|
||||
# Which mechanism tells the driver about sleep transitions. Two options:
|
||||
#
|
||||
# true -> NVreg_UseKernelSuspendNotifiers=1; the driver hooks the
|
||||
# in-kernel PM notifier chain and the systemd units are NOT
|
||||
# generated.
|
||||
# false -> nvidia-{suspend,hibernate,resume}.service call nvidia-sleep.sh
|
||||
# around systemd-{suspend,hibernate}.service.
|
||||
#
|
||||
# Pinned to false deliberately. nixpkgs defaults this to
|
||||
# `useOpenModules && version >= 595`, which is silently true here (open =
|
||||
# true above, driver 595), and that path wedges the GPU on resume — the
|
||||
# notifier callback warns and then the card never comes back:
|
||||
#
|
||||
# WARNING: nvidia/nv.c:4564 at nv_restore_user_channels+0x1d9/0x240
|
||||
# nv_pm_notifier+0x35/0xa0 [nvidia]
|
||||
# nvidia-modeset: ERROR: GPU:0: Failed detecting connected display devices
|
||||
# NVRM: GPU0 _issueRpcAndWait: rpcSendMessage failed with status 0x11
|
||||
#
|
||||
# followed by an unrecoverable GSP RPC storm, i.e. black screens until a
|
||||
# hard power cycle. Revisit once a newer driver fixes the notifier path;
|
||||
# test by flipping this to true and confirming a hibernate round-trip
|
||||
# relights the displays before leaving it that way.
|
||||
powerManagement.kernelSuspendNotifier = false;
|
||||
package = config.boot.kernelPackages.nvidiaPackages.stable;
|
||||
};
|
||||
|
||||
# The nvidia-{suspend,hibernate,resume}.service units nixpkgs generates are
|
||||
# wired only to systemd-{suspend,hibernate}.service. suspend-then-hibernate
|
||||
# runs as a single systemd-suspend-then-hibernate.service that performs both
|
||||
# transitions internally, so those units never fire for it and the GPU would
|
||||
# sleep with VRAM unspooled.
|
||||
#
|
||||
# NVIDIA ships a hook that handles exactly this, dispatching on
|
||||
# $SYSTEMD_SLEEP_ACTION and covering the suspend-after-failed-hibernate case
|
||||
# too. nixpkgs sets `systemd.packages = nvidia_x11.out`, but that only
|
||||
# collects lib/systemd/system/, never lib/systemd/system-sleep/, so upstream's
|
||||
# hook is silently discarded. Install it by hand.
|
||||
#
|
||||
# It must NOT be installed verbatim, though. Past its suspend-then-hibernate
|
||||
# branch the hook ends in an unconditional fallthrough:
|
||||
#
|
||||
# case "$1" in post) nvidia-sleep.sh "resume" ;; esac
|
||||
#
|
||||
# For plain suspend/hibernate that duplicates nvidia-resume.service, and the
|
||||
# duplicate is destructive rather than merely redundant: nvidia-sleep.sh's
|
||||
# RestoreVT() reads /run/nvidia-sleep/Xorg.vt_number, *deletes* it, then
|
||||
# chvt's back. `chvt` lives in pkgs.kbd, which nixpkgs puts on
|
||||
# nvidia-resume.service's `path` but which is absent from the stripped PATH
|
||||
# systemd-sleep gives its hooks. So the hook consumes the VT file, fails to
|
||||
# chvt, and the real unit then finds nothing to restore — no VT switch at
|
||||
# all, i.e. the same black-screen-on-resume this module exists to avoid.
|
||||
#
|
||||
# Hence the first line of the wrapper: bail out unless systemd is actually
|
||||
# doing suspend-then-hibernate, the one transition nixpkgs' units do not
|
||||
# cover. Upstream's own comment notes that its suspend-then-hibernate path
|
||||
# deliberately writes /proc/driver/nvidia/suspend directly and leaves the VT
|
||||
# switch to nvidia-resume.service, so gating here loses nothing.
|
||||
#
|
||||
# The wrapper also has to PATCH A HOLE in upstream's dispatch. Its
|
||||
# suspend-then-hibernate branch is:
|
||||
#
|
||||
# case "$1:$SYSTEMD_SLEEP_ACTION" in
|
||||
# post:*) echo resume > ... ;;
|
||||
# pre:hibernate) echo hibernate > ... ;;
|
||||
# pre:suspend-after-failed-hibernate) echo suspend > ... ;;
|
||||
# esac
|
||||
#
|
||||
# There is no `pre:suspend`. But that is exactly the FIRST leg of
|
||||
# suspend-then-hibernate: systemd invokes the hook with $1=pre and
|
||||
# SYSTEMD_SLEEP_ACTION=suspend before the S3 transition. It matches nothing,
|
||||
# the case falls through, and the driver never gets its VRAM-spool handshake
|
||||
# — so with NVreg_PreserveVideoMemoryAllocations=1 the GPU refuses to
|
||||
# suspend and the whole operation aborts:
|
||||
#
|
||||
# nvidia 0000:2d:00.0: PM: failed to suspend async: error -5
|
||||
# PM: Some devices failed to suspend, or early wake event detected
|
||||
# systemd-sleep: Failed to put system to sleep. System resumed again:
|
||||
# Input/output error
|
||||
#
|
||||
# Observed on this host: plain suspend and plain hibernate both worked (their
|
||||
# nvidia-*.service units did the handshake) while suspend-then-hibernate
|
||||
# failed at the S3 leg with no nvidia unit having run at all. So handle
|
||||
# pre:suspend here, the same way upstream handles pre:hibernate, then hand
|
||||
# everything else to upstream unchanged.
|
||||
#
|
||||
# Guarded on the same condition nixpkgs uses for the units: with the kernel
|
||||
# suspend notifier active the driver handles transitions in-kernel and this
|
||||
# hook must not also poke /proc/driver/nvidia/suspend.
|
||||
environment.etc."systemd/system-sleep/nvidia" = lib.mkIf (config.hardware.nvidia.powerManagement.enable
|
||||
&& !config.hardware.nvidia.powerManagement.kernelSuspendNotifier) {
|
||||
source = pkgs.writeShellScript "nvidia-system-sleep-hook" ''
|
||||
[ "$2" = "suspend-then-hibernate" ] || exit 0
|
||||
|
||||
# The gap upstream leaves: the S3 leg of suspend-then-hibernate.
|
||||
if [ "$1" = "pre" ] && [ "$SYSTEMD_SLEEP_ACTION" = "suspend" ]; then
|
||||
if [ -f /proc/driver/nvidia/suspend ]; then
|
||||
echo suspend > /proc/driver/nvidia/suspend
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec ${config.hardware.nvidia.package.out}/lib/systemd/system-sleep/nvidia "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
vulkan-tools
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user