# Hardware enablement beyond nixos-hardware. # # The nixos-hardware "common-*" profiles the installer selects cover the # BASICS (microcode, the Intel/AMD VA-API media stack, weekly fstrim), and # power.nix adds thermald + power-profiles-daemon. This module fills the GAP # above those: broadly-beneficial bits that default ON when the installer # detects the vendor (opt-OUT), and heavier/experimental bits behind opt-IN # toggles. The installer's hardware-db.sh probes what's present and writes the # matching nomarchy.hardware.* into the generated system.nix. # # Audited against the commons so we don't double-set: we add GuC/HuC, the # amd-pstate governor, the AMD VA-API env, GPU-compute runtimes, fprintd, and # the NPU driver — none of which the commons turn on. { config, lib, pkgs, ... }: let cfg = config.nomarchy.hardware; # Fingerprint PAM can follow state.json (menu toggle → next # sys-rebuild), same bridge as autoTimezone (BACKLOG #55). Missing or # invalid JSON fails closed (state-read.nix) instead of a raw stack. hwState = if config.nomarchy.system.stateFile != null then import ../state-read.nix { inherit lib; } config.nomarchy.system.stateFile else { }; pamFromState = (hwState.settings or { }).fingerprint.pam or false; sync = lib.getExe pkgs.nomarchy-state-sync; # The single fingerprint on/off switch (System › Fingerprint). One state key # for one user-facing decision — it drives login/sudo PAM here AND the # hyprlock unlock in modules/home/idle.nix, which reads the same # settings.fingerprint.pam. Two rebuilds, because the two live in different # configurations: sudo the system switch (PAM), then a home switch # (hyprlock). Same user-owns-the-flake shape as nomarchy-autotimezone. # # This does NOT decide whether login prompts at all — auto-login skips the # greeter entirely, so "fingerprint on" adds the finger to whatever prompts # actually happen (sudo, lock screen, and the greeter only when auto-login # is off). See nomarchy-autologin in ./greeter.nix. nomarchy-fingerprint = pkgs.writeShellScriptBin "nomarchy-fingerprint" '' set -e if [ "$(id -u)" -eq 0 ]; then echo "nomarchy-fingerprint: run as your normal user (it sudos the rebuild itself)" >&2 exit 1 fi flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}" cur=$(${sync} get settings.fingerprint.pam 2>/dev/null) || cur=false case "''${1:-toggle}" in on) new=true ;; off) new=false ;; toggle) case "$cur" in true|True) new=false ;; *) new=true ;; esac ;; status) echo "$cur"; exit 0 ;; *) echo "usage: nomarchy-fingerprint [toggle|on|off|status]" >&2; exit 64 ;; esac # Turning it ON with no enrolled finger would advertise a scan that cannot # succeed on every prompt — refuse instead, and say where to go. if [ "$new" = true ] \ && fprintd-list "$USER" 2>/dev/null | grep -qiE 'no fingers enrolled|No devices available'; then notify-send "Fingerprint" "Enroll a finger first (System › Fingerprint › Enroll)." 2>/dev/null || true echo "nomarchy-fingerprint: no finger enrolled — run fprintd-enroll first" >&2 exit 1 fi ${sync} --quiet set settings.fingerprint.pam "$new" --no-switch notify-send "Fingerprint" "Rebuilding…" 2>/dev/null || true sudo nixos-rebuild switch --flake "$flake#default" home-manager switch --flake "$flake" if [ "$new" = true ]; then notify-send "Fingerprint on" "Password or finger — at sudo, the lock screen, and the greeter." 2>/dev/null || true else notify-send "Fingerprint off" "Password only. Enrolled fingers are kept." 2>/dev/null || true fi ''; in { options.nomarchy.hardware = { intel = { enable = lib.mkEnableOption '' Intel CPU/GPU enablement (the installer turns this on when it detects an Intel CPU or GPU). Complements nixos-hardware's common-gpu-intel''; guc = lib.mkOption { type = lib.types.bool; default = cfg.intel.enable; defaultText = lib.literalExpression "config.nomarchy.hardware.intel.enable"; description = '' Load the GPU's GuC/HuC firmware via the i915 param (i915.enable_guc=3) — better power management and HuC-accelerated media. On by default with intel.enable. NOTE: this is the *i915* driver's param; the newer `xe` driver (Lunar Lake / Battlemage / Panther Lake and other recent Xe GPUs) enables GuC by default and ignores it, so the installer turns this off on xe-driver hardware. ''; }; computeRuntime = lib.mkEnableOption '' Intel GPU compute: the OpenCL / Level Zero (intel-compute-runtime) and oneVPL (vpl-gpu-rt) runtimes for GPU compute and transcode. Opt-in (a few hundred MB) — the Intel counterpart to AMD ROCm''; }; amd = { enable = lib.mkEnableOption '' AMD CPU/GPU enablement (installer-set on an AMD CPU or GPU). Complements nixos-hardware's common-cpu-amd / common-gpu-amd''; pstate = lib.mkOption { type = lib.types.bool; default = cfg.amd.enable; defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable"; description = '' Use the amd-pstate EPP driver (amd_pstate=active) — the modern Zen power/perf governor that power-profiles-daemon drives per profile. On by default with amd.enable (broadly beneficial on Zen 2+). ''; }; vaapi = lib.mkOption { type = lib.types.bool; default = cfg.amd.enable; defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable"; description = '' Point VA-API at mesa's radeonsi (LIBVA_DRIVER_NAME=radeonsi) for hardware video decode/encode. On by default with amd.enable. ''; }; rocm = { enable = lib.mkEnableOption '' AMD ROCm: the HIP / OpenCL GPU-compute stack (multi-GB closure). Opt-in — unlocks GPU PyTorch / Ollama on Radeon''; gfxOverride = lib.mkOption { type = lib.types.str; default = ""; example = "11.0.0"; description = '' HSA_OVERRIDE_GFX_VERSION for GPUs ROCm doesn't officially list (e.g. an RDNA3 780M iGPU, gfx1103, needs "11.0.0"). Empty = no override. ''; }; }; }; fingerprint = { enable = lib.mkEnableOption '' a fingerprint reader via fprintd (the installer turns this on when it detects a known reader). Enroll with `fprintd-enroll`''; pam = lib.mkOption { type = lib.types.bool; default = pamFromState; defaultText = lib.literalExpression "(settings.fingerprint.pam from state.json) or false"; description = '' Use the fingerprint for login and sudo (PAM). Opt-in — password-only stays the default for the cautious; enroll a finger first. Defaults from state.json `settings.fingerprint.pam` (System › Fingerprint menu) when set; otherwise false. ''; }; parallel = lib.mkOption { type = lib.types.bool; default = true; description = '' With fingerprint PAM on, accept the password OR a fingerprint at the same prompt (type or touch, whichever comes first) instead of stock pam_fprintd's sequential wait-for-the-reader-then-password. Uses the pam-fprint-grosshack module (an fprintd fork — source-reviewed; every failure path falls through to the normal password rule, so password login can never be locked out by it). Set false for the stock sequential behavior. ''; }; }; npu.enable = lib.mkEnableOption '' the on-die NPU (AI accelerator) kernel driver — amdxdna on AMD (Ryzen AI), intel_vpu on Intel (Core Ultra and newer). Opt-in and experimental: this loads the in-kernel driver only; the userspace runtime (AMD XRT / oneAPI Level Zero NPU) is yours to add. Needs a recent kernel (see latestKernel)''; latestKernel = lib.mkEnableOption '' the latest mainline kernel (pkgs.linuxPackages_latest) instead of the distro default — for very new hardware whose drivers (a fresh NPU, the `xe` GPU driver, new-platform enablement) only landed recently. Off by default; the default kernel already carries amd-pstate and amdxdna (6.14+)''; camera = { hideIrSensor = lib.mkEnableOption '' hiding a dual-sensor webcam's IR (face-unlock) node from PipeWire so apps only ever see the colour camera. Such modules (common on recent ThinkPads) expose the IR sensor as a SECOND, identically-named "Integrated Camera"; selecting it gives a dark, 8-bit-greyscale image — the classic "my webcam is dark" symptom. The installer turns this on when it detects a paired RGB+IR webcam. Only the PipeWire node is disabled — the kernel /dev/video* device stays open, so face-unlock (Howdy) still works. Acts on the V4L2 path only; the libcamera monitor is left untouched, so an external camera you plug in is never affected. See irMatch''; irMatch = lib.mkOption { type = lib.types.str; default = "~.*(Integrated I|IR Camera|Infrared).*"; description = '' WirePlumber regex (matched against a V4L2 node's api.v4l2.cap.card) selecting the IR sensor to hide. The default catches the common dual-sensor naming ("… Integrated I", "IR Camera", "Infrared"); set it to your camera's IR card name if it differs — find it with `v4l2-ctl --list-devices` or `wpctl inspect`. Only consulted when camera.hideIrSensor is on. ''; }; }; i2c = { enable = lib.mkEnableOption '' I2C devices support. Enables access to /dev/i2c-* (useful for RGB controllers, sensors, and DDC/CI monitor control)''; ddcci = lib.mkEnableOption '' the ddcci-driver kernel module to expose external monitors as standard backlight devices via DDC/CI. This allows brightness keys and swayosd to natively control external displays''; }; }; config = lib.mkMerge [ # ── Intel ────────────────────────────────────────────────────────── (lib.mkIf cfg.intel.guc { boot.kernelParams = [ "i915.enable_guc=3" ]; }) (lib.mkIf cfg.intel.computeRuntime { hardware.graphics.extraPackages = with pkgs; [ intel-compute-runtime vpl-gpu-rt ]; }) # ── AMD ──────────────────────────────────────────────────────────── (lib.mkIf cfg.amd.pstate { boot.kernelParams = [ "amd_pstate=active" ]; }) (lib.mkIf cfg.amd.vaapi { # radeonsi itself comes from mesa (via common-gpu-amd); this just steers # libva at it. mkDefault so a hand-set value or another module wins. environment.sessionVariables.LIBVA_DRIVER_NAME = lib.mkDefault "radeonsi"; hardware.graphics.extraPackages = [ pkgs.libva ]; }) (lib.mkIf cfg.amd.rocm.enable { hardware.graphics.extraPackages = [ pkgs.rocmPackages.clr pkgs.rocmPackages.clr.icd ]; environment.sessionVariables = lib.optionalAttrs (cfg.amd.rocm.gfxOverride != "") { HSA_OVERRIDE_GFX_VERSION = cfg.amd.rocm.gfxOverride; }; }) # ── Fingerprint ──────────────────────────────────────────────────── # NixOS defaults security.pam.services.*.fprintAuth to # services.fprintd.enable — so turning on fprintd alone would enable # finger auth for login/sudo/greetd/passwd/… even when the user (or # the migration template) left fingerprint.pam commented off. Force # every interactive service we care about to follow our opt-in flag. (lib.mkIf cfg.fingerprint.enable { services.fprintd.enable = true; # Ships whenever a reader exists, regardless of the pam flag: the # toggle's whole job is to turn the flag back on while it's off. environment.systemPackages = [ nomarchy-fingerprint ]; security.pam.services = lib.genAttrs [ "login" "sudo" "su" "greetd" "hyprlock" "sshd" "passwd" "chsh" "chfn" "chpasswd" "polkit-1" "swaylock" "groupadd" "groupdel" "groupmod" "groupmems" ] (_: { fprintAuth = cfg.fingerprint.pam; } // lib.optionalAttrs (cfg.fingerprint.pam && cfg.fingerprint.parallel) { # Parallel mode: same rule slot as stock fprintd (so ordering — # sufficient, before pam_unix — is inherited), different module. # grosshack prompts for the password itself while polling the # reader; whichever lands first wins. A typed password makes the # rule FAIL with the token stored, and the stock # `auth sufficient pam_unix.so … try_first_pass` right after it # does the actual validation — password stays sufficient on its # own, so a broken reader/fprintd can never lock login out. rules.auth.fprintd.modulePath = lib.mkForce "${pkgs.pam-fprint-grosshack}/lib/security/pam_fprintd_grosshack.so"; }); }) # ── Newest kernel for very-new hardware (opt-in escape hatch) ────── (lib.mkIf cfg.latestKernel { boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest; }) # ── NPU (in-kernel driver only; userspace runtime is BYO) ────────── # The driver has to actually be in the running kernel — warn (don't fail) # when it predates the shipped one, pointing at latestKernel. (lib.mkIf (cfg.npu.enable && cfg.amd.enable) { boot.kernelModules = [ "amdxdna" ]; warnings = lib.optional (!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14") '' nomarchy.hardware.npu: the amdxdna driver needs kernel >= 6.14, but this config ships ${config.boot.kernelPackages.kernel.version}. Set nomarchy.hardware.latestKernel = true.''; }) (lib.mkIf (cfg.npu.enable && cfg.intel.enable) { boot.kernelModules = [ "intel_vpu" ]; warnings = lib.optional (!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.11") '' nomarchy.hardware.npu: the intel_vpu driver (especially for newer NPUs) wants a recent kernel, but this config ships ${config.boot.kernelPackages.kernel.version}. Consider nomarchy.hardware.latestKernel = true.''; }) # ── Webcam: hide a dual-sensor module's IR node ──────────────────── # A built-in RGB+IR webcam exposes its IR (face-unlock) sensor as a second, # identically-named camera; an app that picks it gets a dark greyscale # image. Disable that node on the V4L2 PipeWire path so only the colour # camera is offered. Matched by card name (irMatch). libcamera is left # alone on purpose — an external camera may rely on it, and surgical # internal-only libcamera scoping isn't possible (the distinguishing # device props bind after the monitor rule runs). The kernel /dev/video* # stays open, so Howdy face-unlock still reads the IR sensor directly. (lib.mkIf (cfg.camera.hideIrSensor && config.services.pipewire.wireplumber.enable) { services.pipewire.wireplumber.extraConfig."90-nomarchy-hide-ir-camera" = { "monitor.v4l2.rules" = [ { matches = [ { "api.v4l2.cap.card" = cfg.camera.irMatch; } ]; actions."update-props"."node.disabled" = true; } ]; }; }) # ── I2C / DDC/CI ─────────────────────────────────────────────────── (lib.mkIf cfg.i2c.enable { hardware.i2c.enable = true; }) (lib.mkIf cfg.i2c.ddcci { # The driver needs I2C underneath it hardware.i2c.enable = true; boot.extraModulePackages = [ config.boot.kernelPackages.ddcci-driver ]; boot.kernelModules = [ "ddcci_backlight" ]; }) # ── Sanity ───────────────────────────────────────────────────────── { assertions = [ { assertion = cfg.amd.rocm.enable -> cfg.amd.enable; message = "nomarchy.hardware.amd.rocm.enable needs nomarchy.hardware.amd.enable."; } { assertion = cfg.intel.computeRuntime -> cfg.intel.enable; message = "nomarchy.hardware.intel.computeRuntime needs nomarchy.hardware.intel.enable."; } { assertion = cfg.npu.enable -> (cfg.amd.enable || cfg.intel.enable); message = "nomarchy.hardware.npu.enable needs a detected Intel or AMD platform."; } ]; } ]; }