2026-08-05 18:57:23 +01:00
|
|
|
{...}: {
|
|
|
|
|
# Dedicated NVIDIA GPU (single discrete card, no hybrid/PRIME).
|
|
|
|
|
# For Intel+NVIDIA laptops (e.g. the T480), use gpuNvidiaPrime instead.
|
|
|
|
|
flake.nixosModules.gpuNvidia = {
|
|
|
|
|
config,
|
2026-08-06 17:08:55 +01:00
|
|
|
lib,
|
2026-08-05 18:57:23 +01:00
|
|
|
pkgs,
|
|
|
|
|
...
|
|
|
|
|
}: {
|
|
|
|
|
services.xserver.videoDrivers = ["nvidia"];
|
|
|
|
|
|
|
|
|
|
hardware.graphics = {
|
|
|
|
|
enable = true;
|
|
|
|
|
enable32Bit = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
hardware.nvidia = {
|
|
|
|
|
modesetting.enable = true;
|
2026-08-06 17:08:55 +01:00
|
|
|
# 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;
|
2026-08-05 18:57:23 +01:00
|
|
|
nvidiaSettings = true;
|
2026-08-06 17:08:55 +01:00
|
|
|
# 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.
|
2026-08-05 18:57:23 +01:00
|
|
|
powerManagement.enable = true;
|
2026-08-06 17:08:55 +01:00
|
|
|
# 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;
|
2026-08-05 18:57:23 +01:00
|
|
|
package = config.boot.kernelPackages.nvidiaPackages.stable;
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-06 17:08:55 +01:00
|
|
|
# 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 "$@"
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-05 18:57:23 +01:00
|
|
|
environment.systemPackages = with pkgs; [
|
|
|
|
|
vulkan-tools
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
}
|