diff --git a/flake.lock b/flake.lock
index e790f68..72a0f31 100644
--- a/flake.lock
+++ b/flake.lock
@@ -706,6 +706,25 @@
"type": "github"
}
},
+ "nixvirt": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1748140003,
+ "narHash": "sha256-DNBZmuk1YRM2PmwbHzVdXumRjCUzQkMarg4iI/37rOQ=",
+ "rev": "5dfe108fd859b122f9a96981cb6bc12297653d6c",
+ "revCount": 407,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/AshleyYakeley/NixVirt/0.6.0/0197059a-e45f-7446-86b5-411ccc894ab0/source.tar.gz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://flakehub.com/f/AshleyYakeley/NixVirt/%2A.tar.gz"
+ }
+ },
"nur": {
"inputs": {
"flake-parts": "flake-parts_5",
@@ -809,6 +828,7 @@
"nixcord": "nixcord",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixpkgs": "nixpkgs_2",
+ "nixvirt": "nixvirt",
"nur": "nur",
"oom-hardware": "oom-hardware",
"personal-website": "personal-website",
diff --git a/flake.nix b/flake.nix
index c396f2d..469de9c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -105,6 +105,11 @@
};
navi.url = "github:cafkafk/navi";
+
+ nixvirt = {
+ url = "https://flakehub.com/f/AshleyYakeley/NixVirt/*.tar.gz";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
};
outputs = inputs:
diff --git a/modules/desktop/niri/default.nix b/modules/desktop/niri/default.nix
index 4b3a494..4324e57 100644
--- a/modules/desktop/niri/default.nix
+++ b/modules/desktop/niri/default.nix
@@ -139,9 +139,17 @@ let
keyringExtra = lib.optionalString keyringEnable keyringNiriStartupKdl;
extraConfigMerged = keyringExtra + rpi5Extra + (userExtra.extraConfig or "");
windowRules = (base.window-rules or [ ]) ++ (userExtra.window-rules or [ ]);
+ userExtraNoSpecial = lib.removeAttrs userExtra [ "window-rules" "extraConfig" "binds" ];
+ mergedBinds =
+ (base.binds or { })
+ // (userExtra.binds or { })
+ // (niriCfg.extraBinds or { });
in
lib.recursiveUpdate base (
- lib.removeAttrs userExtra [ "window-rules" "extraConfig" ]
+ userExtraNoSpecial
+ // {
+ binds = mergedBinds;
+ }
// lib.optionalAttrs (windowRules != [ ]) {
window-rules = windowRules;
}
@@ -195,6 +203,15 @@ in
Merged into shared defaults → `config.kdl` via wrapper-modules. Put raw KDL in `extraConfig`.
'';
};
+
+ extraBinds = lib.mkOption {
+ type = lib.types.attrsOf lib.types.anything;
+ default = { };
+ description = ''
+ Keybind overrides merged with shared defaults and `extraSettings.binds`.
+ Use this when multiple modules need to add binds without overwriting each other.
+ '';
+ };
};
config = lib.mkMerge [
diff --git a/modules/hosts/14900k/_private/displays.nix b/modules/hosts/14900k/_private/displays.nix
index f6b6f75..22529cc 100644
--- a/modules/hosts/14900k/_private/displays.nix
+++ b/modules/hosts/14900k/_private/displays.nix
@@ -1,61 +1,99 @@
-# Monitor layout for 14900k (ported from NixOS-New `hosts/clients/14900k/home.nix`).
-# Niri: `chiasson.desktop.niri.extraSettings` (`extraConfig` KDL + `binds` merged with defaults).
+# Monitor layout for 14900k.
+# NVIDIA (default): DP-2 ultrawide, HDMI-A-3 + DP-4 side/top stack.
+# Intel iGPU (gpu passthru): DP-1 ultrawide, HDMI-A-2 Samsung to the left.
+# Niri: `extraSettings` (KDL via `extraConfig`, other wrapper-modules keys) + `extraBinds` (merged keybinds).
# Hyprland: `chiasson.desktop.hyprland.settings` (merged in HM when `chiasson.desktop.hyprland.enable`).
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
{ config, lib, ... }:
-{
- chiasson.desktop.niri.extraSettings = {
- extraConfig = ''
- output "DP-2" {
- mode "2560x1080@144"
- scale 1.0
- position x=0 y=0
- focus-at-startup
- }
- output "HDMI-A-3" {
- mode "1920x1080@60"
- scale 1.0
- position x=-1920 y=0
- }
- output "DP-4" {
- mode "1920x1080@144"
- scale 1.0
- position x=0 y=-1080
- }
+let
+ gpuPassthru = config.chiasson.system.gpuPassthru.enable;
- '';
+ niriOutputs =
+ if gpuPassthru then
+ ''
+ output "DP-1" {
+ mode "2560x1080@144"
+ scale 1.0
+ position x=0 y=0
+ focus-at-startup
+ }
+ output "HDMI-A-2" {
+ mode "1920x1080@60"
+ scale 1.0
+ position x=-1920 y=0
+ }
+ ''
+ else
+ ''
+ output "DP-3" {
+ mode "2560x1080@144"
+ scale 1.0
+ position x=0 y=0
+ focus-at-startup
+ }
+ output "HDMI-A-3" {
+ mode "1920x1080@60"
+ scale 1.0
+ position x=-1920 y=0
+ }
+ output "DP-4" {
+ mode "1920x1080@144"
+ scale 1.0
+ position x=0 y=-1080
+ }
+ '';
- binds."XF86Tools".spawn = [
- "wpctl"
- "set-mute"
- "@DEFAULT_AUDIO_SOURCE@"
- "toggle"
- ];
- };
-
- chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable (
- let
- monitorList = [
- "DP-2, 2560x1080@144, 0x0, 1"
+ hyprlandMonitors =
+ if gpuPassthru then
+ [
+ "DP-1, 2560x1080@144, 0x0, 1"
+ "HDMI-A-2, 1920x1080@60, -1920x0, 1"
+ ]
+ else
+ [
+ "DP-3, 2560x1080@144, 0x0, 1"
"DP-4, 1920x1080@144, 0x-1080, 1"
"HDMI-A-3, 1920x1080@60, -1920x0, 1"
];
- workspaceList = [
+
+ hyprlandWorkspaces =
+ if gpuPassthru then
+ [
+ "1, monitor:DP-1, default:true"
+ "2, monitor:DP-1"
+ "3, monitor:DP-1"
+ "4, monitor:HDMI-A-2, default:true"
+ "5, monitor:HDMI-A-2"
+ "6, monitor:HDMI-A-2"
+ ]
+ else
+ [
"1, monitor:DP-3, default:true"
"2, monitor:DP-3"
"3, monitor:DP-3"
- "4, monitor:Unknown-2, default:true"
- "5, monitor:Unknown-2"
- "6, monitor:Unknown-2"
+ "4, monitor:HDMI-A-3, default:true"
+ "5, monitor:HDMI-A-3"
+ "6, monitor:HDMI-A-3"
"7, monitor:DP-4"
"8, monitor:DP-4"
"9, monitor:DP-4"
];
- in
- {
- monitor = lib.mkBefore monitorList;
- workspace = workspaceList;
- }
- );
+in
+{
+ chiasson.desktop.niri.extraSettings = {
+ extraConfig = niriOutputs;
+ };
+
+ chiasson.desktop.niri.extraBinds."XF86Tools".spawn = [
+ "wpctl"
+ "set-mute"
+ "@DEFAULT_AUDIO_SOURCE@"
+ "toggle"
+ ];
+
+ chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable {
+ monitor = lib.mkBefore hyprlandMonitors;
+ workspace = hyprlandWorkspaces;
+ };
}
diff --git a/modules/hosts/14900k/_private/gpu-passthru.nix b/modules/hosts/14900k/_private/gpu-passthru.nix
new file mode 100644
index 0000000..d567af4
--- /dev/null
+++ b/modules/hosts/14900k/_private/gpu-passthru.nix
@@ -0,0 +1,71 @@
+# 14900k: RTX 2070 VFIO passthrough + Windows 11 gaming VM on Crucial MX500.
+{ config, lib, ... }:
+let
+ cfg = config.chiasson.system.gpuPassthru;
+in
+{
+ chiasson.system.gpuPassthru = {
+ enable = false;
+ pciIds = [
+ "10de:1f07" # RTX 2070 VGA
+ "10de:10f9" # HDMI audio
+ "10de:1ada" # USB 3.1
+ "10de:1adb" # USB-C UCSI
+ "14c3:7927" # MediaTek MT7927 WiFi (M.2, no Linux driver — Windows only)
+ ];
+ windowsDisk = "/dev/disk/by-id/ata-CT1000MX500SSD1_1830E14B054A";
+ lookingGlass = {
+ enable = true;
+ shmSizeMiB = 32;
+ captureOnFocus = false;
+ };
+ vm = {
+ enable = true;
+ name = "win11";
+ memoryGiB = 16;
+ vcpus = 8;
+ installMode = false;
+ };
+ libvirtUsers = [ "olivier" ];
+ };
+
+ chiasson.desktop.niri.extraBinds = lib.mkIf (cfg.enable && cfg.lookingGlass.enable) {
+ "Mod+J"."spawn-sh" = "looking-glass-toggle";
+ };
+
+ # Applied last in configuration.nix imports — wins over system module defaults.
+ virtualisation.libvirtd.qemu.verbatimConfig = lib.mkIf (cfg.lookingGlass.enable) (
+ lib.mkForce ''
+ namespaces = []
+ seccomp_sandbox = 0
+ security_driver = "none"
+ security_default_confined = 0
+ security_require_confined = 0
+ cgroup_device_acl = [
+ "/dev/null", "/dev/full", "/dev/zero",
+ "/dev/random", "/dev/urandom",
+ "/dev/ptmx", "/dev/kvm", "/dev/kqemu",
+ "/dev/rtc", "/dev/hpet",
+ "/dev/vfio/", "/dev/vfio/vfio",
+ "/dev/kvmfr0", "c 234:* rwm",
+ "/dev/disk/", "/dev/disk/by-id/",
+ "/dev/bus/usb/",
+ ]
+ ''
+ );
+
+ virtualisation.libvirt = lib.mkIf (cfg.enable && cfg.vm.enable) {
+ enable = true;
+ swtpm.enable = true;
+ connections."qemu:///system".domains = [
+ {
+ definition =
+ if cfg.vm.installMode then
+ ./win11-domain-install.xml
+ else
+ ./win11-domain.xml;
+ active = false;
+ }
+ ];
+ };
+}
diff --git a/modules/hosts/14900k/_private/nvidia.nix b/modules/hosts/14900k/_private/nvidia.nix
index 9a83700..f0819fc 100644
--- a/modules/hosts/14900k/_private/nvidia.nix
+++ b/modules/hosts/14900k/_private/nvidia.nix
@@ -1,6 +1,6 @@
-# NVIDIA for host desktop.
+# NVIDIA for host desktop (disabled when GPU passthrough binds the card to VFIO).
{ config, lib, pkgs, ... }:
-{
+lib.mkIf (!config.chiasson.system.gpuPassthru.enable) {
boot.kernelParams = [ "snd_hda_core.gpu_bind=0" ];
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
diff --git a/modules/hosts/14900k/_private/win11-domain-install.xml b/modules/hosts/14900k/_private/win11-domain-install.xml
new file mode 100644
index 0000000..db1652a
--- /dev/null
+++ b/modules/hosts/14900k/_private/win11-domain-install.xml
@@ -0,0 +1,91 @@
+
+
+ win11
+ a7b3c8d1-4e2f-5a6b-9c0d-1e2f3a4b5c6d
+ 16
+ 16
+ 8
+
+ hvm
+ /run/libvirt/nix-ovmf/edk2-x86_64-secure-code.fd
+ /var/lib/libvirt/qemu/nvram/win11_VARS.fd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ destroy
+ restart
+ preserve
+
+ /run/libvirt/nix-emulators/qemu-system-x86_64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /dev/urandom
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/hosts/14900k/_private/win11-domain.xml b/modules/hosts/14900k/_private/win11-domain.xml
new file mode 100644
index 0000000..f0ebc63
--- /dev/null
+++ b/modules/hosts/14900k/_private/win11-domain.xml
@@ -0,0 +1,177 @@
+
+
+ win11
+ a7b3c8d1-4e2f-5a6b-9c0d-1e2f3a4b5c6d
+ 16
+ 16
+ 8
+
+
+
+
+
+
+ hvm
+ /run/libvirt/nix-ovmf/edk2-x86_64-secure-code.fd
+ /var/lib/libvirt/qemu/nvram/win11_VARS.fd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ destroy
+ restart
+ preserve
+
+
+
+
+
+ /run/libvirt/nix-emulators/qemu-system-x86_64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/hosts/14900k/configuration.nix b/modules/hosts/14900k/configuration.nix
index 8da8c14..d834e9f 100644
--- a/modules/hosts/14900k/configuration.nix
+++ b/modules/hosts/14900k/configuration.nix
@@ -6,6 +6,7 @@
self.nixosModules."14900kHardware"
inputs.home-manager.nixosModules.home-manager
inputs.sops-nix.nixosModules.sops
+ inputs.nixvirt.nixosModules.default
self.nixosModules.system
self.nixosModules.desktop
@@ -14,6 +15,7 @@
self.nixosModules."client-services"
./_private/platform.nix
./_private/nvidia.nix
+ ./_private/gpu-passthru.nix
./_private/peripherals.nix
# ./_private/printing-epson.nix
./_private/displays.nix
diff --git a/modules/system/default.nix b/modules/system/default.nix
index 9b634fb..b24e860 100644
--- a/modules/system/default.nix
+++ b/modules/system/default.nix
@@ -17,6 +17,7 @@
self.nixosModules.systemIdeapadMrubyOverlay
self.nixosModules.systemYtDlpTelequebecPatch
self.nixosModules.systemGaming
+ self.nixosModules.systemGpuPassthru
self.nixosModules.systemUconsoleKernelBuilder
self.nixosModules.systemLibrepods
self.nixosModules.systemPalera1n
diff --git a/modules/system/gpu-passthru.nix b/modules/system/gpu-passthru.nix
new file mode 100644
index 0000000..08e1a6c
--- /dev/null
+++ b/modules/system/gpu-passthru.nix
@@ -0,0 +1,383 @@
+{ ... }: {
+ flake.nixosModules.systemGpuPassthru =
+ { config, lib, pkgs, ... }:
+ let
+ cfg = config.chiasson.system.gpuPassthru;
+ vfioIds = lib.concatStringsSep "," cfg.pciIds;
+ # Must match the Windows host installer build (B7-244-4bb2c58f).
+ lgRev = "4bb2c58fb6d0df9e863ad45924dd4decc7e9cf4e";
+ lgVersion = "B7-244-4bb2c58f";
+ # Tarball hashes omit submodules; fetchSubmodules needs the git tree hash.
+ lgHash = "sha256-Dc0sFnJVM0xJ0FfIQqGQKxFUj6mhp3qgB772h3Dgowc=";
+ # https://wiki.nixos.org/wiki/Looking_Glass — systemd 258+ needs namespaces=[]
+ # and an explicit ACL; seccomp can block mmap on /dev/kvmfr0.
+ lgQemuVerbatimConfig = ''
+ namespaces = []
+ seccomp_sandbox = 0
+ security_driver = "none"
+ security_default_confined = 0
+ security_require_confined = 0
+ cgroup_device_acl = [
+ "/dev/null", "/dev/full", "/dev/zero",
+ "/dev/random", "/dev/urandom",
+ "/dev/ptmx", "/dev/kvm", "/dev/kqemu",
+ "/dev/rtc", "/dev/hpet",
+ "/dev/vfio/", "/dev/vfio/vfio",
+ "/dev/kvmfr0", "c 234:* rwm",
+ "/dev/disk/", "/dev/disk/by-id/",
+ "/dev/bus/usb/",
+ ]
+ '';
+ in
+ {
+ options.chiasson.system.gpuPassthru = {
+ enable = lib.mkEnableOption ''
+ Bind passthrough GPU(s) to VFIO, run the host on Intel iGPU, and enable libvirt +
+ Looking Glass for a Windows gaming VM.
+
+ Prerequisites (manual, one-time):
+ - BIOS: VT-d/IOMMU on, Above 4G Decoding / Resizable BAR on
+ - Move host monitors to motherboard (Intel) ports; dummy HDMI/DP plug on passthrough GPU
+ - Wipe the VM disk before first boot: `wipefs -a `
+ - Verify IOMMU groups after switch: GPU functions should be isolated
+
+ Rollout: leave `enable = false` until cabling and disk backup are done, then rebuild and switch.
+ '';
+
+ pciIds = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ example = [ "10de:1f07" "10de:10f9" "10de:1ada" "10de:1adb" ];
+ description = ''
+ PCI vendor:device IDs to bind to vfio-pci (all GPU functions: VGA, audio, USB, etc.).
+ '';
+ };
+
+ windowsDisk = lib.mkOption {
+ type = lib.types.str;
+ default = "";
+ example = "/dev/disk/by-id/ata-CT1000MX500SSD1_1830E14B054A";
+ description = ''
+ Whole-disk by-id path for the Windows VM (raw block passthrough). Must not be mounted by the host.
+ '';
+ };
+
+ lookingGlass = {
+ enable = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = ''
+ Load the kvmfr kernel module and grant QEMU access to /dev/kvmfr0 for Looking Glass
+ (required for NVIDIA VFIO; plain /dev/shm IVSHMEM causes DMA mapping failures).
+ '';
+ };
+
+ shmSizeMiB = lib.mkOption {
+ type = lib.types.int;
+ default = 32;
+ description = ''
+ kvmfr shared memory size (MiB). Must match `kvmfr.static_size_mb` and the VM
+ `memory-backend-file` size in the domain XML.
+ '';
+ };
+
+ captureOnFocus = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = ''
+ Grab keyboard/mouse when the Looking Glass window is focused. Set `false` to
+ keep input on the Linux host while viewing the guest.
+ '';
+ };
+ };
+
+ vm = {
+ enable = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Declaratively define the Windows VM via NixVirt (host module must import NixVirt and set
+ `virtualisation.libvirt.connections`).
+ '';
+ };
+
+ name = lib.mkOption {
+ type = lib.types.str;
+ default = "win11";
+ description = "libvirt domain name.";
+ };
+
+ memoryGiB = lib.mkOption {
+ type = lib.types.int;
+ default = 16;
+ description = "VM RAM in GiB (informational; domain XML is authoritative).";
+ };
+
+ vcpus = lib.mkOption {
+ type = lib.types.int;
+ default = 8;
+ description = "vCPU count (informational; domain XML is authoritative).";
+ };
+
+ installMode = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = ''
+ Use install domain (ISO, no GPU). Set `false` after Windows is installed.
+ '';
+ };
+ };
+
+ hugepages.enable = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Reserve 2 MiB hugepages for Looking Glass IVSHMEM (enable if frame drops).";
+ };
+
+ libvirtUsers = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Users added to libvirtd, kvm, and disk groups.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable (lib.mkMerge [
+ {
+ assertions = [
+ {
+ assertion = cfg.pciIds != [ ];
+ message = "chiasson.system.gpuPassthru: pciIds must list every passthrough GPU function.";
+ }
+ {
+ assertion = cfg.windowsDisk != "";
+ message = "chiasson.system.gpuPassthru: windowsDisk must be set to a /dev/disk/by-id/ path.";
+ }
+ {
+ assertion = cfg.libvirtUsers != [ ];
+ message = "chiasson.system.gpuPassthru: libvirtUsers must name at least one user.";
+ }
+ ];
+
+ boot.kernelParams = [
+ "intel_iommu=on"
+ "iommu=pt"
+ "kvm.ignore_msrs=1"
+ "vfio-pci.ids=${vfioIds}"
+ ];
+
+ boot.initrd.kernelModules = [
+ "vfio_pci"
+ "vfio"
+ "vfio_iommu_type1"
+ ];
+
+ boot.blacklistedKernelModules = [
+ "nouveau"
+ "nvidia"
+ "nvidia_drm"
+ "nvidia_modeset"
+ "i2c_nvidia_gpu"
+ "mt7921e"
+ "mt7925e"
+ ];
+
+ boot.extraModprobeConfig = ''
+ softdep nvidia pre: vfio-pci
+ softdep nvidia_drm pre: vfio-pci
+ softdep nouveau pre: vfio-pci
+ options vfio_iommu_type1 allow_unsafe_interrupts=1
+ '';
+
+ services.xserver.videoDrivers = lib.mkForce [ "modesetting" ];
+
+ hardware.nvidia-container-toolkit.enable = lib.mkForce false;
+ chiasson.system.gaming.sunshine.cudaSupport = lib.mkForce false;
+
+ # libvirt enables ZFS storage pools on Linux by default, which pulls in
+ # zfs-user (currently broken to build on nixos-unstable). Raw block disk
+ # passthrough does not need libvirt ZFS support.
+ nixpkgs.overlays =
+ [
+ (final: prev: {
+ libvirt = prev.libvirt.override { enableZfs = false; };
+ })
+ ]
+ ++ lib.optionals cfg.lookingGlass.enable [
+ (final: prev: {
+ looking-glass-client = prev.looking-glass-client.overrideAttrs (old: {
+ version = lgVersion;
+ src = prev.fetchFromGitHub {
+ owner = "gnif";
+ repo = "LookingGlass";
+ rev = lgRev;
+ hash = lgHash;
+ fetchSubmodules = true;
+ };
+ patches = old.patches;
+ postUnpack = ''
+ echo ${lgRev} > $sourceRoot/VERSION
+ export sourceRoot="$sourceRoot/client"
+ '';
+ });
+ })
+ ];
+
+ programs.virt-manager.enable = true;
+ virtualisation.spiceUSBRedirection.enable = true;
+
+ virtualisation.libvirtd = {
+ enable = true;
+ onBoot = "ignore";
+ onShutdown = "shutdown";
+ qemu = {
+ package = pkgs.qemu_kvm;
+ # GPU + raw block passthrough needs root (or fragile per-device ACLs).
+ runAsRoot = true;
+ swtpm.enable = true;
+ verbatimConfig = lib.mkIf cfg.lookingGlass.enable (lib.mkForce lgQemuVerbatimConfig);
+ };
+ };
+
+ systemd.services.libvirtd.restartTriggers = [
+ "${pkgs.qemu_kvm}/bin/qemu-system-x86_64"
+ ];
+
+ environment.systemPackages =
+ with pkgs;
+ [
+ virt-manager
+ qemu_kvm
+ OVMF
+ edk2
+ looking-glass-client
+ swtpm
+ ]
+ ++ lib.optionals cfg.lookingGlass.enable [
+ (writeShellScriptBin "looking-glass-toggle" ''
+ set -euo pipefail
+ pkill -f 'looking-glass-client' || true
+ exec looking-glass-client -F -m 97
+ '')
+ ];
+
+ # B7 defaults to /dev/kvmfr0; pin SPICE to the fixed port in the domain XMLs.
+ environment.etc."looking-glass-client.ini" = lib.mkIf cfg.lookingGlass.enable (lib.mkForce {
+ text = ''
+ [app]
+ shmFile=/dev/kvmfr0
+ allowDMA=yes
+
+ [win]
+ fullScreen=yes
+
+ [spice]
+ enable=yes
+ host=127.0.0.1
+ port=5900
+ input=yes
+ audio=yes
+ clipboard=yes
+
+ [input]
+ captureOnFocus=${lib.boolToString cfg.lookingGlass.captureOnFocus}
+ escapeKey=97
+ '';
+ });
+
+ users.groups.libvirtd.members = cfg.libvirtUsers;
+ users.users =
+ lib.genAttrs cfg.libvirtUsers (_: {
+ extraGroups = [
+ "libvirtd"
+ "kvm"
+ "disk"
+ ];
+ })
+ // {
+ qemu-libvirtd.extraGroups = [
+ "kvm"
+ "disk"
+ ];
+ };
+ }
+ {
+ systemd.tmpfiles.rules = [
+ "d /var/lib/libvirt/boot 0755 root root -"
+ ];
+ }
+ (lib.mkIf cfg.lookingGlass.enable {
+ boot.extraModulePackages = lib.mkAfter [ config.boot.kernelPackages.kvmfr ];
+ boot.initrd.kernelModules = lib.mkAfter [ "kvmfr" ];
+ boot.kernelModules = lib.mkAfter [ "kvmfr" ];
+ boot.kernelParams = lib.mkAfter [
+ "kvmfr.static_size_mb=${toString cfg.lookingGlass.shmSizeMiB}"
+ ];
+
+ boot.extraModprobeConfig = lib.mkAfter ''
+ options kvmfr static_size_mb=${toString cfg.lookingGlass.shmSizeMiB}
+ '';
+
+ services.udev.packages = [
+ (pkgs.writeTextFile {
+ name = "kvmfr-udev";
+ text = ''
+ SUBSYSTEM=="kvmfr", GROUP="kvm", MODE="0660", TAG+="uaccess"
+ '';
+ destination = "/etc/udev/rules.d/70-kvmfr.rules";
+ })
+ ];
+
+ # insmod from the nix store: extraModulePackages only lands in
+ # /run/booted-system/kernel-modules after a reboot.
+ systemd.services.kvmfr = {
+ description = "Looking Glass kvmfr module";
+ wantedBy = [ "multi-user.target" ];
+ before = [
+ "libvirtd.service"
+ "libvirtd-config.service"
+ "libvirt-guests.service"
+ ];
+ after = [ "systemd-modules-load.service" ];
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ ExecStart = pkgs.writeShellScript "kvmfr-start" (
+ let
+ kvmfrKo = "${config.boot.kernelPackages.kvmfr}/lib/modules/${config.boot.kernelPackages.kernel.modDirVersion}/kernel/drivers/misc/kvmfr.ko";
+ shmMiB = toString cfg.lookingGlass.shmSizeMiB;
+ in
+ ''
+ set -euo pipefail
+ if [ -e /dev/kvmfr0 ] && [ ! -c /dev/kvmfr0 ]; then
+ rm -f /dev/kvmfr0
+ fi
+ if [ -c /dev/kvmfr0 ]; then
+ exit 0
+ fi
+ if ${pkgs.kmod}/bin/lsmod | ${pkgs.gnugrep}/bin/grep -q '^kvmfr '; then
+ ${pkgs.kmod}/bin/rmmod kvmfr
+ fi
+ if [ ! -f ${kvmfrKo} ]; then
+ echo "kvmfr: module missing at ${kvmfrKo}" >&2
+ exit 1
+ fi
+ ${pkgs.kmod}/bin/insmod ${kvmfrKo} static_size_mb=${shmMiB}
+ if [ ! -c /dev/kvmfr0 ]; then
+ echo "kvmfr: /dev/kvmfr0 not created (static_size_mb=${shmMiB})" >&2
+ exit 1
+ fi
+ ''
+ );
+ };
+ };
+ systemd.services.libvirtd = lib.mkIf cfg.lookingGlass.enable {
+ after = [ "kvmfr.service" ];
+ requires = [ "kvmfr.service" ];
+ };
+ })
+ (lib.mkIf cfg.hugepages.enable {
+ boot.kernelParams = [ "hugepagesz=2M" "hugepages=64" ];
+ })
+ ]);
+ };
+}