add work host
This commit is contained in:
parent
0742e53e45
commit
d390530b35
6 changed files with 411 additions and 0 deletions
26
modules/_hardware/work-hardware.nix
Normal file
26
modules/_hardware/work-hardware.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{ config, lib, modulesPath, ... }: {
|
||||
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
|
||||
|
||||
# AMD Ryzen 7 4000 series (Renoir)
|
||||
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usbhid" "sd_mod" ];
|
||||
boot.kernelModules = [ "kvm-amd" "amdgpu" ];
|
||||
boot.extraModulePackages = [];
|
||||
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
|
||||
hardware.cpu.amd.updateMicrocode =
|
||||
lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||
|
||||
# PRIME offload: AMD iGPU renders the display, NVIDIA dGPU handles offloaded workloads.
|
||||
# Fill in the Bus IDs before building the ISO.
|
||||
# To get them: boot any live Linux on the machine and run:
|
||||
# lspci -D | grep -E '(VGA|3D|Display)'
|
||||
# Then convert the PCI address (e.g. 0000:06:00.0) to the format "PCI:6:0:0".
|
||||
hardware.nvidia.prime = {
|
||||
offload.enable = true;
|
||||
offload.enableOffloadCmd = true;
|
||||
amdgpuBusId = "PCI:FILL:IN:ME"; # TODO: AMD Radeon Vega iGPU bus ID
|
||||
nvidiaBusId = "PCI:FILL:IN:ME"; # TODO: NVIDIA GTX 1650 dGPU bus ID
|
||||
};
|
||||
}
|
||||
|
|
@ -17,6 +17,11 @@ in {
|
|||
home-manager.module = inputs.home-manager.nixosModules.home-manager;
|
||||
};
|
||||
|
||||
den.hosts.x86_64-linux.work = {
|
||||
users.zoty = {};
|
||||
home-manager.module = inputs.home-manager.nixosModules.home-manager;
|
||||
};
|
||||
|
||||
den.aspects.notebook = {
|
||||
includes = [
|
||||
den.provides.hostname
|
||||
|
|
@ -33,6 +38,21 @@ in {
|
|||
};
|
||||
};
|
||||
|
||||
den.aspects.work = {
|
||||
includes = [
|
||||
den.provides.hostname
|
||||
den.aspects.core
|
||||
den.aspects.wayland
|
||||
den.aspects.niri
|
||||
den.aspects.work-specific
|
||||
den.aspects.dev
|
||||
den.aspects.secrets
|
||||
];
|
||||
nixos = {
|
||||
system.stateVersion = "25.11";
|
||||
};
|
||||
};
|
||||
|
||||
den.aspects.desktop = {
|
||||
includes = [
|
||||
den.provides.hostname
|
||||
|
|
@ -46,6 +66,7 @@ in {
|
|||
den.aspects.steam
|
||||
den.aspects.prismlauncher
|
||||
den.aspects.androidcam
|
||||
den.aspects.winvm
|
||||
];
|
||||
nixos = {
|
||||
system.stateVersion = "25.11";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
den.aspects.desktop-specific.nixos = { config, pkgs, ... }: {
|
||||
imports = [ ./_hardware/desktop-hardware.nix ];
|
||||
|
||||
local.winvm = {
|
||||
gpuPcieAddress = "0000:01:00.0"; # NVIDIA GeForce RTX 3060 Ti
|
||||
gpuAudioPcieAddress = "0000:01:00.1"; # NVIDIA RTX 3060 Ti HDMI audio
|
||||
};
|
||||
|
||||
hardware = {
|
||||
graphics = {
|
||||
enable = true;
|
||||
|
|
|
|||
146
modules/winvm.nix
Normal file
146
modules/winvm.nix
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
{ den, ... }:
|
||||
{
|
||||
den.aspects.winvm.nixos = { config, pkgs, lib, ... }:
|
||||
let
|
||||
cfg = config.local.winvm;
|
||||
|
||||
hookScript = pkgs.writeShellScript "qemu-hook" ''
|
||||
#!/usr/bin/env bash
|
||||
# Libvirt QEMU hook for single-GPU passthrough.
|
||||
# Libvirt calls this script as: qemu <vm-name> <operation> <sub-operation>
|
||||
# On "prepare begin": unbind GPU from NVIDIA and hand it to the VM.
|
||||
# On "release end": return GPU to the host and restart the display session.
|
||||
|
||||
VM_NAME="$1"
|
||||
OPERATION="$2"
|
||||
|
||||
GPU_ADDR="${cfg.gpuPcieAddress}"
|
||||
GPU_AUDIO_ADDR="${cfg.gpuAudioPcieAddress}"
|
||||
|
||||
# Exit early for any VM that is not the configured passthrough target
|
||||
if [[ "$VM_NAME" != "${cfg.vmName}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bind_vfio() {
|
||||
# Stop greetd before terminating the session so it cannot restart tuigreet
|
||||
systemctl stop greetd.service
|
||||
|
||||
# Terminate the Niri/Wayland session and all user services (including Pipewire)
|
||||
loginctl terminate-user zoty 2>/dev/null || true
|
||||
sleep 3
|
||||
|
||||
# Detach virtual console framebuffer bindings held by the NVIDIA driver
|
||||
echo 0 > /sys/class/vtconsole/vtcon0/bind 2>/dev/null || true
|
||||
echo 0 > /sys/class/vtconsole/vtcon1/bind 2>/dev/null || true
|
||||
|
||||
# Detach the EFI framebuffer platform driver if it is present
|
||||
echo efi-framebuffer.0 \
|
||||
> /sys/bus/platform/drivers/efi-framebuffer/unbind 2>/dev/null || true
|
||||
|
||||
sleep 1
|
||||
|
||||
# Unload NVIDIA kernel modules in reverse dependency order
|
||||
modprobe -r nvidia_drm nvidia_modeset nvidia_uvm nvidia
|
||||
|
||||
# Bind GPU and its HDMI audio function to the vfio-pci driver
|
||||
echo "$GPU_ADDR" > /sys/bus/pci/drivers/vfio-pci/bind
|
||||
echo "$GPU_AUDIO_ADDR" > /sys/bus/pci/drivers/vfio-pci/bind
|
||||
}
|
||||
|
||||
unbind_vfio() {
|
||||
# Release both GPU PCI functions from vfio-pci
|
||||
echo "$GPU_ADDR" > /sys/bus/pci/drivers/vfio-pci/unbind
|
||||
echo "$GPU_AUDIO_ADDR" > /sys/bus/pci/drivers/vfio-pci/unbind
|
||||
|
||||
sleep 1
|
||||
|
||||
# Reload NVIDIA modules in dependency order
|
||||
modprobe nvidia
|
||||
modprobe nvidia_modeset
|
||||
modprobe nvidia_uvm
|
||||
modprobe nvidia_drm
|
||||
|
||||
# Restore the EFI framebuffer driver
|
||||
echo efi-framebuffer.0 \
|
||||
> /sys/bus/platform/drivers/efi-framebuffer/bind 2>/dev/null || true
|
||||
|
||||
# Restore virtual console framebuffer bindings
|
||||
echo 1 > /sys/class/vtconsole/vtcon0/bind 2>/dev/null || true
|
||||
echo 1 > /sys/class/vtconsole/vtcon1/bind 2>/dev/null || true
|
||||
|
||||
# Restart greetd so tuigreet is available for login
|
||||
systemctl start greetd.service
|
||||
}
|
||||
|
||||
case "$OPERATION" in
|
||||
prepare) bind_vfio ;;
|
||||
release) unbind_vfio ;;
|
||||
esac
|
||||
'';
|
||||
|
||||
screamService = {
|
||||
Unit = {
|
||||
Description = "Scream audio receiver for Windows VM";
|
||||
# Start after Pipewire is ready so the sink exists when scream connects
|
||||
After = [ "pipewire-pulse.service" ];
|
||||
Requires = [ "pipewire-pulse.service" ];
|
||||
};
|
||||
Service = {
|
||||
# virbr0 is the default libvirt NAT bridge; scream listens for UDP audio
|
||||
# streams from Windows and forwards them to the PulseAudio/Pipewire sink
|
||||
ExecStart = "${pkgs.scream}/bin/scream -i virbr0 -o pulse";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "default.target" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
options.local.winvm = {
|
||||
gpuPcieAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "PCIe address of the NVIDIA GPU (e.g. 0000:01:00.0).";
|
||||
};
|
||||
gpuAudioPcieAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "PCIe address of the NVIDIA GPU HDMI audio device (e.g. 0000:01:00.1).";
|
||||
};
|
||||
vmName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "windows";
|
||||
description = "Libvirt VM name that triggers the GPU handoff hook.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
boot.kernelParams = [ "intel_iommu=on" "iommu=pt" ];
|
||||
|
||||
boot.kernelModules = [ "vfio" "vfio_pci" "vfio_iommu_type1" ];
|
||||
|
||||
virtualisation.libvirtd = {
|
||||
enable = true;
|
||||
qemu = {
|
||||
package = pkgs.qemu_kvm;
|
||||
runAsRoot = true;
|
||||
swtpm.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
programs.virt-manager.enable = true;
|
||||
|
||||
environment.systemPackages = [ pkgs.virtio-win ];
|
||||
|
||||
users.users.zoty.extraGroups = [ "libvirtd" "kvm" ];
|
||||
|
||||
environment.etc."libvirt/hooks/qemu" = {
|
||||
source = hookScript;
|
||||
mode = "0755";
|
||||
};
|
||||
|
||||
# Scream receiver runs as a user service so it has access to the
|
||||
# Pipewire session. It starts automatically after login and waits
|
||||
# for audio packets from the Windows VM on the libvirt bridge.
|
||||
home-manager.users.zoty.systemd.user.services.scream = screamService;
|
||||
};
|
||||
};
|
||||
}
|
||||
68
modules/work.nix
Normal file
68
modules/work.nix
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{ den, inputs, lib, config, ... }: {
|
||||
den.aspects.work-specific.nixos = { pkgs, config, ... }: {
|
||||
imports = [
|
||||
./_hardware/work-hardware.nix
|
||||
./_hardware/notebook-disko.nix
|
||||
inputs.disko.nixosModules.disko
|
||||
];
|
||||
|
||||
hardware.graphics = {
|
||||
enable = true;
|
||||
enable32Bit = true;
|
||||
};
|
||||
|
||||
hardware.nvidia = {
|
||||
modesetting.enable = true;
|
||||
open = false;
|
||||
nvidiaSettings = true;
|
||||
# Fine-grained power management is required for PRIME offload mode.
|
||||
powerManagement.enable = true;
|
||||
powerManagement.finegrained = true;
|
||||
package = config.boot.kernelPackages.nvidiaPackages.stable;
|
||||
};
|
||||
|
||||
# amdgpu drives the display; nvidia is available for offloaded rendering.
|
||||
services.xserver.videoDrivers = [ "amdgpu" "nvidia" ];
|
||||
|
||||
environment.sessionVariables = {
|
||||
GBM_BACKEND = "nvidia-drm";
|
||||
__GLX_VENDOR_LIBRARY_NAME = "nvidia";
|
||||
};
|
||||
|
||||
security.sudo.wheelNeedsPassword = lib.mkForce true;
|
||||
|
||||
networking.firewall = {
|
||||
enable = true;
|
||||
allowedTCPPorts = [];
|
||||
allowedUDPPorts = [];
|
||||
};
|
||||
|
||||
services.thermald.enable = true;
|
||||
powerManagement.enable = true;
|
||||
|
||||
services.tlp = {
|
||||
enable = true;
|
||||
settings = {
|
||||
CPU_SCALING_GOVERNOR_ON_AC = "performance";
|
||||
CPU_SCALING_GOVERNOR_ON_BAT = "powersave";
|
||||
};
|
||||
};
|
||||
|
||||
boot = {
|
||||
initrd.kernelModules = [ "dm-crypt" "amdgpu" ];
|
||||
initrd.compressor = "zstd";
|
||||
kernelParams = [ "nvidia.NVreg_PreserveVideoMemoryAllocations=1" ];
|
||||
|
||||
loader = {
|
||||
grub = {
|
||||
enable = true;
|
||||
device = "nodev";
|
||||
efiSupport = true;
|
||||
configurationLimit = 2;
|
||||
enableCryptodisk = true;
|
||||
};
|
||||
efi.canTouchEfiVariables = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
145
windows-vm.md
Normal file
145
windows-vm.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Windows 11 VM - Single GPU Passthrough
|
||||
|
||||
Windows 11 virtual machine with NVIDIA GPU passthrough for running Adobe software on the NixOS desktop. Audio is handled via [Scream](https://github.com/duncanthrax/scream) over the libvirt NAT bridge.
|
||||
|
||||
## Hardware
|
||||
|
||||
| Component | Device |
|
||||
|-----------|--------|
|
||||
| CPU | Intel i5-9400F (6 cores, no iGPU) |
|
||||
| GPU | NVIDIA GeForce RTX 3060 Ti |
|
||||
| GPU PCIe | `0000:01:00.0` (VGA), `0000:01:00.1` (HDMI audio) |
|
||||
| Onboard audio | Intel 200 Series PCH HDA at `0000:00:1f.3` - shares IOMMU group 8 with PCH, not passthrough-capable |
|
||||
|
||||
## How it works
|
||||
|
||||
When the VM starts, a libvirt hook script terminates the host graphical session, unloads the NVIDIA kernel modules, and binds the GPU to `vfio-pci`. The monitor then displays the Windows guest directly. When the VM shuts down, the hook reverses the process and restarts greetd.
|
||||
|
||||
Audio uses Scream: a Windows kernel driver streams PCM over UDP to a receiver service running on the host, which forwards it to Pipewire.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before the first switch, enable **Intel VT-d** in the BIOS (Intel Virtualization for Directed I/O). After rebooting, verify IOMMU is active:
|
||||
|
||||
```bash
|
||||
dmesg | grep -i iommu
|
||||
# expected: DMAR: IOMMU enabled
|
||||
```
|
||||
|
||||
Then apply the configuration and reboot:
|
||||
|
||||
```bash
|
||||
just switch desktop
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
## VM setup
|
||||
|
||||
### 1. Create the disk
|
||||
|
||||
```bash
|
||||
mkdir -p ~/vms
|
||||
qemu-img create -f qcow2 ~/vms/windows.qcow2 100G
|
||||
```
|
||||
|
||||
### 2. Get the Windows 11 ISO
|
||||
|
||||
Download from `microsoft.com/software-download/windows11`. Save to `~/vms/win11.iso`.
|
||||
|
||||
### 3. Create the VM in virt-manager
|
||||
|
||||
```bash
|
||||
virt-manager
|
||||
```
|
||||
|
||||
Create a new VM with **"Local install media"**, select `~/vms/win11.iso`, and on the final screen check **"Customize configuration before install"** with name set to `windows`.
|
||||
|
||||
Apply the following settings before starting the installation:
|
||||
|
||||
**Overview**
|
||||
- Chipset: `Q35`
|
||||
- Firmware: any UEFI entry (not secboot for now)
|
||||
|
||||
**CPU**
|
||||
- Enable "Copy host CPU configuration"
|
||||
- Topology: 1 socket, 4 cores, 1 thread
|
||||
|
||||
**Disk**
|
||||
- Bus: `SATA`
|
||||
|
||||
**NIC**
|
||||
- Device model: `virtio`
|
||||
|
||||
**Add hardware - PCI Host Device** (add both)
|
||||
- `0000:01:00.0` - NVIDIA GeForce RTX 3060 Ti
|
||||
- `0000:01:00.1` - NVIDIA RTX 3060 Ti HDMI audio
|
||||
|
||||
**Remove**
|
||||
- Video QXL (or Virtio)
|
||||
- Display Spice
|
||||
|
||||
**Add hardware - TPM**
|
||||
- Type: Emulated, Version: 2.0
|
||||
|
||||
Click **Begin Installation**. The host session will terminate and the Windows installer will appear on the monitor via GPU passthrough.
|
||||
|
||||
### 4. Install Windows 11
|
||||
|
||||
Follow the installer normally. The SATA disk appears without any driver loading step.
|
||||
|
||||
To skip the Microsoft account requirement: press `Shift+F10` at the network screen to open CMD, run `oobe\bypassnro`, and the system will reboot offering a local account option.
|
||||
|
||||
### 5. Install drivers
|
||||
|
||||
**VirtIO drivers**
|
||||
|
||||
On the host, serve the MSI over HTTP while the VM is running:
|
||||
|
||||
```bash
|
||||
VIRTIO_PATH=$(ls /nix/store | grep virtio-win | grep -v '\.drv')
|
||||
python3 -m http.server --directory /nix/store/$VIRTIO_PATH 8080
|
||||
```
|
||||
|
||||
Inside Windows, open a browser and go to `http://192.168.122.1:8080`. Download and run `virtio-win-gt-x64.msi`, install everything, then reboot. Press `Ctrl+C` on the host to stop the server.
|
||||
|
||||
**NVIDIA drivers**
|
||||
|
||||
Download the GeForce RTX 3060 Ti driver (Game Ready or Studio) from the NVIDIA website inside the VM. Install and reboot.
|
||||
|
||||
### 6. Set up Scream audio
|
||||
|
||||
**On Windows:** download the latest `Scream-x.x-setup.exe` from the [Scream releases page](https://github.com/duncanthrax/scream/releases), run the installer, accept the unsigned driver prompt, and reboot.
|
||||
|
||||
After reboot, go to **Settings > System > Sound** and set the output device to **Scream (WDM)**.
|
||||
|
||||
**On the host:** the Scream receiver runs as a user systemd service and starts automatically on login. Verify it is running:
|
||||
|
||||
```bash
|
||||
systemctl --user status scream.service
|
||||
```
|
||||
|
||||
Audio from the VM will play through the headphone jack on the host.
|
||||
|
||||
## Daily usage
|
||||
|
||||
```
|
||||
virsh start windows
|
||||
```
|
||||
|
||||
The hook terminates the host session and hands the GPU to the VM. The monitor shows Windows. Shut down Windows normally from the Start menu to return the GPU to the host and restart the login screen.
|
||||
|
||||
## IOMMU groups (desktop)
|
||||
|
||||
Verified after enabling VT-d:
|
||||
|
||||
| Group | Address | Device |
|
||||
|-------|---------|--------|
|
||||
| 1 | `0000:01:00.0` | NVIDIA GeForce RTX 3060 Ti |
|
||||
| 1 | `0000:01:00.1` | NVIDIA RTX 3060 Ti HDMI audio |
|
||||
| 8 | `0000:00:1f.0` | Intel Z370 LPC/eSPI Controller |
|
||||
| 8 | `0000:00:1f.2` | Intel Z370 Power Management Controller |
|
||||
| 8 | `0000:00:1f.3` | Intel 200 Series PCH HD Audio |
|
||||
| 8 | `0000:00:1f.4` | Intel Z370 SMBus Controller |
|
||||
| 9 | `0000:00:1f.6` | Intel Ethernet Connection I219-V |
|
||||
|
||||
The HDA audio controller shares group 8 with critical PCH devices and cannot be passed through. Audio is handled by Scream instead.
|
||||
Loading…
Add table
Add a link
Reference in a new issue