commit b5789d2df88846e9f86f5481032c2d8f46bc3556 parent d0b4c83e5351b3a5c954924fcd978a0acc533dd0 Author: maydayv7 <maydayv7@gmail.com> Date: Sat, 27 Jun 2026 22:52:19 +0530 feat: A bit of everything! - VFIO, Windows VM, Obsidian, Antigravity - Drop Niri UWSM and Bottles Diffstat:
61 files changed, 1371 insertions(+), 458 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -2,6 +2,11 @@ ### June +- Declaratively configure a Windows 11 VM +- Fix and document VFIO +- Use KVMFR with Looking Glass +- Migrate from Logseq to Obsidian +- Add Antigravity IDE & CLI - Use [`adw-catppuccin`](https://github.com/LuminarLeaf/adw-catppuccin) for Catppuccin GTK Theme - Use Discord instead of Vesktop - Script to update Minecraft mods diff --git a/README.md b/README.md @@ -44,13 +44,10 @@ It also builds and deploys my website to [maydayv7.cc](https://maydayv7.cc). - [Adwaita GTK3](https://github.com/lassekongo83/adw-gtk3): Theme from `libadwaita` ported to GTK3 - [KvLibadwaita](https://github.com/GabePoel/KvLibadwaita) Kvantum Theme: Integrates QT Apps with GNOME Desktop -- Firefox [GNOME Theme](https://github.com/rafaelmardojai/firefox-gnome-theme): GNOME Theme for the Mozilla Firefox Browser, used for better desktop integration +- Firefox [GNOME Theme](https://github.com/rafaelmardojai/firefox-gnome-theme): GNOME Theme for the Mozilla Firefox Browser - VS Code [Adwaita Theme](https://github.com/piousdeer/vscode-adwaita): Integrates Visual Studio Code with GNOME Desktop - Discord [GNOME Theme](https://github.com/ricewind012/discord-gnome-theme): A GNOME theme for Discord, following the Adwaita style & GNOME HIG - -- Firefox [Elementary Theme](https://github.com/Zonnev/elementaryos-firefox-theme): Elementary OS Theme for the Mozilla Firefox Browser, used for better desktop integration -- VS Code [Elementary Theme](https://github.com/sixpounder/vscode-elementary-theme): Integrates Visual Studio Code with Pantheon Desktop -- Logseq [Bonofix Theme](https://github.com/Sansui233/logseq-bonofix-theme): A clean Logseq theme with focus on long-time writing experience +- Obsidian [Adwaita Theme](https://github.com/birneee/obsidian-adwaita-theme): A GNOME Adwaita theme for Obsidian </details> @@ -108,7 +105,7 @@ It also builds and deploys my website to [maydayv7.cc](https://maydayv7.cc). - Support for Base16 color theming using [`stylix`](https://github.com/danth/stylix) - Support for declaratively installing [Flatpak](./modules/apps/flatpak.nix) applications using [`nix-flatpak`](https://github.com/gmodena/nix-flatpak) - Support for Android Virtualisation using [Waydroid](https://waydro.id/) -- Support for VFIO PCI Device Passthrough along with [Looking Glass](https://looking-glass.io/) for high-performance VMs +- Support for VFIO PCI Device Passthrough with a declarative, performant Windows VM using [NixVirt](https://github.com/AshleyYakeley/NixVirt) and [Looking Glass](https://looking-glass.io/) (see my [blog post](https://maydayv7.cc/blog/nixos/win11vm)) ## Programs @@ -382,11 +379,4 @@ A `git` [hook](./files/git/hooks) is used to check the commit message to adhere --- -<details> -<summary><b>Known Limitations</b></summary> - -- [Logseq](./modules/apps/notes.nix) Plugins have to be manually installed - -</details> - If you like this project, consider leaving a [star](https://github.com/maydayv7/dotfiles) diff --git a/files/_module.nix b/files/_module.nix @@ -9,12 +9,13 @@ in { flake.files = rec { # File Paths - path = { + path = rec { toplevel = ./.; system = "/etc/nixos"; persist = "/nix/state"; data = "/data"; + sync = "${data}/sync"; gpg = "/etc/gpg"; sops = ../secrets/secrets.yaml; @@ -37,7 +38,7 @@ in { # Directory Bookmarks bookmarks = '' file://${path.data}/files Files - file://${path.data}/sync Sync + file://${path.sync} Sync file:/// Computer ''; @@ -90,12 +91,6 @@ in { # Password Manager keepassxc = readFile ./keepassxc.ini; - # Logseq Notes - logseq = { - settings = ./logseq/settings; - prefs = readFile ./logseq/preferences.json; - }; - # Nano Text Editor nano = readFile ./nanorc; diff --git a/files/gnome/menu.json b/files/gnome/menu.json @@ -68,9 +68,9 @@ "type": "switch", "title": "Windows", "icon": "im-msn-symbolic", - "start": "virsh -c qemu:///system start Windows", - "stop": "virsh -c qemu:///system shutdown Windows", - "check": "sh -c 'virsh -c qemu:///system list | grep Windows'", + "start": "virsh -c qemu:///system start windows", + "stop": "virsh -c qemu:///system shutdown windows", + "check": "sh -c 'virsh -c qemu:///system list | grep windows'", "interval_s": 10 } ] diff --git a/files/logseq/preferences.json b/files/logseq/preferences.json @@ -1,22 +0,0 @@ -{ - "theme": null, - "themes": { - "mode": "dark", - "light": null, - "dark": { - "name": "Default Dark Theme", - "url": null, - "description": "Logseq default dark theme.", - "mode": "dark", - "selected": true, - "group-first": true, - "group-desc": "dark themes" - } - }, - "externals": [], - "pinnedToolbarItems": [ - "logseq-git:git", - "logseq-tocgen:open-toc", - "logseq-journals-calendar:open-calendar" - ] -} diff --git a/files/logseq/settings/logseq-awesome-ui.json b/files/logseq/settings/logseq-awesome-ui.json @@ -1,23 +0,0 @@ -{ - "disabled": false, - "alwaysShowNewBlockBullet": true, - "compactSidebarMenu": true, - "contentHeading": null, - "headerHeading": null, - "headerVariant": "Wide", - "hideAddNewPage": false, - "hideHomeButton": true, - "hideRightSidebarToolbar": false, - "hideSidebarPageIcons": false, - "killAnimations": false, - "leftSidebarHeading": null, - "menuCalendar": true, - "navigationPosition": "Left", - "otherHeading": null, - "rightSidebarHeading": null, - "searchStyle": "Wide", - "tabsHeading": null, - "tabsPosition": "TopPanel", - "tabsStyle": "Awesome", - "vaultButtonToBottom": true -} diff --git a/files/logseq/settings/logseq-bullet-threading.json b/files/logseq/settings/logseq-bullet-threading.json @@ -1,6 +0,0 @@ -{ - "disabled": false, - "color": "", - "customColor": false, - "width": "2px" -} diff --git a/files/logseq/settings/logseq-git.json b/files/logseq/settings/logseq-git.json @@ -1,9 +0,0 @@ -{ - "disabled": false, - "autoCheckSynced": true, - "autoPush": false, - "checkWhenDBChanged": true, - "customCommitMessage": "feat: Notes", - "typeCommitMessage": "Custom Message With Date", - "buttons": ["Check Status", "Show Log", "Pull Rebase", "Commit & Push"] -} diff --git a/files/logseq/settings/logseq-journals-calendar.json b/files/logseq/settings/logseq-journals-calendar.json @@ -1,10 +0,0 @@ -{ - "disabled": false, - "backgroundColorOfContainerDark": "#@bg", - "backgroundColorOfContainerLight": "#@bg", - "firstDayOfWeek": 1, - "hotkey": null, - "keepOpenOnSelect": false, - "showTodayBtn": false, - "showWeekNumbers": true -} diff --git a/files/logseq/settings/logseq-move-block.json b/files/logseq/settings/logseq-move-block.json @@ -1,7 +0,0 @@ -{ - "disabled": false, - "settingsVersion": "v1", - "keyBindings": { - "moveBlock": "mod+shift+m" - } -} diff --git a/files/logseq/settings/logseq-pdf-export-plugin.json b/files/logseq/settings/logseq-pdf-export-plugin.json @@ -1,14 +0,0 @@ -{ - "disabled": false, - "blockExportHandling": "Keep title as heading of block", - "retainedOptions": "", - "template1CSS": "", - "template1Choice": "Bullets for non top level elements", - "template1Options": ["Inherit logseq CSS"], - "template2CSS": "", - "template2Choice": "Bullets througout the document", - "template2Options": ["Inherit logseq CSS"], - "template3CSS": "", - "template3Choice": "", - "template3Options": ["Inherit logseq CSS"] -} diff --git a/files/logseq/settings/logseq-tabs.json b/files/logseq/settings/logseq-tabs.json @@ -1,11 +0,0 @@ -{ - "disabled": false, - "tabs:close-button-left": false, - "tabs:close": "mod+shift+w", - "tabs:hide-close-all-button": false, - "tabs:inherit-custom-css": true, - "tabs:select-next": "ctrl+tab", - "tabs:select-prev": "ctrl+shift+tab", - "tabs:show-single-tab": true, - "tabs:toggle-pin": "" -} diff --git a/files/logseq/settings/logseq-tocgen.json b/files/logseq/settings/logseq-tocgen.json @@ -1,13 +0,0 @@ -{ - "disabled": false, - "defaultExpansionLevel": 1, - "defaultHeadingType": "h", - "defaultLevels": 6, - "hideBackTop": false, - "hideGoDown": false, - "maxScrollTryCount": 20, - "noPageJump": false, - "openShortcut": "", - "pageScrollBehavior": "top", - "showTags": false -} diff --git a/flake.lock b/flake.lock @@ -480,11 +480,11 @@ ] }, "locked": { - "lastModified": 1781184346, - "narHash": "sha256-cZRlW47U6A2nWvAmnZeeO6Xvq23gxYbVLel4KxqOrcQ=", + "lastModified": 1781981105, + "narHash": "sha256-/1nNBbA7PrSQpTc9Qazkhl4kIPg+TNl0CjxS3UQJKlw=", "owner": "nix-community", "repo": "home-manager", - "rev": "ea6d221d7aa85652d014b6f719dddf036037515b", + "rev": "7bfff44b465909f69a442701293bc0badcf476dc", "type": "github" }, "original": { @@ -1666,6 +1666,27 @@ "type": "github" } }, + "nixvirt": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748140003, + "narHash": "sha256-DNBZmuk1YRM2PmwbHzVdXumRjCUzQkMarg4iI/37rOQ=", + "owner": "AshleyYakeley", + "repo": "NixVirt", + "rev": "5dfe108fd859b122f9a96981cb6bc12297653d6c", + "type": "github" + }, + "original": { + "owner": "AshleyYakeley", + "ref": "v0.6.0", + "repo": "NixVirt", + "type": "github" + } + }, "noctalia": { "inputs": { "nixpkgs": [ @@ -1821,6 +1842,7 @@ "nixpkgs": [ "stable" ], + "nixvirt": "nixvirt", "noctalia": "noctalia", "noctalia-greeter": "noctalia-greeter", "proprietary": "proprietary", @@ -1901,11 +1923,11 @@ }, "stable": { "locked": { - "lastModified": 1781216227, - "narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=", + "lastModified": 1782375420, + "narHash": "sha256-wiPYmEuHbJvleW489n6+lamL7JSJg3pcKUYwURU9CkI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0", + "rev": "4062d36ebeae843c750011eef6b61ec9a9dbc9a9", "type": "github" }, "original": { @@ -2074,11 +2096,11 @@ }, "unstable": { "locked": { - "lastModified": 1781577229, - "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", + "lastModified": 1782467914, + "narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", + "rev": "e73de5be04e0eff4190a1432b946d469c794e7b4", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix @@ -134,6 +134,12 @@ # Declarative Flatpak Wrapper flatpak.url = "github:gmodena/nix-flatpak?ref=latest"; + # Declarative VM Management + nixvirt = { + url = "github:AshleyYakeley/NixVirt?ref=v0.6.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Discord Module nixcord = { url = "github:FlameFlag/nixcord"; diff --git a/modules/README.md b/modules/README.md @@ -24,12 +24,13 @@ An overview of every configuration module in this repository. | `discord` | Home Manager | Discord chat client | | `firefox` | Home Manager | Firefox browser | | `internet` | Home Manager | Internet apps | -| `notes` | Home Manager | Logseq note-taking | +| `notes` | Home Manager | Obsidian note-taking | | `spotify` | Home Manager | Spotify music client | | `stream` | Home Manager | Streaming tools | | `syncthing` | Home Manager | Syncthing file sync | | `youtube` | Home Manager | YouTube Music & TUI | | `vscode` | Home Manager | Visual Studio Code editor | +| `antigravity` | Home Manager | Google Antigravity | | `flatpak` | Both | Flatpak app sandboxing | | `latex` | Both | LaTeX typesetting | | `office` | Both | Office suite environment | @@ -101,9 +102,9 @@ An overview of every configuration module in this repository. | Module | Type | Description | | --------- | ----- | --------------------------- | -| `libvirt` | Both | Libvirt/QEMU virtualisation | | `android` | NixOS | Android virtualisation | | `docker` | NixOS | Docker containers | +| `libvirt` | Both | Libvirt/QEMU virtualisation | | `vfio` | NixOS | VFIO GPU passthrough | ### [`secrets`](../secrets) @@ -166,6 +167,8 @@ The following are the custom configuration options exposed by the modules above: - `vfio` - - `setup`: Enable VFIO Setup Mode - `true / false` - `passthrough`: PCI Device IDs for VFIO - Ex. `[ "10de:28e0" "10de:22be" ]` + - `isolate`: Host CPU set to isolate for pinned vCPUs - Ex. `"2-4,10-12"` + - `hugepages`: Number of 1 GiB hugepages to reserve - Ex. `16` - [`user`](./system/user) - - `credentials`: Individual User Credentials - diff --git a/modules/apps/antigravity.nix b/modules/apps/antigravity.nix @@ -0,0 +1,58 @@ +## Google Antigravity ## +{config, ...}: let + inherit (config.flake) files; +in { + flake.modules.homeManager.antigravity = { + config, + lib, + pkgs, + osConfig ? {}, + ... + }: let + font = config.stylix.fonts.monospace.name; + isGnome = osConfig.services.desktopManager.gnome.enable or false; + isWM = (osConfig.programs.hyprland.enable or false) || (osConfig.programs.niri.enable or false); + in { + imports = [ + (import ./vscode/_mutable.nix { + program = "antigravity"; + configDir = "Antigravity"; + }) + ]; + + # IDE + programs.antigravity = { + enable = true; + package = pkgs.antigravity; + profiles.default = import ./vscode/_profile.nix { + inherit lib pkgs files font isGnome isWM; + }; + }; + + # CLI + programs.antigravity-cli = { + enable = true; + package = pkgs.unstable.antigravity-cli; + enableMcpIntegration = true; + settings.altScreenMode = "always"; + permissions = { + deny = ["command(rm -rf)"]; + ask = ["command(*)"]; + }; + + # Context7 Extension + mcpServers.context7.serverUrl = "https://mcp.context7.com/mcp"; + }; + + home = { + persist.directories = [ + ".config/Antigravity" + ".antigravity" + ".gemini" + ]; + + # Superpowers Extension + file.".gemini/extensions/superpowers".source = "${pkgs.custom.superpowers}/share/superpowers"; + }; + }; +} diff --git a/modules/apps/git/git.nix b/modules/apps/git/git.nix @@ -136,7 +136,7 @@ in { ci = "commit -v"; cam = "${ci} -am"; cia = "${ci} -a"; - cmx = "log -1 --pretty=%s"; + cmx = "log -1 --format=%B"; co = "checkout"; cp = "cherry-pick"; cpp = "!CURRENT=$(git ${bc}) && git ${st} && git ${co} -B $2 $3 && git ${cp} $1 && git ${pf} $4 && git ${co} $CURRENT && git ${stp} && :"; diff --git a/modules/apps/notes.nix b/modules/apps/notes.nix @@ -1,55 +1,94 @@ -## Logseq Configuration ## +## Obsidian Configuration ## {config, ...}: let - inherit (config) util; inherit (config.flake) files; in { flake.modules.homeManager.notes = { - config, lib, pkgs, osConfig ? {}, ... }: let - mutable = { - mutable = true; - force = true; - }; + vault = "${files.path.sync}/Notes"; + target = "Documents/Notes"; isGnome = osConfig.services.desktopManager.gnome.enable or false; - isHyprland = osConfig.programs.hyprland.enable or false; - style = + isWM = (osConfig.programs.hyprland.enable or false) || (osConfig.programs.niri.enable or false); + theme = if isGnome - then "url('https://cdn.jsdelivr.net/gh/sansui233/logseq-bonofix-theme/custom.css')" - else if isHyprland - then "url('https://logseq.catppuccin.com/ctp-${config.catppuccin.flavor}.css')" - else ""; + then pkgs.custom.obsidian-adwaita + else if isWM + then pkgs.custom.obsidian-catppuccin + else null; in { - config.home = { - packages = [pkgs.logseq]; - persist.directories = [ - ".logseq" - ".config/Logseq" - ]; + config = { + home = { + persist.directories = [".config/obsidian"]; + activation.obsidianVault = lib.hm.dag.entryBetween ["linkGeneration"] ["writeBoundary"] '' + run mkdir -p ${vault} "$HOME/$(dirname ${target})" + run ln -sfn ${vault} "$HOME/${target}" + + stignore="${files.path.sync}/.stignore" + if ! { [ -e "$stignore" ] && ${pkgs.gnugrep}/bin/grep -qxF "/Notes/.obsidian" "$stignore"; } + then + echo "/Notes/.obsidian" >> "$stignore" + fi + ''; + }; + + programs.obsidian = { + enable = true; + vaults.${target} = {}; + defaultSettings = { + appearance.theme = "obsidian"; + corePlugins = [ + "file-explorer" + "global-search" + "switcher" + "graph" + "backlink" + "outgoing-link" + "tag-pane" + "page-preview" + "daily-notes" + "templates" + "note-composer" + "command-palette" + "outline" + "word-count" + "file-recovery" + "bookmarks" + "properties" + "canvas" + ]; - file = with files.logseq; - { - ".config/logseq/configs.edn".text = "{:window/native-titlebar? true}"; - ".logseq/preferences.json" = {text = prefs;} // mutable; - ".logseq/config/config.edn".text = - if style != "" - then ''{:custom-css-url "@import ${style};"}'' - else lib.mkDefault ""; - } - // util.map.folder { - directory = settings; - path = ".logseq/settings"; - extension = ".json"; - apply = text: {inherit text;} // mutable; - replace = { - placeholders = ["@bg"]; - values = [config.lib.stylix.colors.base00]; - }; + communityPlugins = [ + { + pkg = pkgs.custom.obsidian-git; + settings = { + commitMessage = "feat: Notes {{date}}"; + autoCommitMessage = "feat: Notes {{date}}"; + commitDateFormat = "YYYY-MM-DD HH:mm:ss"; + autoSaveInterval = 0; + autoPushInterval = 0; + autoPullInterval = 0; + disablePush = false; + pullBeforePush = true; + showStatusBar = true; + }; + } + pkgs.custom.obsidian-calendar + pkgs.custom.obsidian-outliner + { + pkg = pkgs.custom.obsidian-style-settings; + settings = lib.mkIf isWM { + "catppuccin-theme-dark" = "ctp-macchiato"; + "catppuccin-theme-accents" = "ctp-accent-blue"; + }; + } + ]; + themes = [{pkg = theme;}]; }; + }; }; }; } diff --git a/modules/apps/spotify.nix b/modules/apps/spotify.nix @@ -6,7 +6,7 @@ osConfig ? {}, ... }: let - isHyprland = osConfig.programs.hyprland.enable or false; + isWM = (osConfig.programs.hyprland.enable or false) || (osConfig.programs.niri.enable or false); in { imports = [inputs.spicetify.homeManagerModules.default]; home.persist.directories = [ @@ -36,7 +36,7 @@ volumePercentage ]; } - // lib.optionalAttrs isHyprland { + // lib.optionalAttrs isWM { theme = pkgs.spicetify.themes.catppuccin; colorScheme = "macchiato"; }; diff --git a/modules/apps/syncthing.nix b/modules/apps/syncthing.nix @@ -1,5 +1,6 @@ ## Syncthing Configuration ## # ? # Configure via Web UI: http://localhost:8384 +# ? # Shared directory is at 'files.path.sync' { flake.modules.homeManager.syncthing = { services.syncthing.enable = true; diff --git a/modules/apps/vscode/_mutable.nix b/modules/apps/vscode/_mutable.nix @@ -1,18 +1,12 @@ # https://gist.github.com/piousdeer/b29c272eaeba398b864da6abf6cb5daa { + program, + configDir, +}: { config, lib, ... }: let - configDir = - { - "vscode" = "Code"; - "vscode-insiders" = "Code - Insiders"; - "vscodium" = "VSCodium"; - } - .${ - config.programs.vscode.package.pname - }; userDir = "${config.xdg.configHome}/${configDir}/User"; configFilePath = "${userDir}/settings.json"; @@ -20,7 +14,7 @@ keybindingsFilePath = "${userDir}/keybindings.json"; snippetDir = "${userDir}/snippets"; - cfg = config.programs.vscode.profiles.default; + cfg = config.programs.${program}.profiles.default; pathsToMakeWritable = lib.flatten [ (lib.optional (cfg.userTasks != {}) tasksFilePath) (lib.optional (cfg.userSettings != {}) configFilePath) diff --git a/modules/apps/vscode/_profile.nix b/modules/apps/vscode/_profile.nix @@ -0,0 +1,65 @@ +## Shared Editor Profile +{ + lib, + pkgs, + files, + font, + isGnome, + isWM, +}: +with files.vscode; { + # Keyboard Shortcuts + inherit keybindings; + + # Settings + userSettings = lib.mkMerge [ + (settings + // { + "workbench.colorTheme" = lib.mkDefault "Dark 2026"; + "editor.fontFamily" = "'${font}', 'monospace', monospace"; + }) + (lib.mkIf isGnome { + "workbench.productIconTheme" = "adwaita"; + "terminal.external.linuxExec" = "ghostty"; + }) + (lib.mkIf isWM { + "terminal.external.linuxExec" = "kitty"; + }) + ]; + + ## Editor Extensions + extensions = with pkgs.vscode-extensions; + [ + aaron-bond.better-comments # Annotations + editorconfig.editorconfig # .editorconfig + esbenp.prettier-vscode # Formatter + file-icons.file-icons # File Icons + naumovs.color-highlight # Color Viewer + johnpapa.vscode-peacock # Workspace Color + + github.vscode-pull-request-github # GitHub + github.copilot # Copilot AI + dart-code.flutter # Flutter + jnoortheen.nix-ide # Nix + ms-python.python # Python + ms-vscode.cpptools # C/C++ + redhat.java # Java + rust-lang.rust-analyzer # Rust + tomoki1207.pdf # PDF Viewer + yzhang.markdown-all-in-one # Markdown + + # HTML/CSS/XML + ecmel.vscode-html-css + formulahendry.auto-rename-tag + redhat.vscode-xml + + # JS + dbaeumer.vscode-eslint + ritwickdey.liveserver + ] + ++ (with pkgs.vscode-marketplace; [ + langningchen.cph-ng # CP + kisstkondoros.vscode-gutter-preview # Image Preview + ]) + ++ lib.optionals isGnome [pkgs.vscode-extensions.piousdeer.adwaita-theme]; +} diff --git a/modules/apps/vscode/default.nix b/modules/apps/vscode/default.nix @@ -1,4 +1,4 @@ -## Visual Studio Code Editor Configuration ## +## Visual Studio Code Editor ## {config, ...}: let inherit (config) util; inherit (config.flake) files; @@ -12,10 +12,14 @@ in { }: let font = config.stylix.fonts.monospace.name; isGnome = osConfig.services.desktopManager.gnome.enable or false; - isHyprland = osConfig.programs.hyprland.enable or false; + isWM = (osConfig.programs.hyprland.enable or false) || (osConfig.programs.niri.enable or false); in { - # Environment - imports = [./_mutable.nix]; + imports = [ + (import ./_mutable.nix { + program = "vscode"; + configDir = "Code"; + }) + ]; xdg.mimeApps = let mime = util.build.mime { code = ["code.desktop"]; @@ -27,6 +31,14 @@ in { associations.added = mime; }; + programs.vscode = { + enable = true; + package = pkgs.vscode; + profiles.default = import ./_profile.nix { + inherit lib pkgs files font isGnome isWM; + }; + }; + home = { persist.directories = [ ".config/Code" @@ -38,66 +50,5 @@ in { alejandra ]; }; - - programs.vscode = { - enable = true; - package = pkgs.vscode; - profiles.default = with files.vscode; { - # Keyboard Shortcuts - inherit keybindings; - - # Settings - userSettings = lib.mkMerge [ - (settings - // { - "workbench.colorTheme" = lib.mkDefault "Dark 2026"; - "editor.fontFamily" = "'${font}', 'monospace', monospace"; - }) - (lib.mkIf isGnome { - "workbench.productIconTheme" = "adwaita"; - "terminal.external.linuxExec" = "ghostty"; - }) - (lib.mkIf isHyprland { - "terminal.external.linuxExec" = "kitty"; - }) - ]; - - ## Editor Extensions - extensions = with pkgs.vscode-extensions; - [ - aaron-bond.better-comments # Annotations - editorconfig.editorconfig # .editorconfig - esbenp.prettier-vscode # Formatter - file-icons.file-icons # File Icons - naumovs.color-highlight # Color Viewer - johnpapa.vscode-peacock # Workspace Color - - github.vscode-pull-request-github # GitHub - github.copilot # Copilot AI - dart-code.flutter # Flutter - jnoortheen.nix-ide # Nix - ms-python.python # Python - ms-vscode.cpptools # C/C++ - redhat.java # Java - rust-lang.rust-analyzer # Rust - tomoki1207.pdf # PDF Viewer - yzhang.markdown-all-in-one # Markdown - - # HTML/CSS/XML - ecmel.vscode-html-css - formulahendry.auto-rename-tag - redhat.vscode-xml - - # JS - dbaeumer.vscode-eslint - ritwickdey.liveserver - ] - ++ (with pkgs.vscode-marketplace; [ - langningchen.cph-ng # CP - kisstkondoros.vscode-gutter-preview # Image Preview - ]) - ++ lib.optionals isGnome [pkgs.vscode-extensions.piousdeer.adwaita-theme]; - }; - }; }; } diff --git a/modules/apps/youtube.nix b/modules/apps/youtube.nix @@ -9,9 +9,9 @@ in { osConfig ? {}, ... }: let - isHyprland = osConfig.programs.hyprland.enable or false; + isWM = (osConfig.programs.hyprland.enable or false) || (osConfig.programs.niri.enable or false); style = - if isHyprland + if isWM then "@import url('https://youtubemusic.catppuccin.com/src/${config.catppuccin.flavor}.css');" else ""; in { diff --git a/modules/desktop/_wm/terminal.nix b/modules/desktop/_wm/terminal.nix @@ -22,6 +22,7 @@ _: { enable_audio_bell = "no"; visual_bell_duration = "0.1"; + background_blur = 1; cursor_shape = "beam"; cursor_shape_unfocused = "underline"; tab_bar_style = "powerline"; diff --git a/modules/desktop/gnome/_settings/apps.nix b/modules/desktop/gnome/_settings/apps.nix @@ -198,7 +198,7 @@ lib.mkIf (osConfig != null) ( "org/gnome/desktop/app-folders/folders/7464f88f-e282-4cd7-b4ec-956276e9f709" = { name = "Writing"; apps = [ - "Logseq.desktop" + "obsidian.desktop" "org.cvfosammmm.Setzer.desktop" "com.github.xournalpp.xournalpp.desktop" "org.gnome.gitlab.somas.Apostrophe.desktop" @@ -239,7 +239,6 @@ lib.mkIf (osConfig != null) ( "org/gnome/desktop/app-folders/folders/1c3e59e4-a571-4ada-af1d-ed1ced384cfb" = { name = "Wine"; apps = [ - "com.usebottles.bottles.desktop" "net.lutris.Lutris.desktop" "winetricks.desktop" "protontricks.desktop" diff --git a/modules/desktop/niri/_features/main.nix b/modules/desktop/niri/_features/main.nix @@ -6,19 +6,6 @@ _: { enable = true; package = pkgs.niri; }; - - # Session - programs.uwsm = { - enable = true; - waylandCompositors.niri = { - prettyName = "Niri"; - comment = "Niri compositor managed by UWSM"; - binPath = "/run/current-system/sw/bin/niri"; - }; - }; - - # App Environment - xdg.portal.extraPortals = [pkgs.xdg-desktop-portal-gtk]; }; home = {pkgs, ...}: { diff --git a/modules/desktop/niri/_settings/apps.nix b/modules/desktop/niri/_settings/apps.nix @@ -7,27 +7,8 @@ _: { programs.niri.settings = { ## Autostart spawn-at-startup = let - exec = app: args: - [ - "uwsm" - "app" - "-t" - "service" - "-u" - "${app}.service" - "--" - app - ] - ++ args; + exec = app: args: [app] ++ args; in [ - { - command = [ - "uwsm" - "finalize" - "NIRI_SOCKET" - "DISPLAY" - ]; - } {command = exec "pcmanfm-qt" ["--desktop"];} {command = [(lib.getExe pkgs.xwayland-satellite) ":0"];} ]; @@ -35,7 +16,7 @@ _: { ## Keybindings binds = with config.lib.niri.actions; let sh = spawn "sh" "-c"; - runOnce = app: args: sh "pgrep ${app} || uwsm app -u ${app}.scope -- ${app} ${args}"; + runOnce = app: args: sh "pgrep ${app} || ${app} ${args}"; in { # Applications "Super+F" = { diff --git a/modules/games/games.nix b/modules/games/games.nix @@ -1,17 +1,7 @@ ## Games Configuration ## _: { flake.modules = { - nixos.games = { - lib, - pkgs, - ... - }: { - # Packages - environment.systemPackages = with pkgs; [ - bottles - lutris - ]; - + nixos.games = {lib, ...}: { # Steam programs.steam = { enable = true; @@ -29,21 +19,19 @@ _: { }; homeManager.games = { - config, lib, + osConfig ? {}, ... }: { - # Runner - xdg.dataFile."lutris/runners/wine/wine-system" = - lib.mkIf (config.apps.wine or null != null) - { - source = config.apps.wine; - }; + # Lutris + programs.lutris = { + enable = true; + steamPackage = osConfig.programs.steam.package; + winePackages = lib.optional ((osConfig.apps.wine or null) != null) osConfig.apps.wine; + }; - # Directories home.persist.directories = [ "Games" - ".local/share/bottles" # Lutris ".cache/lutris" diff --git a/modules/gui/theme.nix b/modules/gui/theme.nix @@ -100,9 +100,10 @@ in { enable = lib.mkDefault true; targets = lib.mkMerge [ { - firefox.enable = lib.mkDefault false; gnome.enable = lib.mkDefault false; + firefox.enable = lib.mkDefault false; vscode.enable = lib.mkDefault false; + obsidian.enable = lib.mkDefault false; } (lib.genAttrs (lib.unique config.gui._unmanaged) (_: {enable = false;})) ]; diff --git a/modules/hardware/README.md b/modules/hardware/README.md @@ -21,20 +21,3 @@ The system may be set up using either a `simple` or `advanced` filesystem scheme All important persisted files are stored at `/data` (declared using either `system.fs.persist`, or `home.persist` for user files), while persisted system files are stored at `/nix/state` (declared using `environment.persist`). Personal files and media are stored at `/data/files` - -### Virtualisation - -Configuration has been provided to run VMs using `qemu-kvm` and `libvirt`, along with support for VFIO PCI Passthrough. This can be used to create high-performance VMs, especially useful for virtualising Windows - -#### PCI Passthrough - -Use [`scripts/pci.sh`](../../scripts/pci.sh) in order to determine the PCI Device IDs which must be added to `virt.vfio.passthrough` - -#### Windows Virtualisation - -In order to create a highly performant Windows VM using the `virt-manager` GUI and [Virtio](https://wiki.libvirt.org/Virtio.html) drivers, follow the instructions given on [this](https://sysguides.com/install-a-windows-11-virtual-machine-on-kvm) page. To configure GPU passthrough, add the relevant GPU and audio IDs to `virt.vfio.passthrough` (See the [Arch Wiki](https://wiki.archlinux.org/title/PCI_passthrough_via_OVMF) for additional information). Confirm that the GPU is recognized within the Windows VM, and download the relevant drivers. In order to utilize full graphical bandwidth, one of the following must be done: - -1. Connect an external monitor to the GPU -2. Use a dummy monitor plug or fake a display using [`Virtual Display Driver`](https://github.com/itsmikethetech/Virtual-Display-Driver), then use [Looking Glass](https://looking-glass.io/) to extract video output onto the main monitor. The configuration for Looking Glass on the Linux (client) side is already implemented, and only the Windows (host) needs to be configured by following instructions on [this](https://looking-glass.io/docs/stable/install/) page (Follow the section titled 'IVSHMEM with shared memory') - -The `vfio` and `no-vfio` Specialisations can be selected in the boot menu to toggle GPU passthrough as required diff --git a/modules/hardware/gpu.nix b/modules/hardware/gpu.nix @@ -38,7 +38,7 @@ _: { ]; }) - (mkIf (cfg.model == "nvidia") ( + (mkIf (cfg.enable && cfg.model == "nvidia") ( let hybrid = with config.hardware.nvidia.prime; (amdgpuBusId != "" || intelBusId != ""); in { diff --git a/modules/hosts/valkyrie/_vm/default.nix b/modules/hosts/valkyrie/_vm/default.nix @@ -0,0 +1,17 @@ +## Windows VM +# ? # Run 'virsh -c qemu:///system start windows' +{inputs}: { + nixos = {pkgs, ...}: { + imports = [inputs.nixvirt.nixosModules.default]; + virtualisation.libvirt = { + enable = true; + package = pkgs.libvirt; + connections."qemu:///system".domains = [ + { + definition = ./windows.xml; + active = null; + } + ]; + }; + }; +} diff --git a/modules/hosts/valkyrie/_vm/windows.xml b/modules/hosts/valkyrie/_vm/windows.xml @@ -0,0 +1,248 @@ +<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'> + <name>windows</name> + <uuid>58e5ada6-f247-4a07-91eb-8b361f99cca3</uuid> + <metadata> + <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0"> + <libosinfo:os id="http://microsoft.com/win/11"/> + </libosinfo:libosinfo> + </metadata> + <memory unit='KiB'>16777216</memory> + <currentMemory unit='KiB'>16777216</currentMemory> + <memoryBacking> + <hugepages> + <page size='1048576' unit='KiB'/> + </hugepages> + </memoryBacking> + <vcpu placement='static'>6</vcpu> + <iothreads>1</iothreads> + <cputune> + <vcpupin vcpu='0' cpuset='2'/> + <vcpupin vcpu='1' cpuset='10'/> + <vcpupin vcpu='2' cpuset='3'/> + <vcpupin vcpu='3' cpuset='11'/> + <vcpupin vcpu='4' cpuset='4'/> + <vcpupin vcpu='5' cpuset='12'/> + <emulatorpin cpuset='0-1,8-9'/> + <iothreadpin iothread='1' cpuset='0-1,8-9'/> + </cputune> + <os firmware='efi'> + <type arch='x86_64' machine='pc-q35-10.2'>hvm</type> + <firmware> + <feature enabled='no' name='enrolled-keys'/> + <feature enabled='yes' name='secure-boot'/> + </firmware> + <loader readonly='yes' secure='yes' type='pflash' format='raw'>/run/libvirt/nix-ovmf/edk2-x86_64-secure-code.fd</loader> + <nvram template='/run/libvirt/nix-ovmf/edk2-i386-vars.fd' templateFormat='raw' format='raw'>/var/lib/libvirt/qemu/nvram/windows_VARS.fd</nvram> + </os> + <features> + <acpi/> + <apic/> + <hyperv mode='custom'> + <relaxed state='on'/> + <vapic state='on'/> + <spinlocks state='on' retries='8191'/> + <vpindex state='on'/> + <runtime state='on'/> + <synic state='on'/> + <stimer state='on'/> + <frequencies state='on'/> + <tlbflush state='on'/> + <ipi state='on'/> + <avic state='on'/> + </hyperv> + <vmport state='off'/> + <smm state='on'/> + </features> + <cpu mode='host-passthrough' check='none' migratable='on'> + <topology sockets='1' dies='1' clusters='1' cores='3' threads='2'/> + <cache mode='passthrough'/> + <feature policy='require' name='topoext'/> + </cpu> + <clock offset='localtime'> + <timer name='rtc' tickpolicy='catchup'/> + <timer name='pit' tickpolicy='delay'/> + <timer name='hpet' present='no'/> + <timer name='hypervclock' present='yes'/> + </clock> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <pm> + <suspend-to-mem enabled='no'/> + <suspend-to-disk enabled='no'/> + </pm> + <devices> + <emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator> + <disk type='block' device='disk'> + <driver name='qemu' type='raw' cache='none' io='native' discard='unmap' iothread='1'/> + <source dev='/dev/zvol/fspool/vm/windows'/> + <target dev='vda' bus='virtio'/> + <address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/> + </disk> + <!-- Disk images + <disk type='file' device='cdrom'> + <driver name='qemu' type='raw'/> + <source file='/data/vm/iso/Win11.iso'/> + <target dev='sda' bus='sata'/> + <readonly/> + <boot order='1'/> + <address type='drive' controller='0' bus='0' target='0' unit='0'/> + </disk> + <disk type='file' device='cdrom'> + <driver name='qemu' type='raw'/> + <source file='/data/vm/iso/virtio-win.iso'/> + <target dev='sdb' bus='sata'/> + <readonly/> + <boot order='2'/> + <address type='drive' controller='0' bus='0' target='0' unit='1'/> + </disk> + --> + <controller type='usb' index='0' model='qemu-xhci' ports='15'> + <address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/> + </controller> + <controller type='pci' index='0' model='pcie-root'/> + <controller type='pci' index='1' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='1' port='0x10'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0' multifunction='on'/> + </controller> + <controller type='pci' index='2' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='2' port='0x11'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x1'/> + </controller> + <controller type='pci' index='3' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='3' port='0x12'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x2'/> + </controller> + <controller type='pci' index='4' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='4' port='0x13'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x3'/> + </controller> + <controller type='pci' index='5' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='5' port='0x14'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x4'/> + </controller> + <controller type='pci' index='6' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='6' port='0x15'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x5'/> + </controller> + <controller type='pci' index='7' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='7' port='0x16'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x6'/> + </controller> + <controller type='pci' index='8' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='8' port='0x17'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x7'/> + </controller> + <controller type='pci' index='9' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='9' port='0x18'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0' multifunction='on'/> + </controller> + <controller type='pci' index='10' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='10' port='0x19'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x1'/> + </controller> + <controller type='pci' index='11' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='11' port='0x1a'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x2'/> + </controller> + <controller type='pci' index='12' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='12' port='0x1b'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x3'/> + </controller> + <controller type='pci' index='13' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='13' port='0x1c'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x4'/> + </controller> + <controller type='pci' index='14' model='pcie-root-port'> + <model name='pcie-root-port'/> + <target chassis='14' port='0x1d'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x5'/> + </controller> + <controller type='sata' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/> + </controller> + <controller type='virtio-serial' index='0'> + <address type='pci' domain='0x0000' bus='0x03' slot='0x00' function='0x0'/> + </controller> + <interface type='network'> + <mac address='52:54:00:89:97:df'/> + <source network='default'/> + <model type='virtio'/> + <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> + </interface> + <serial type='pty'> + <target type='isa-serial' port='0'> + <model name='isa-serial'/> + </target> + </serial> + <console type='pty'> + <target type='serial' port='0'/> + </console> + <channel type='spicevmc'> + <target type='virtio' name='com.redhat.spice.0'/> + <address type='virtio-serial' controller='0' bus='0' port='1'/> + </channel> + <channel type='unix'> + <target type='virtio' name='org.qemu.guest_agent.0'/> + <address type='virtio-serial' controller='0' bus='0' port='2'/> + </channel> + <input type='mouse' bus='ps2'/> + <input type='keyboard' bus='ps2'/> + <tpm model='tpm-crb'> + <backend type='emulator' version='2.0'> + <profile name='default-v1'/> + </backend> + </tpm> + <graphics type='spice' autoport='yes'> + <listen type='address'/> + <image compression='off'/> + </graphics> + <sound model='ich9'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x1b' function='0x0'/> + </sound> + <audio id='1' type='spice'/> + <video> + <model type='qxl' ram='65536' vram='65536' vgamem='16384' heads='1' primary='yes'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x0'/> + </video> + <hostdev mode='subsystem' type='pci' managed='yes'> + <source> + <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> + </source> + <address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x0'/> + </hostdev> + <hostdev mode='subsystem' type='pci' managed='yes'> + <source> + <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/> + </source> + <address type='pci' domain='0x0000' bus='0x07' slot='0x00' function='0x0'/> + </hostdev> + <redirdev bus='usb' type='spicevmc'> + <address type='usb' bus='0' port='2'/> + </redirdev> + <redirdev bus='usb' type='spicevmc'> + <address type='usb' bus='0' port='3'/> + </redirdev> + <watchdog model='itco' action='reset'/> + <memballoon model='none'/> + </devices> + <qemu:commandline> + <qemu:arg value='-device'/> + <qemu:arg value='ivshmem-plain,id=shmem0,memdev=looking-glass,bus=pcie.0,addr=0x0a'/> + <qemu:arg value='-object'/> + <qemu:arg value='memory-backend-file,id=looking-glass,mem-path=/dev/kvmfr0,size=67108864,share=yes'/> + </qemu:commandline> +</domain> diff --git a/modules/hosts/valkyrie/default.nix b/modules/hosts/valkyrie/default.nix @@ -17,10 +17,12 @@ "docker" "mc-server" "roblox" + "vfio" ]; hmModules = [ "secrets" + "antigravity" "auth" "discord" "firefox" @@ -46,7 +48,7 @@ "latex" "wine" "games" - #"libvirt" + "libvirt" "niri" ]; @@ -56,6 +58,7 @@ asus = import ./_asus {}; dev = import ./_dev.nix {}; + vm = import ./_vm {inherit inputs;}; in { configurations.nixos.valkyrie = { system = "x86_64-linux"; @@ -67,7 +70,7 @@ in { imports = util.map.array nixosModules nixos ++ util.map.array mixedModules nixos - ++ [(asus.nixos or {}) (dev.nixos or {})] + ++ [(asus.nixos or {}) (dev.nixos or {}) (vm.nixos or {})] ++ util.map.array ["asus-zephyrus-ga402x-nvidia"] inputs.hardware.nixosModules; networking.hostId = builtins.substring 0 8 (builtins.hashString "md5" "valkyrie"); @@ -117,13 +120,15 @@ in { }; # Virtualisation - # virt.vfio = { - # setup = true; - # passthrough = [ - # "10de:28e0" # Graphics - # "10de:22be" # Audio - # ]; - # }; + virt.vfio = { + setup = true; + passthrough = [ + "10de:28e0" # Graphics + "10de:22be" # Audio + ]; + isolate = "2-4,10-12"; + hugepages = 16; + }; # Minecraft Server games.mc-servers = [ @@ -164,8 +169,8 @@ in { "systemd-journal" "video" "minecraft" - #"kvm" - #"libvirtd" + "kvm" + "libvirtd" ]; }; diff --git a/modules/system/filesystem.nix b/modules/system/filesystem.nix @@ -204,7 +204,7 @@ in { "/var/log" "/var/lib/AccountsService" "/var/lib/nixos" - "/var/lib/systemd/coredump" + "/var/lib/systemd" ]; }; } diff --git a/modules/virt/libvirt.nix b/modules/virt/libvirt.nix @@ -8,21 +8,14 @@ _: { }: let inherit (config.hardware.cpu) model; in { - environment.persist.directories = ["/var/lib/libvirt"]; - # Environment Setup + environment.persist.directories = ["/var/lib/libvirt"]; security.virtualisation.flushL1DataCache = "cond"; boot = { kernelModules = ["kvm-${model}"]; extraModprobeConfig = "options kvm_${model} nested=1"; }; - # VM Utilities - programs.virt-manager.enable = true; - - # VM Packages - environment.systemPackages = [pkgs.libguestfs]; - virtualisation = { kvmgt.enable = true; spiceUSBRedirection.enable = true; @@ -38,6 +31,10 @@ _: { }; }; }; + + # VM Utilities + programs.virt-manager.enable = true; + environment.systemPackages = [pkgs.libguestfs]; }; homeManager.libvirt = _: { diff --git a/modules/virt/vfio.nix b/modules/virt/vfio.nix @@ -6,108 +6,134 @@ _: { pkgs, ... }: let - inherit (builtins) concatStringsSep; - inherit - (lib) - generators - mkEnableOption - mkForce - mkIf - mkMerge - mkOption - types - ; cfg = config.virt.vfio; inherit (config.hardware.cpu) model; + shmSize = 64; # KVMFR Shared Memory size in { options.virt.vfio = { - setup = mkEnableOption "VFIO Setup Mode"; - passthrough = mkOption { + setup = lib.mkEnableOption "VFIO Setup Mode"; + passthrough = lib.mkOption { description = "PCI Device IDs for VFIO Passthrough"; - type = types.listOf types.str; + type = lib.types.listOf lib.types.str; default = []; example = [ "10de:28e0" # Graphics "10de:22be" # Audio ]; }; + + isolate = lib.mkOption { + description = "Host CPU set to isolate for pinned guest vCPUs"; + type = lib.types.str; + default = ""; + example = "2-4,10-12"; + }; + + hugepages = lib.mkOption { + description = "Number of 1 GiB hugepages to reserve for guest"; + type = lib.types.ints.unsigned; + default = 0; + example = 16; + }; }; - config = mkMerge [ - { + config = lib.mkMerge [ + (lib.mkIf cfg.setup { specialisation.vfio.configuration = { system.nixos.label = "special.vfio"; - virt.vfio.setup = true; - hardware.cpu.mode = mkForce "performance"; - }; - } - - (mkIf (!cfg.setup) { - specialisation.no-vfio.configuration = { - system.nixos.label = "special.no-vfio"; - virt.vfio.setup = false; + virt.vfio.setup = lib.mkForce false; }; + }) + (lib.mkIf (!cfg.setup) { # Disable GPU - hardware.gpu.enable = mkForce false; + hardware.gpu.enable = lib.mkForce false; boot = { - kernelParams = [ - "iommu=pt" - "${model}_iommu=on" - "kvm.ignore_msrs=1" - "kvm.report_ignored_msrs=0" - ("vfio-pci.ids=" + concatStringsSep "," cfg.passthrough) - ]; + kernelParams = + [ + "iommu=pt" + "${model}_iommu=on" + "kvm.ignore_msrs=1" + "kvm.report_ignored_msrs=0" + ("vfio-pci.ids=" + builtins.concatStringsSep "," cfg.passthrough) + ] + ++ lib.optionals (cfg.isolate != "") [ + "isolcpus=${cfg.isolate}" + "nohz_full=${cfg.isolate}" + "rcu_nocbs=${cfg.isolate}" + ] + ++ lib.optionals (cfg.hugepages > 0) [ + "default_hugepagesz=1G" + "hugepagesz=1G" + "hugepages=${toString cfg.hugepages}" + ]; initrd.kernelModules = [ "vfio" "vfio_pci" "vfio_iommu_type1" ]; + + # KVMFR + extraModulePackages = [config.boot.kernelPackages.kvmfr]; + kernelModules = ["kvmfr"]; + extraModprobeConfig = "options kvmfr static_size_mb=${toString shmSize}"; }; # Looking Glass - systemd.tmpfiles.rules = ["f /dev/shm/looking-glass 0660 root qemu-libvirtd -"]; - environment = { - systemPackages = [pkgs.looking-glass-client]; - etc."looking-glass-client.ini".text = generators.toINI {} { - app.renderer = "egl"; - win = { - title = "Virtual Machine"; - autoResize = "yes"; - borderless = "no"; - dontUpscale = "yes"; - fullScreen = "no"; - keepAspect = "yes"; - maximize = "no"; - noScreensaver = "yes"; - quickSplash = "yes"; - uiSize = 16; - }; - - egl = { - scale = 2; - multisample = "yes"; - vsync = "yes"; - }; + environment.systemPackages = [pkgs.looking-glass-client]; + services.udev.extraRules = '' + SUBSYSTEM=="kvmfr", KERNEL=="kvmfr0", MODE="0660", GROUP="kvm", TAG+="systemd" + ''; - input = { - autoCapture = "yes"; - grabKeyboardOnFocus = "yes"; - rawMouse = "yes"; - releaseKeysOnFocusLoss = "yes"; - }; + users.users.qemu-libvirtd.extraGroups = ["kvm"]; + virtualisation.libvirtd.qemu.verbatimConfig = '' + namespaces = [] + cgroup_device_acl = [ + "/dev/null", "/dev/full", "/dev/zero", + "/dev/random", "/dev/urandom", + "/dev/ptmx", "/dev/kvm", + "/dev/userfaultfd", + "/dev/kvmfr0" + ] + ''; - spice = { - enable = "yes"; - clipboard = "yes"; - }; - - wayland = { - warpSupport = "yes"; - fractionScale = "no"; - }; + environment.etc."looking-glass-client.ini".text = lib.generators.toINI {} { + app = { + renderer = "egl"; + shmFile = "/dev/kvmfr0"; + }; + win = { + title = "Virtual Machine"; + autoResize = "yes"; + borderless = "no"; + dontUpscale = "yes"; + fullScreen = "no"; + keepAspect = "yes"; + maximize = "no"; + noScreensaver = "yes"; + quickSplash = "yes"; + uiSize = 16; + }; + egl = { + scale = 2; + multisample = "yes"; + vsync = "yes"; + }; + input = { + autoCapture = "yes"; + grabKeyboardOnFocus = "yes"; + rawMouse = "yes"; + releaseKeysOnFocusLoss = "yes"; + }; + spice = { + enable = "yes"; + clipboard = "yes"; + }; + wayland = { + warpSupport = "yes"; + fractionScale = "no"; }; }; }) diff --git a/packages/_obsidian.nix b/packages/_obsidian.nix @@ -0,0 +1,75 @@ +# Obsidian Package Builders # +{ + lib, + stdenvNoCC, + fetchFromGitHub, + fetchurl, +}: { + mkTheme = { + pname, + meta ? {}, + ... + } @ metadata: + stdenvNoCC.mkDerivation { + inherit pname; + version = builtins.substring 0 7 metadata.rev; + + src = fetchFromGitHub { + inherit (metadata) owner repo rev; + hash = metadata.sha256; + }; + + dontBuild = true; + installPhase = '' + runHook preInstall + mkdir -p $out + cp manifest.json theme.css $out/ + runHook postInstall + ''; + + meta = {maintainers = ["maydayv7"];} // meta; + }; + + mkPlugin = { + pname, + owner, + repo, + version, + hashes, + meta ? {}, + ... + }: let + base = "https://github.com/${owner}/${repo}/releases/download/${version}"; + main = fetchurl { + url = "${base}/main.js"; + hash = hashes.main; + }; + manifest = fetchurl { + url = "${base}/manifest.json"; + hash = hashes.manifest; + }; + styles = + if hashes ? styles + then + fetchurl { + url = "${base}/styles.css"; + hash = hashes.styles; + } + else null; + in + stdenvNoCC.mkDerivation { + inherit pname version; + + dontUnpack = true; + installPhase = '' + runHook preInstall + mkdir -p $out + cp ${main} $out/main.js + cp ${manifest} $out/manifest.json + ${lib.optionalString (styles != null) "cp ${styles} $out/styles.css"} + runHook postInstall + ''; + + meta = {maintainers = ["maydayv7"];} // meta; + }; +} diff --git a/packages/obsidian-adwaita/default.nix b/packages/obsidian-adwaita/default.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + metadata = import ./metadata.nix; + obsidian = import ../_obsidian.nix { + inherit lib; + inherit (pkgs) stdenvNoCC fetchFromGitHub fetchurl; + }; +in + obsidian.mkTheme { + pname = "obsidian-adwaita"; + owner = "birneee"; + repo = "obsidian-adwaita-theme"; + inherit (metadata) rev sha256; + + meta = { + description = "GNOME Adwaita theme for Obsidian"; + homepage = metadata.repo; + license = lib.licenses.mit; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/obsidian-adwaita/metadata.nix b/packages/obsidian-adwaita/metadata.nix @@ -0,0 +1,5 @@ +{ + repo = "https://github.com/birneee/obsidian-adwaita-theme"; + rev = "75efcbbd6a89abc04265b467997fb60638de9157"; + sha256 = "sha256-snjCfPNMUhfHxD+4WGSSvHnvoMhRS03KxOmZwYULMTM="; +} diff --git a/packages/obsidian-calendar/default.nix b/packages/obsidian-calendar/default.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + metadata = import ./metadata.nix; + obsidian = import ../_obsidian.nix { + inherit lib; + inherit (pkgs) stdenvNoCC fetchFromGitHub fetchurl; + }; +in + obsidian.mkPlugin { + pname = "obsidian-calendar"; + owner = "liamcain"; + repo = "obsidian-calendar-plugin"; + inherit (metadata) version hashes; + + meta = { + description = "Calendar view of your daily notes for Obsidian"; + homepage = metadata.repo; + license = lib.licenses.mit; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/obsidian-calendar/metadata.nix b/packages/obsidian-calendar/metadata.nix @@ -0,0 +1,10 @@ +{ + repo = "https://github.com/liamcain/obsidian-calendar-plugin"; + rev = "1.5.10"; + version = "1.5.10"; + skip = true; + hashes = { + main = "sha256-f7M56c+f2+WoAforirhbNmtbN3f70ZPLyHKLwncR0SU="; + manifest = "sha256-8+lYEzhkhRK6oS1bRYSQ9/02eRj3vba9hhcc5Xvn0Is="; + }; +} diff --git a/packages/obsidian-catppuccin/default.nix b/packages/obsidian-catppuccin/default.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + metadata = import ./metadata.nix; + obsidian = import ../_obsidian.nix { + inherit lib; + inherit (pkgs) stdenvNoCC fetchFromGitHub fetchurl; + }; +in + obsidian.mkTheme { + pname = "obsidian-catppuccin"; + owner = "catppuccin"; + repo = "obsidian"; + inherit (metadata) rev sha256; + + meta = { + description = "Soothing pastel theme for Obsidian"; + homepage = metadata.repo; + license = lib.licenses.mit; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/obsidian-catppuccin/metadata.nix b/packages/obsidian-catppuccin/metadata.nix @@ -0,0 +1,5 @@ +{ + repo = "https://github.com/catppuccin/obsidian"; + rev = "1e7aabc47addeefe79dd0362c9976dfa3367c1f9"; + sha256 = "sha256-9fSFj9Tzc2aN9zpG5CyDMngVcwYEppf7MF1ZPUWFyz4="; +} diff --git a/packages/obsidian-git/default.nix b/packages/obsidian-git/default.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + metadata = import ./metadata.nix; + obsidian = import ../_obsidian.nix { + inherit lib; + inherit (pkgs) stdenvNoCC fetchFromGitHub fetchurl; + }; +in + obsidian.mkPlugin { + pname = "obsidian-git"; + owner = "Vinzent03"; + repo = "obsidian-git"; + inherit (metadata) version hashes; + + meta = { + description = "Backup and version your Obsidian vault with git"; + homepage = metadata.repo; + license = lib.licenses.mit; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/obsidian-git/metadata.nix b/packages/obsidian-git/metadata.nix @@ -0,0 +1,11 @@ +{ + repo = "https://github.com/Vinzent03/obsidian-git"; + rev = "2.38.5"; + version = "2.38.5"; + skip = true; + hashes = { + main = "sha256-u0YYGzZxWz93ahwkZ/zbvi4jxO/nux2b8wJwDUK2AZ8="; + manifest = "sha256-qqfMsQ9jfPNhoLZZL5M3k5b5uqlDAAr8pCrULrVSZRY="; + styles = "sha256-9auT9NW03RvR5XeGTFx5CH9639RIrDRuBInlhHzmki0="; + }; +} diff --git a/packages/obsidian-outliner/default.nix b/packages/obsidian-outliner/default.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + metadata = import ./metadata.nix; + obsidian = import ../_obsidian.nix { + inherit lib; + inherit (pkgs) stdenvNoCC fetchFromGitHub fetchurl; + }; +in + obsidian.mkPlugin { + pname = "obsidian-outliner"; + owner = "vslinko"; + repo = "obsidian-outliner"; + inherit (metadata) version hashes; + + meta = { + description = "Work with your lists like in Workflowy or RoamResearch, in Obsidian"; + homepage = metadata.repo; + license = lib.licenses.mit; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/obsidian-outliner/metadata.nix b/packages/obsidian-outliner/metadata.nix @@ -0,0 +1,11 @@ +{ + repo = "https://github.com/vslinko/obsidian-outliner"; + rev = "4.10.1"; + version = "4.10.1"; + skip = true; + hashes = { + main = "sha256-7bBVPyIR2c6lHZNY+YyJ73QXLn8mSP+tqWI7W8VzQAQ="; + manifest = "sha256-IBPrSDIE9IpK+j5ttIliCWz6jr9q6HHxG4PDmpJqGTQ="; + styles = "sha256-eSKiZIg4lOafIsN/VJdE99RtHekm/IAqpOlMdc0vvOs="; + }; +} diff --git a/packages/obsidian-style-settings/default.nix b/packages/obsidian-style-settings/default.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + metadata = import ./metadata.nix; + obsidian = import ../_obsidian.nix { + inherit lib; + inherit (pkgs) stdenvNoCC fetchFromGitHub fetchurl; + }; +in + obsidian.mkPlugin { + pname = "obsidian-style-settings"; + owner = "mgmeyers"; + repo = "obsidian-style-settings"; + inherit (metadata) version hashes; + + meta = { + description = "Configure CSS theme, snippet, and plugin settings in Obsidian"; + homepage = metadata.repo; + license = lib.licenses.gpl3Only; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/obsidian-style-settings/metadata.nix b/packages/obsidian-style-settings/metadata.nix @@ -0,0 +1,11 @@ +{ + repo = "https://github.com/mgmeyers/obsidian-style-settings"; + rev = "1.0.9"; + version = "1.0.9"; + skip = true; + hashes = { + main = "sha256-GCirqs2rTFV4twWmJcWFswUS+O+tTHz8WhjnDMNVdGg="; + manifest = "sha256-nP/cIM8qoTVIIOAFC2lLD5tXZEbj1dRKNq6LAYflv7g="; + styles = "sha256-7nk30r5QZTqJzLMK5fBXKyNQfVt/EyjQBScaNjB1v9g="; + }; +} diff --git a/packages/superpowers/default.nix b/packages/superpowers/default.nix @@ -0,0 +1,31 @@ +{ + lib, + pkgs, + ... +}: +with pkgs; let + metadata = import ./metadata.nix; +in + stdenv.mkDerivation { + pname = "superpowers"; + version = metadata.rev; + + src = fetchFromGitHub { + owner = "obra"; + repo = "superpowers"; + inherit (metadata) rev sha256; + }; + + dontBuild = true; + installPhase = '' + mkdir -p $out/share/superpowers + cp -r ./. $out/share/superpowers/ + ''; + + meta = with lib; { + description = "Composable agentic skills library for coding agents"; + homepage = metadata.repo; + license = licenses.mit; + maintainers = ["maydayv7"]; + }; + } diff --git a/packages/superpowers/metadata.nix b/packages/superpowers/metadata.nix @@ -0,0 +1,6 @@ +{ + repo = "https://github.com/obra/superpowers"; + release = true; + rev = "v6.0.3"; + sha256 = "sha256-+lT2a/qq0SF4k0PgnEDKiuidVlZX2p0vEso4d/5T1os="; +} diff --git a/site/content/blog/nixos/prelude.md b/site/content/blog/nixos/prelude.md @@ -1,7 +1,7 @@ +++ title = "Prelude" description = "My NixOS Configuration" -date = 2025-12-15 +date = 2025-12-17 [taxonomies] series = ["NixOS Desktop"] diff --git a/site/content/blog/nixos/win11vm.md b/site/content/blog/nixos/win11vm.md @@ -0,0 +1,321 @@ ++++ +title = "Windows VM" +description = "A declarative, performant Windows 11 VM with dGPU passthrough and Looking Glass" +date = 2026-06-27 + +[taxonomies] +series = ["NixOS Desktop"] +tags = ["NixOS", "VFIO", "Virtualisation"] + +[extra] +ToC = true +edit = true +comments = true ++++ + +# Intro + +This is a guide to running a Windows 11 VM with an NVIDIA dGPU passed straight through to it, displayed via [Looking Glass](https://looking-glass.io/) directly on the primary display (no secondary monitor required), CPU-pinned and hugepage-backed for lower latency, and defined declaratively with [NixVirt](https://github.com/AshleyYakeley/NixVirt) - all on NixOS. + +I run this on a laptop with an AMD Ryzen CPU, an integrated Radeon iGPU, and an NVIDIA RTX 4060. Any other host with 2 GPUs, or a dGPU + iGPU, should work the same way. + +# Prerequisites + +- CPU support for **IOMMU** +- A GPU you can dedicate to the VM that sits alone in its **own IOMMU group**. Check with: + + ```console + $ for d in /sys/kernel/iommu_groups/*/devices/*; do + echo "Group ${d%/devices/*}: $(lspci -nns ${d##*/})"; done | sort + ``` + + Find your GPU and note its `vendor:device` IDs: + + ```console + $ lspci -nnk -s 01:00.0 + 01:00.0 VGA compatible controller [0300]: NVIDIA ... [10de:28e0] + 01:00.1 Audio device [0403]: NVIDIA ... [10de:22be] + ``` + +- Download ISOs: + - **Windows 11** - Download from [Microsoft](https://www.microsoft.com/software-download/windows11). + - **virtio-win** - The para-virtual Windows drivers: `nix build nixpkgs#virtio-win` + +# Binding the GPU for VFIO + +The core idea is to bind the GPU to the `vfio-pci` stub driver **at boot, before its real driver** kicks in. How you do that depends on your setup: + +**Case A - the host renders on a _different_ GPU** (Eg. your desktop runs on the iGPU). Just bind the dGPU permanently: + +```nix +boot.initrd.kernelModules = [ "vfio" "vfio_pci" "vfio_iommu_type1" ]; +boot.kernelParams = [ + "amd_iommu=on" "iommu=pt" + "vfio-pci.ids=10de:28e0,10de:22be" # your GPU + its audio function +]; +``` + +**Case B - the host renders on the _same_ GPU you want to pass** (Eg. NVIDIA PRIME Sync, where the desktop holds `nvidia_drm`). A NixOS [specialisation](https://nixos.org/manual/nixos/stable/#sec-specialisation) can be used: your default boot keeps the GPU for the desktop, and a different boot entry binds it to `vfio-pci` instead: + +``` +specialisation.vfio.configuration = { + boot = { + blacklistedKernelModules = [ # don't load host driver + "nvidia" + "nvidia_drm" + "nvidia_modeset" + "nvidia_uvm" + ]; + ... # rest is same as above + }; +}; +``` + +You reboot, pick the `vfio` entry , and reboot back to normal afterwards. + +Either way, confirm after booting: + +```console +$ lspci -nnk -s 01:00.0 | grep 'in use' + Kernel driver in use: vfio-pci +``` + +_Dynamic GPU passthrough (w/o reboot) is possible, but is cumbersome and comes with its own set of drawbacks_ + +# Building the VM + +First, enable libvirt/QEMU on the host. The essentials are `libvirtd` itself, `swtpm` (the software TPM that Windows 11 requires), SPICE USB redirection, and `virt-manager` to drive it all - plus adding your user to the `libvirtd` and `kvm` groups so you can manage VMs and open `/dev/kvm` without `sudo`: + +``` +{ pkgs, ... }: { + virtualisation.libvirtd = { + enable = true; + qemu = { + package = pkgs.qemu_kvm; + runAsRoot = false; + swtpm.enable = true; + }; + }; + + virtualisation.spiceUSBRedirection.enable = true; + programs.virt-manager.enable = true; + + users.users.<you>.extraGroups = [ "libvirtd" "kvm" ]; +} +``` + +The guest disk should be a raw block device or image (rather than an emulated SATA disk) for performance. I back it with a sparse ZFS zvol (a `qcow2` file works just as well): + +```console +$ sudo zfs create -s -V 120G -o volblocksize=16k <pool>/vm/windows +# appears as /dev/zvol/<pool>/vm/windows +``` + +Now create a standard Windows 11 guest (`virt-manager` makes this easy, you may refer to [**this guide**](https://sysguides.com/install-a-windows-11-virtual-machine-on-kvm)). The settings that matter: + +- **Q35** chipset + **UEFI/OVMF** with **Secure Boot**, and **TPM 2.0** (`swtpm`) - Windows 11 requires the latter two. +- **`host-passthrough`** CPU. +- **virtio everywhere** - disk (on the zvol above) with `cache=none`, `io=native`, `discard=unmap`; virtio NIC (for performance). +- **Hyper-V enlightenments** so Windows uses its kernel fast paths. +- A **QEMU guest agent** channel. + +On AMD, in the Hyper-V block, omit `<evmcs/>` - that enlightenment is Intel-only and the VM won't start with it. A good set: + +```xml +<hyperv mode="custom"> + <relaxed state="on"/> <vapic state="on"/> <spinlocks state="on" retries="8191"/> + <vpindex state="on"/> <runtime state="on"/> <synic state="on"/> <stimer state="on"/> + <frequencies state="on"/> <tlbflush state="on"/> <ipi state="on"/> <avic state="on"/> +</hyperv> +``` + +Since the disk is virtio, the Windows installer won't see it until you **Load driver** from the mounted `virtio-win.iso` (`viostor\w11\amd64`, then `NetKVM` for network). After install, run `virtio-win-guest-tools` for the rest. Finally, add the GPU (and its audio function) as PCI host devices. + +# Looking Glass + +A passed-through GPU usually has no monitor attached, so [Looking Glass](https://looking-glass.io/) captures the guest framebuffer and passes it through a shared-memory device into a client window on the NixOS host, with very low latency. + +The transport is the `kvmfr` kernel module, which exposes `/dev/kvmfr0`: + +```nix +boot = { + extraModulePackages = [ config.boot.kernelPackages.kvmfr ]; + kernelModules = [ "kvmfr" ]; + extraModprobeConfig = "options kvmfr static_size_mb=64"; +}; + +# Add your user to the 'kvm' group to open the device +services.udev.extraRules = '' + SUBSYSTEM=="kvmfr", KERNEL=="kvmfr0", MODE="0660", GROUP="kvm", TAG+="systemd" +''; + +environment.systemPackages = [ pkgs.looking-glass-client ]; +``` + +Give the VM the matching `ivshmem` device via a `<qemu:commandline>` block that points `mem-path` at `/dev/kvmfr0` (this needs `xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'` on the root `<domain>` tag): + +```xml +<qemu:commandline> + <qemu:arg value='-device'/> + <qemu:arg value='ivshmem-plain,id=shmem0,memdev=looking-glass,bus=pcie.0,addr=0x0a'/> + <qemu:arg value='-object'/> + <qemu:arg value='memory-backend-file,id=looking-glass,mem-path=/dev/kvmfr0,size=67108864,share=yes'/> +</qemu:commandline> +``` + +Inside Windows, install the following: + +1. **NVIDIA driver** - Automatic via Windows Update. +2. **A virtual display driver** ([MikeTheTech's VDD](https://github.com/VirtualDrivers/Virtual-Display-Driver)) - gives the desktop a head to render on. Its config (`C:\VirtualDisplayDriver\vdd_settings.xml`) can pin the virtual display to a specific GPU and define exact modes, so bind it to the passed-through GPU at your native resolution: + + ```xml + <vdd_settings> + <monitors><count>1</count></monitors> + <gpu><friendlyname>NVIDIA GeForce RTX 4060 Laptop GPU</friendlyname></gpu> + <resolutions> + <resolution><width>2560</width><height>1600</height><refresh_rate>165</refresh_rate></resolution> + </resolutions> + </vdd_settings> + ``` + +3. **Looking Glass (host)** - its build **must match the client build exactly**. Its installer also provides the IVSHMEM driver for the "PCI standard RAM Controller" device in Device Manager. Install it as a **service** (`looking-glass-host.exe InstallService`) rather than launching it interactively - the service can capture the secure desktop, so UAC prompts don't black out the stream, and it starts at boot on its own. + +Then run `looking-glass-client` on the host, and a full-resolution Windows desktop appears in a window, GPU-accelerated, captured off the GPU framebuffer over DirectX 12. Keyboard, mouse and clipboard ride back over SPICE. + +> [!NOTE] +> Enabling **autologon** in Windows lets the host app (and the virtual display) come up automatically at boot - otherwise the desktop only exists after you log in. + +# Performance tuning + +The values below (which cores, how many hugepages) are **machine-specific** - compute them from your own topology. + +## CPU pinning + +By default the scheduler shuffles vCPUs across host threads and runs QEMU's emulator/IO threads on top of them, which shows up as frame-time spikes. +Pin each guest vCPU to a fixed host thread, matching SMT siblings, and push the emulator/IO threads onto separate housekeeping cores. +Find your sibling pairs first: + +```console +$ cat /sys/devices/system/cpu/cpu*/topology/thread_siblings_list +``` + +For a 6-vCPU guest whose host has siblings `N`/`N+8`, mapping three physical cores (2, 3, 4) and keeping cores 0–1 for the host: + +```xml +<vcpu placement='static'>6</vcpu> +<iothreads>1</iothreads> +<cputune> + <vcpupin vcpu='0' cpuset='2'/> <vcpupin vcpu='1' cpuset='10'/> <!-- core 2 --> + <vcpupin vcpu='2' cpuset='3'/> <vcpupin vcpu='3' cpuset='11'/> <!-- core 3 --> + <vcpupin vcpu='4' cpuset='4'/> <vcpupin vcpu='5' cpuset='12'/> <!-- core 4 --> + <emulatorpin cpuset='0-1,8-9'/> + <iothreadpin iothread='1' cpuset='0-1,8-9'/> +</cputune> +``` + +Assign that iothread to the virtio disk so storage interrupts stay off the cores: + +```xml +<driver name='qemu' type='raw' cache='none' io='native' discard='unmap' iothread='1'/> +``` + +## Core isolation + +Pinning chooses which host threads the guest uses - isolating those cores stops the host from scheduling its own work there. +If your passthrough lives in a dedicated boot entry, scope the isolation to it so it costs nothing during normal use: + +```nix +boot.kernelParams = [ + "isolcpus=2-4,10-12" + "nohz_full=2-4,10-12" + "rcu_nocbs=2-4,10-12" +]; +``` + +## Hugepages + +Backing the guest RAM with 1 GiB hugepages reduces TLB misses and the cost of memory virtualisation (more about latency consistency than throughput). +Reserve them at boot - and since they're held exclusively, this is another good thing to scope to the boot entry: + +```nix +boot.kernelParams = [ "default_hugepagesz=1G" "hugepagesz=1G" "hugepages=16" ]; # 16 GiB +``` + +Request them in the domain, and drop the memory balloon (it fights fixed hugepage-backed RAM): + +```xml +<memoryBacking><hugepages><page size='1048576' unit='KiB'/></hugepages></memoryBacking> +... +<memballoon model='none'/> +``` + +> [!IMPORTANT] +> Keep these in sync: the `<cputune>` cpuset must match `isolcpus`, and the hugepage count must match the guest's RAM. + +## Other tweaks + +Run the host CPU governor at `performance` (`powerManagement.cpuFreqGovernor = "performance"`). Inside Windows, a few standard tweaks help: + +- `bcdedit /set useplatformclock No` - with Hyper-V enlightenments on, leaving it enabled hurts performance. +- Disable **SysMain / SuperFetch**, set **Visual Effects → best performance**, and disable **ScheduledDefrag** (never defrag a virtio disk). + +# Declarative Domain configuration + +[NixVirt](https://github.com/AshleyYakeley/NixVirt) manages libvirt domains from Nix and accepts an XML file directly: + +```nix +{ inputs, pkgs, ... }: { + imports = [ inputs.nixvirt.nixosModules.default ]; + virtualisation.libvirt = { + enable = true; + package = pkgs.libvirt; + connections."qemu:///system".domains = [ + { + definition = ./windows.xml; + active = null; + } + ]; + }; +} +``` + +`active = null` is important: NixVirt redefines the domain on every rebuild but never starts, stops or destroys a running guest. + +If you use **impermanence** (an erase-on-boot root), the domain definition is now regenerated from Nix, so you only need to persist the actual state. Two directories matter: + +```nix +environment.persist.directories = [ + "/var/lib/libvirt" # NVRAM, swtpm state, networks + "/var/lib/systemd" # Host key that decrypts libvirt's secrets +]; +``` + +Also ensure the disk image itself is persisted. + +# Networking + +The virtio NIC on libvirt's default network gives the guest internet through NAT out of the box. Just run the following command once: + +```console +$ virsh -c qemu:///system net-autostart default +``` + +# Useful commands + +Run as your user (in the `libvirtd` + `kvm` groups, no `sudo` needed): + +```console +$ virsh -c qemu:///system list --all +$ virsh -c qemu:///system start windows +$ virsh -c qemu:///system shutdown windows --mode=agent # Clean shutdown +$ virsh -c qemu:///system destroy windows # Force off +$ virsh -c qemu:///system net-dhcp-leases default # Guest IP + +# Peek at the guest headlessly +$ virsh -c qemu:///system screenshot windows --file /tmp/vm.ppm +$ nix shell nixpkgs#imagemagick -c magick /tmp/vm.ppm /tmp/vm.png +``` + +# Wrapping up + +You may refer to my own [configuration](https://github.com/maydayv7/dotfiles), which sets up all of the above (on the host `valkyrie`), if you'd like a reference. diff --git a/site/content/test.md b/site/content/test.md @@ -59,18 +59,29 @@ fn foo(arg: String) -> Result<u32, Io::Error> { ## Alerts +> [!NOTE] +> Highlights information to be taken into account, even when skimming. + +> [!TIP] +> Optional information for greater success. + +> [!IMPORTANT] +> Necessary crucial information. + > [!WARNING] +> Critical content demanding immediate attention. + +> [!CAUTION] +> Negative potential consequences of an action. + +## Block Quote + > Do not fall in love with people like me. > I will take you to museums, and parks, and monuments, and kiss you in every beautiful place, so that you can never go back to them without tasting me like blood in your mouth. > I will destroy you in the most beautiful way possible. > And when I leave you will finally understand, why storms are named after people. > --> **Caitlyn Siehl** -## Block Quote - -> Heroes get remembered, but legends never die. -> --> **Babe Ruth** - ## Math Supports Math Type-Setting using [$\KaTeX$](http://khan.github.io/KaTeX/) diff --git a/site/sass/_post.scss b/site/sass/_post.scss @@ -4,9 +4,13 @@ blockquote { color: var(--color); font-style: italic; margin-left: 0.5em; - padding: 1em 10px 1px 20px; + padding: 1em 10px 0.8em 20px; position: relative; + p:last-child { + margin-bottom: 0; + } + &:before { color: var(--accent-alpha-70); content: "\201C"; @@ -24,13 +28,18 @@ blockquote { &[class*="markdown-alert"] { border-left: 4px solid; font-style: normal; - padding: 0.25em 1em 0.25em 2em; + padding: 0.25em 1em 0.25em 2.8em; position: relative; &:before { position: absolute; left: 0.4em; - font-size: 1.5em; + top: 0.35em; + width: 1.2em; + font-family: "JetBrainsMono", monospace; + font-size: 1.2em; + line-height: 1; + text-align: center; } p { @@ -42,7 +51,7 @@ blockquote { border-left-color: var(--accent-secondary); &:before { - content: "ℹ"; + content: "\f449"; color: var(--accent-secondary); } } @@ -51,7 +60,7 @@ blockquote { border-left-color: var(--accent-secondary-alpha-70); &:before { - content: "✦"; + content: "\f400"; color: var(--accent-secondary-alpha-70); } } @@ -60,7 +69,7 @@ blockquote { border-left-color: var(--accent); &:before { - content: "‼"; + content: "\f643"; color: var(--accent); } } @@ -69,7 +78,7 @@ blockquote { border-left-color: var(--accent-alpha-70); &:before { - content: "⚠"; + content: "\f421"; color: var(--accent-alpha-70); } } @@ -79,22 +88,35 @@ blockquote { background-color: var(--accent-alpha-20); &:before { - content: "◆"; + content: "\f28b"; color: var(--accent); } } } +.code-wrapper { + position: relative; + + &:hover .copy, + .copy:focus { + opacity: 1; + } +} + .copy { background: var(--background); border-color: var(--accent-secondary); border-radius: $corner-radius; color: var(--color); font-family: inherit; - display: block; font-size: 0.8em; - margin: -15px 0 -2px auto; + opacity: 0; padding: 3px 8px; + position: absolute; + top: 8px; + right: 8px; + transition: opacity 0.15s ease; + z-index: 1; &:active { background-color: var(--accent-alpha-70); diff --git a/site/sass/_syntax.scss b/site/sass/_syntax.scss @@ -215,8 +215,8 @@ .z-invalid.z-deprecated, .z-invalid.z-illegal, .z-invalid.z-unimplemented { - color: var(--accent); - font-style: italic; + color: var(--color); + background-color: transparent; } // Markup diff --git a/site/shell.nix b/site/shell.nix @@ -1,4 +1,4 @@ -{pkgs ? import ../., ...}: +{pkgs ? import <nixpkgs> {}, ...}: pkgs.mkShell { name = "Website"; packages = with pkgs; [ diff --git a/site/static/js/clipboard.js b/site/static/js/clipboard.js @@ -19,10 +19,13 @@ document.querySelectorAll("pre > code").forEach(function (codeBlock) { }); var pre = codeBlock.parentNode; - if (pre.parentNode.classList.contains("highlight")) { - var highlight = pre.parentNode; - highlight.parentNode.insertBefore(button, highlight); - } else { - pre.parentNode.insertBefore(button, pre); - } + var block = pre.parentNode.classList.contains("highlight") + ? pre.parentNode + : pre; + + var wrapper = document.createElement("div"); + wrapper.className = "code-wrapper"; + block.parentNode.insertBefore(wrapper, block); + wrapper.appendChild(block); + wrapper.appendChild(button); });