未完待须!

TODO:

  • Looking-glass的kvmfr加速
  • 无需重启进行显卡隔离(通过脚本重启DE+卸载显卡内核模块+VFIO)
  • 无需重启DE进行显卡隔离(KDE下应该是可以做到的,Gnome这边因为Mutter的陈年老Issues没人解决暂时不行)

以上内容其实我都已经实现了,不过还没时间写Blog,欢迎参考我的Flake仓库以及催更

以及有时间我再折腾一下vGPU吧

前情提要

一些牢骚

起因

最早我切换到Linux时,并没有将其作为主力系统,最早用的Ubuntu,后来用Manjaro用了一段时间。

真正让我彻底拥抱Linux的是Github给我推荐了一个叫做archlinux 简明指南的仓库,我跟随这篇非常优秀的指南转向了ArchLinux,这篇指南中推荐我使用Btrfs作为文件系统,我当时没想多少,照着用了,后来通过Timeshift接触了Btrfs的快照,并学会了用pacman的脚本系统自动在安装/更新软件包时创建快照,这才把我正式拉入Linux的世界。

但是吧。。Arch。。不滚挂不炸依赖那就不叫Arch了,虽然我渐渐的有了越来越强的排错能力,但是我仍然期望着找到更好地解决方案。

转机

事情就一直这样继续着,直到一位叫做Sezren的好朋友给我强烈安利了NixOS!
函数式、声明式、可复现的优点让我着迷,我尝试后一下就决定要使用这个发行版!
但,优点说完了,剩下的呢?

对我来说,第一个难住我的是,我英语不好,而NixOS的文档和社区全部都是英文,国内在使用NixOS的人可能甚至都不破千(当时),那中文资料那只能是天方夜谭。
还好,时代和我都在进步,越来越多的词汇量和非常好用的划词翻译让我渐渐越过了这个门槛,NixOS-CN社区也给了我很大的帮助。

第二个摆在我面前的问题是,相当一部分我的常用软件包,要不就是根本没有,要不就是版本老旧。但是还好Flatpak和Appimage帮助我暂缓了这个问题。后来,在朋友们的帮助下,我逐渐学会了打包,也渐渐地开始给NixOS社区贡献软件包代码,摆脱了Flatpak和Appimage。

解决了拦住我的问题,并随着我越来越深入Linux,Nix的优点也越来越耀眼,我也非常庆幸能看到Devbox这样基于Nix的工具能够流行,让更多人用到如此强大好用的包管理器
如果你想详细了解Nix的优点,你或许可以参考以下中文博客:

矛盾

是的,它们很好用,很强大,很符合我对理想中操作系统的想像。但是很遗憾,我没有办法彻底和Windows断交,因为并不是所有的软件都能够在Linux上理想的运行,尤其是游戏。

那,怎么办呢?

最基本的,双系统,但是它的灵活性很差,每次切换意味着重启,且不能同时运行。
而且因为文件系统的不兼容,你的分区表会变得很乱,反正这让我很烦躁。
很早之前我就听说了GPU直通这个概念,于是现在我尝试去使用它。

最基本的,你可以使用虚拟机,这基于虚拟化技术,可以做到CPU,内存(控制器)等设备的几乎无损共用。一切看似很美好。但是日常使用中,你不可能离开GPU,你的游戏,甚至是桌面,都需要GPU来提高画面的渲染。

但是现在你能轻松用到的虚拟化技术,要么完全不能具备GPU的虚拟化能力,要么最多能提供性能一言难尽的OpenGL加速,日常使用就连桌面都卡成鬼,别说3D性能了
事实上,整个市场中,能提供可用的GPU虚拟化的,只有AMD和NVIDIA(现在Intel似乎也可以了),开源社区没有办法提供这样的能力,你需要去购买它们的专业卡,然后才能通过闭源驱动使用它。

Tips:不过目前NVIDIA的GPU虚拟化是所有新显卡都拥有的能力,不能使用是驱动层面的人为限制(驱动/内核模块内甚至有提供相关能力的代码,只是检测到消费级显卡给你禁用了),目前在Windows上通过HyperV使用GPU虚拟化能力无任何限制,基本开箱即用,Linux上也有一些通过逆向工程实现的破解NVIDIA闭源内核模块从而使用GPU虚拟化的开源项目,之后会填坑的!

所以?

不能虚拟化,但是我可以把整个PCI设备共享给虚拟机啊!这就是直通,将物理设备直接接入到虚拟机。
因为IOMMU的出现,上述操作得以被轻松实现,但是一个问题是,对于台式机,你将视频信号线接入哪个GPU,就是从哪个GPU获取显示画面。

落实到实际中,由于大部分显示器都有不止一个视频接口,可以给两个GPU都接上视频线,接到不同接口,通过显示器切换信号源来切换系统。

但是在笔记本中,为了实现核显和独显的配合,研发出了Hybrid这种架构,其本质是显示器接入核显,需要独显渲染的内容由独显渲染完通过总线传到到核显中,核显对其进行混合,最终由核显输出,独显本身可以压根没连线。

在这个时候,外接显示器这个需求出现了,在单个接口也就是只可以外接一台显示器时,核显拥有提供两个接口这样级别io的能力,但是随着typec接口的普及和需求的越来越高,因为usb type-c里包含了完整的dp,现在笔记本的c口一般都能视频输出,这意味着现在的笔记本动辄拥有外接两三个显示器的能力,这超出了核显的io能力,并且因为这些强大的笔记本一般都拥有独显,所以一般会把这些接口按照能力分给不同的显卡。

这么做还有一个好处,刚刚说的Hybrid架构,因为最终需要经过总线(会占用带宽),还需要由核显混成,其实是会有性能损耗的,这样如果把外接显示器接到独显,并且关闭内屏,就可以实现直连独显,解决这部分损耗提升性能。

这些年的笔记本厂商宣传的一个功能:“独显直连”,就是通过允许你通过打开它,把内屏也直接接到独显,绕过核显,提升性能。

40系GPU宣称实现了独显直连热切换,不过这似乎没什么难度,本质是只是显示器接到哪个接口罢了,不过因为我没40系笔记本,不知道具体还有没有什么实现细节。

问题

但是上述Hybrid架构,对于GPU直通来说是灾难性的

我们可以分开考虑:

在你外接显示器时,这个问题还没那么严重,你如果接入到连接核显的接口,就可以使用宿主机系统,如果接入到连接独显的接口,就可以使用直通了的虚拟机。

在你不外接显示器时,问题就变得难以解决了,因为你的独显根本就没有接显示器,首先可以想到的,可以通过远程桌面访问它,但是远程桌面的流畅度都问题很大(以及RDP不能调用GPU),更别说游戏了,完全失去了使用GPU直通的意义。

一个可能的解决方法是,你可以接一个显示接口诱骗器(HDMI诱骗器之类的),让你的独显以为你接了一个显示器,然后再通过parsec或者looking-glass等低延迟串流解决方案进行使用。

解决…?

通过非常久的折腾,我捣鼓出了一个我的解决方案:

首先,没有接显示器这个事实没办法改变,这意味着通过直接的电信号无损的把画面从虚拟机传到显示器是不可能实现的,需要通过串流传输画面是必然的,这里looking-glass在这里是一个非常好的方案,对比parsec通过网络传流,looking-glass直接通过内存,显然速度更快更加高效,但是现阶段的looking-glass似乎连帧内压缩都没有实现,对带宽的占用可能会比较夸张,对内存敏感的应用可能会有性能差别。

而HDMI诱骗器实在是太不优雅了,我在使用Parsec的时候发现,它提供了“Virtual Display Driver”的驱动和如果没有显示器就fallback到Virtual Display的功能,深入研究后发现,这玩意是通过微软的Indirect Display Driver (IDD) model实现的,这是一个用于创建桥接于GPU的间接的显示器的驱动模型,可以用来实现虚拟显示器类似的功能。

微软还提供了IndirectDisplay的样例代码,我通过对其进行修改,并添加了我自己显示器的EDID和我需要的扫描模式,最终实现了一个可以虚拟出和自己笔记本显示器参数一样的显示器的IndirectDisplay驱动。

我习惯在Linux平台使用Libvirt管理QEMU虚拟机,并且Libvirt还有Virt-Manager这个很好用的GUI前端。

至此,通过IndirectDisplay+Looking-Glass串流,我实现了效果非常优秀的GPU虚拟化!

(好吧其实两三年前我折腾HyperV GPU虚拟化的时候就知道IDD Display Driver并且成功用上了,当时似乎也没太多人知道这玩意,NVIDIA的专业卡驱动其实也提供了类似的功能(作为GPU虚拟化能力的周边配套能力),叫FakeEDID,如果你使用的是专业卡也可以直接用)

开始操作吧!

硬件条件

首先你得有俩GPU,不管是不是上述Hybrid架构其ss实都可以参考本文内容设置显卡直通,不过如果不是笔记本,如前文所说,你也许没必要用Looking-Gla

IOMMU

从本质上讲,IOMMU 是一种可以让你更安全可靠地对虚拟设备执行 PCI 直通的技术。
直通基于 PCI 设备组,你如果要直通一个 PCI 设备,你必须将整组设备都直通进去。所以一个 IOMMU组 是可以传递给虚拟机的最小物理设备集。

如何启用

你可能需要从BIOS中启用IOMMU,或者它默认就是启用的,然后通过向内核传递intel_iommu=onamd_iommu=on来启用它。

如果 intel_iommu=onamd_iommu=on 设置可以正常工作,您可以尝试把它们替换为 intel_iommu=ptamd_iommu=pt。pt 选项只为使用透传功能的设备启用 IOMMU,并可以提供更好的功能。但不是所有硬件都支持这个选项。如果 pt 选项在您的主机上无法正常工作,请转换回使用前面的选项。

iommu=pt具体的区别可参考这里
更多关于IOMMU开启的内容可参考这里

具体到NixOS上

你可以通过boot.kernelParams来添加内核参数,比如对我来说是:

boot.kernelParams = [ "amd_iommu=pt" ];

查找显卡所在的IOMMU组和PCI ID

这是一段用于打印所有PCI设备是哪个IOMMU组的脚本

#!/usr/bin/env bash
shopt -s nullglob
for g in /sys/kernel/iommu_groups/*; do
    echo "IOMMU Group ${g##*/}:"
    for d in $g/devices/*; do
        echo -e "\t$(lspci -nns ${d##*/})"
    done;
done;

你应该在脚本输出的内容中看到类型为VGA compatible controller的设备,比如对我来说

IOMMU Group 8:
        01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106M [GeForce RTX 3060 Mobile / Max-Q] [10de:2520] (rev a1)
        01:00.1 Audio device [0403]: NVIDIA Corporation GA106 High Definition Audio Controller [10de:228e] (rev a1)

这分别是我的显卡和其音频控制器,它们在一个组里。

你需要它们的找到它们的PCI ID,它大概长这样:<vendor>:<product>,其中vendor和product都是四位长,在这里也就是10de:252010de:228e,你也可以通过lspci找到它:

$ lspci -nn | grep -i nvidia
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106M [GeForce RTX 3060 Mobile / Max-Q] [10de:2520] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation GA106 High Definition Audio Controller [10de:228e] (rev a1)

VFIO

Virtual Function I/O (VFIO) 是一种现代化的设备直通方案,它充分利用了VT-d/AMD-Vi技术提供的DMA Remapping和Interrupt Remapping特性, 在保证直通设备的DMA安全性同时可以达到接近物理设备的I/O的性能。 用户态进程可以直接使用VFIO驱动直接访问硬件,并且由于整个过程是在IOMMU的保护下进行因此十分安全, 而且非特权用户也是可以直接使用。 换句话说,VFIO是一套完整的用户态驱动(userspace driver)方案,因为它可以安全地把设备I/O、中断、DMA等能力呈现给用户空间。

这一步的目的是将独显放到VFIO中,将它“隔离”起来,以便虚拟机后续可以通过VFIO驱动去使用它,这种隔离是内核层面的,可以保证后续虚拟机的访问是安全的。

这里有一些关于VFIO更详细的资料

在NixOS上配置VFIO

你可以通过以下配置来使用VFIO隔离的你的GPU,它会创建一个名为GPUPaththrough的specialisation,通过选择这个specialisation启动来隔离GPU,以下是我的配置,请根据注释进行修改

let
  # 替换成你的设备的PCI ID
  # IOMMU Group 8:
  # 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106M [GeForce RTX 3060 Mobile / Max-Q] [10de:2520] (rev a1)
  # 01:00.1 Audio device [0403]: NVIDIA Corporation GA106 High Definition Audio Controller [10de:228e] (rev a1)
  gpuIDs = [
    "10de:2520" # Graphics
    "10de:228e" # Audio
  ];
in 
{ lib, ... }:
{
  specialisation."GPUPaththrough".configuration = {
    system.nixos.tags = [ "Nvidia-GPU-vfio" "NoXpad" ];

    # 把vfio的内核模块放在nvidia的内核模块之前的是有意的,因为它让vfio在nvidia之前声明要使用这个 GPU
    # The vfio modules before the nvidia modules is very intentional because it lets vfio claim my GPU before nvidia does.
    boot.initrd.kernelModules = [
      # vifo
      "vfio_pci"
      "vfio"
      "vfio_iommu_type1"
      # 在Linux 6.2中已经集成了vfio_virqfd,如果你使用>=6.2的内核,你不再需要以指定模块的方式启用它,否则请取消这里的注释
      # Built into kernel at linux 6.2
      # "vfio_virqfd"
      # 如果你和我一样没在initrd中就加载nvidia的内核模块,你不需要指定这些模块在vfio后加载,否则请取消这里的注释
      # If you load nvidia driver in initrd, you need specific vfio load before nvidia driver
      # "nvidia"
      # "nvidia_modeset"
      # "nvidia_uvm"
      # "nvidia_drm"
    ];
    boot.kernelParams = [ 
      ("vfio-pci.ids=" + lib.concatStringsSep "," gpuIDs) # vfio-pci
    ];
  };
}

配置虚拟机

配置虚拟化(Libvirt)

你可以根据nixos.wiki的Libvirt词条中的方法配置Libvirt:

virtualisation.libvirtd.enable = true;

你可能还需要启用OpenGL来使用Spice,不过它大概率已经被启用了

hardware.opengl.enable = true;

我还推荐你启用Spice的USB重定向,这允许你将宿主机的USB设备直接直通到虚拟机内:

virtualisation.spiceUSBRedirection.enable = true;

安装Windows

这时我们就可以开始安装Windows了,先通过Virt-Manager创建虚拟机,创建到最后确认时勾选Customize configuration before install以对安装进行配置。

如果你期望安装Windows11,你还需要安全启动TPM2.0以满足最低配置要求:

TPM2.0

对于TPM2.0,你可以选择模拟,如果你的设备支持TPM2.0,那也可以选择直通,如果你想要模拟,QEMU的TPM模拟基于swtpm,你需要在安装虚拟机前启用它:

virtualisation.libvirtd.qemu.swtpm.enable = true;

然后在安装虚拟机的配置界面选择TPM,如果需要模拟则设置为Emulated,如果需要直通则设置为PassthroughDevice Path设定为你TPM模块在/dev中的路径(一般是/dev/tpm0

安全启动

启用支持安全启动的UEFI固件(OVMF)

安全启动则比较麻烦,首先QEMU的UEFI支持基于OVMF(Open Virtual Machine Firmware),但是NixOS中Libvirt默认使用的OVMF包(基于EDK2)没有开启TPM和安全启动的支持,这时有两种办法:

使用QEMU提供的预构建的OVMF

一是使用这篇Reddit帖子的思路,使用qemu的share目录下预构建的ovmf-edk2,在NixOS配置中添加:

environment.etc = {
  "ovmf/edk2-x86_64-secure-code.fd" = {
    source = config.virtualisation.libvirtd.qemu.package + "/share/qemu/edk2-x86_64-secure-code.fd";
  };

  "ovmf/edk2-i386-vars.fd" = {
    source = config.virtualisation.libvirtd.qemu.package + "/share/qemu/edk2-i386-vars.fd";
  };
};

在虚拟机配置的<OS>子项中的内容修改成下面这样:

<loader readonly="yes" secure="yes" type="pflash">/etc/ovmf/edk2-x86_64-secure-code.fd</loader>
<nvram template="/etc/ovmf/edk2-i386-vars.fd"/>

其中secure="yes"代表启用安全启动,而nvram是类似于用于你的BIOS配置的存储,默认没有进行配置,Libvirt在你选择开始安装后会自动给你添加一个,这里通过template选项设置模版,这样自动给你创建时就会以这个模版为基础进行创建,而不是空的。

值得注意的是,原贴中似乎那个人不知道Libvirt的nvram选项有template功能,它将edk2的默认配置的那个nvram文件设置为了可写的,然后在尝试直接使用它作为nvram,这种做法显然不是很好,而且Nix默认创建的是符号链接,这么做会强制Nix对文件进行复制操作,但是Nix在这方面经常抽风(

设置Libvirt使用的OVMF为支持安全启动的OVMF

通过这种办法指定OVMF可以顺利运行,但是一个显而易见的更好方法是,我们可以替换Libvirt使用的OVMF包为支持安全启动的版本。

事实上,Nix中OVMF有四个构建选项,允许我们定义构建的OVMF固件支持的功能:

{ stdenv, nixosTests, lib, edk2, util-linux, nasm, acpica-tools, llvmPackages
, csmSupport ? false, seabios ? null
, secureBoot ? false
, httpSupport ? false
, tpmSupport ? false
}:

而软件包声明中,提供了OVMFFull这个包:

OVMF = callPackage ../applications/virtualization/OVMF { };
OVMFFull = callPackage ../applications/virtualization/OVMF {
  secureBoot = true;
  csmSupport = true;
  httpSupport = true;
  tpmSupport = true;
};

libvirtdNix Module中,提供了配置OVMF包的选项:

packages = mkOption {
  type = types.listOf types.package;
  default = [ pkgs.OVMF.fd ];
  defaultText = literalExpression "[ pkgs.OVMF.fd ]";
  example = literalExpression "[ pkgs.OVMFFull.fd pkgs.pkgsCross.aarch64-multiplatform.OVMF.fd ]";
  description = lib.mdDoc ''
    List of OVMF packages to use. Each listed package must contain files names FV/OVMF_CODE.fd and FV/OVMF_VARS.fd or FV/AAVMF_CODE.fd and FV/AAVMF_VARS.fd
  '';
};

那么很显而易见的,我们可以通过这样的配置来替换Libvirt所使用的OVMF的包:

virtualisation.libvirtd.qemu.ovmf.packages = [ (pkgs.OVMFFull.fd ];

这么做以后,创建虚拟机时默认使用的OVMF_CODE.fd就已经是OVMFFull的了,然后再和之前一样配置虚拟机启用安全启动并指定nvram

<loader readonly="yes" secure="yes" type="pflash">/run/libvirt/nix-ovmf/OVMF_CODE.fd</loader>
<nvram template="/run/libvirt/nix-ovmf/OVMF_VARS.fd"/>

这样应该就可以工作了!

但是并不是那么显而易见的是,这么做完,启动,Boom!什么画面都没有(

我疑惑了个半死也没想通为什么,通过测试发现,在使用OVMFFull作为OVMF固件的情况下,如果尝试直通PCI设备,OVMF就会启动失败(黑屏),否则可以正常启动。

但是如果使用默认的OVMF作为OVMF固件,则无论是否直通PCI设备,都能正常启动。

那问题就必然是出现在了OVMFFull上,而OVMFFull和OVMF所有的不同就是启用了安全启动、HTTP支持、TPM支持、和CSM兼容。

乍一看没啥问题…吗?CSM兼容???在我记忆中PCI直通、IOMMU、高级的虚拟化都是需要UEFI才能够实现的,CSM下应该是没法提供这些能力的。

禁用后重新尝试发现,还真是被这玩意弄得。。。很无语。。

所以最终的方法是,使用覆盖csmSupport为关闭的OVMFFull包:

virtualisation.libvirtd.qemu.ovmf.packages = [ (pkgs.OVMFFull.override { csmSupport = false; }).fd ];

值得注意的是,你对virtualisation.libvirtd.qemu.ovmf.packages的修改不会立刻生效,因为Libvirt使用的OVMF固件只有在libvirtd守护进程启动和关闭时才会进行同步,你可以重启libvirtd守护进程使其生效:

systemctl restart libvirtd

启用SMM

如果此时你直接点击开始安全,会提示你没有开启SMM,安全启动需要SMM功能才能工作,你需要在<features>子项的末尾添加<smm state="on"/>以启用它,像这样:

<features>
  ......
  <smm state="on"/>
</features>

正确设置拓扑

在虚拟机配置的CPU选项卡中,有Topology这个子项,下面可以手动设置拓扑,如果你是AMD处理器,我建议你设置为一个Socket一个Thread,Cores设置为你想索取的核心数量,不要设置虚拟机使用超线程(Thread>1)

VirtIO

这一步是可选的,不过如果你希望有优秀的IO性能,那我建议你使用,具体可以参考这篇文章

PCI直通

终于到了本文的核心了,PCI直通:

Add Hardware -> PCI Host Device -> 你需要直通的PCI设备

请确保你选择的设备已经通过VFIO等方法隔离,并且直通时请务必将该设备所在的IOMMU组内的所有设备全部直通进虚拟机,否则严重时可能损坏设备!

在本文中,我们需要直通显卡和其音频控制器,在上文查找所在的IOMMU组时可以看到PCI设备的位置ID,在本案例中就是01:00.001:00.1两个设备

一些其他杂七杂八的设置

建议将Spice的鼠标模式设置为Server,不然在切换显示器时经常会出现鼠标失灵的问题:

<mouse mode="server"/>

具体可以参考这里

安装Windows

一切就绪后,开始安装,如果能出现OVMF的加载界面(启动Logo),则说明直通已经成功了!

然后正常安装Windows即可,如果你用了VirtIO记得根据上文的参考上文安装驱动!

显卡驱动

安装完成开机后,安装Nividia的驱动(可以通过GFE,比较方便),然后设备管理器应该就可以看到显示适配器正常工作了,这时如果外接显示器接入到连接独显的接口,应该就能正确使用了!

不过无论你有没有外接显示器,通过Spice协议,你还是可以在Virt-Manager中直接使用QEMU提供的VGA视频(在没有安装QXL驱动的情况下,提供的其实本质还是VGA视频/接口),这会使用Microsoft基本显示驱动,它基于CPU渲染。

一些提示:如果你看到了你的设备没有正常工作,并且报错误代码43,Nvidia驱动在21年之后就已经不会检测虚拟机了,一个可能的原因是内核隔离导致的,之前它在我的虚拟机中导致了显卡无法正确加载驱动,你可以尝试关闭它,内核隔离依托于虚拟化来实现,在这样的虚拟机上嵌套虚拟化可能没有正常运行导致了这样的问题(只是猜测)

Spice Guest Tools

这是一个工具,它会提供虚拟机和宿主机的通信能力(如剪贴板共享),并安装QXL驱动以通过QXL作为显示接口来支持更多的分辨率。
你可以在这里下载它:https://www.spice-space.org/download.html

完成

至此,你已经获得了一个可用的Windows虚拟机,它能正确驱动由PCI直通的提供的显卡并可以使用Spice提供的显示接口,Spice显示驱动在没有外接显示器或没开启Indirect Display驱动时,非常有用,至少你可以通过它对虚拟机做基本的操作。

我测试发现虽然Looking-glass文档的安装教程让你将QXL接口改为VGA或禁用,但是似乎使用QXL也不会有任何问题。

Indirect Display

介绍

在我们使用通过串流访问虚拟机的显示器之前,我们先得有个显示器(

之前也说过,一个不完美但是很简单的方法是,可以通过把诱骗器连接到独显提供的接口实现,但是缺点也是显而易见的

Spice提供的QXL/VGA驱动是使用的Microsoft基本显示器驱动,可以理解为微软为没有显卡或没有显卡驱动时提供的一个基本渲染器。

一般来说,在不真的给显卡的接口连接显示器时,我们没有办法告诉显卡驱动它有连接一个显示器…吗?

之前用Parsec的时候,看到它提供了虚拟显示器驱动,在对Hyper-V GPU虚拟化的虚拟机进行串流时工作的非常好,在笔记本合盖状态下进行远程串流这种没显示器的场景也非常有用。我就很好奇怎么实现的。

于是我搜来搜去,发现微软有一个叫做Indirect display driver (IDD) 的显示驱动模型(以下简称IDD驱动)

这套模型实现了往显示适配器驱动(显卡)桥接虚拟的显示器,而且它还提供了案例

我对它进行了一些修改,你也可以使用我修改后的版本:https://github.com/LostAttractor/IndirectDisplay

我以Nvidia驱动会提供的扫描模式作为模版,添加了更多的扫描模式,然后默认将会创建无EDID的显示器。

你可以通过删除EDID_LESS宏来创建有EDID的显示器,并且你可以将你的显示器提供的EDID写入到s_SampleMonitors数组,这样你就可以获得一个和你显示器EDID一样的虚拟显示器。你可以像下面这样将你的EDID写入到文件中:

# 查看现在系统获取到的所有显示器的EDID文件列表
$ ls -1 /sys/class/drm/*/edid
/sys/class/drm/card0-eDP-1/edid
/sys/class/drm/card0-HDMI-A-1/edid
# 将eDP-1的EDID写入到文件中
$ cat /sys/class/drm/card0-eDP-1/edid > edid.bin

然后你可以通过HEX编辑器来查看EDID,转成C数组来使用它。这个仓库中还有一个名为my-edid分支,它基于master分支并添加了我显示器的EDID,你可以作为参考。

编译

要编译这个驱动,你需要使用Visual Studio并安装使用C++的桌面开发 (MSVC)通用Windwos平台开发 (Windows SDK)Windows Driver Kit (WDK),其中Windows Driver Kit不能直接在Visual Studio Installer中安装,你需要通过这里的流程安装:https://learn.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk

你可能还需要适用于你编译器版本的Spectre缓解库(一般直接选择x86平台的最新就行)来进行编译。

然后你应该就可以直接编译这个项目(解决方案)了。

安装

这个项目有两个部分组成,一个是IDD的驱动部分,一个是用来调用这个驱动并创建显示器的程序,在安装驱动后打开这个程序就可以通过IDD驱动创建显示器了

你编译后会得到一个WDK Test的自签证书,你的驱动会被这个证书签名,所以你需要将它作为根证书信任:双击它选择安装证书并把它到本地计算机的受信任的根证书颁发机构目录

这时我们就可以安装驱动了,打开构建结果中的IddSampleDriver目录,右键里面的IddSampleDriver.inf并选择安装,这样驱动就安装好了。

然后这时,通过以管理员身份运行IddSampleApp.exe,就可以通过IDD驱动创建显示器了。

你可以通过打开 右键->属性->兼容性->以管理员身份允许此程序 来默认申请以管理员权限运行。

你还可以通过在计划任务程序中通过:

创建基本任务->给任务取个名->触发器:计算机启动时->操作:启动程序->启动程序:选择IddSampleApp->完成
然后找到这个任务->安全选项->选择不管用户是否登录都要运行->打开不存储密码,该任务只有本地计算机的访问权限->打开使用最高权限运行

来创建一个开机自动运行IddSampleApp的计划任务。

使用

你可以使用设置或者通过Win+P来切换显示器模式为仅第二显示器(也就是仅IDD显示器)

需要注意的是,如果你设置成仅第二显示器并没有设置串流,这时你将无法通过Virt-Manager中通过Spice里看到画面,因为它没有输出视频流给Spice接口(默认显示器),但是你的键盘仍然可以使用,你仍然可以通过Win+P改回来!

通过设置自动启动IddSampleApp并把显示器模式设置为仅第二屏幕,再加上Looking-glass或Parsec串流,可以实现开机后直接通过串流控制虚拟机!

串流

Parsec

Parsec是一个通过网络进行远程串流/远程控制的工具,于市面上其他诸多工具不同的是,该工具设计之初就非常注重串流的流畅度,并且我认为是目前市面上唯一一个真正把流畅度做到了能用来打游戏程度的工具。

于其他远程桌面工具不同的是,它基于P2P传输,对硬件编解码拥有非常完善的支持(x264、NVENC/DEC、AMF等等),这为高帧率低延迟的串流打下了坚实的基础。

在我使用NVENC作为编码方式进行串流时,能做到编码延迟<=3ms、解码延迟<=4ms、网络延迟在10~15ms浮动的120帧2K串流,几乎除了FPS游戏都能愉快游玩了。

在使用IDD显示器的情况下,可以直接像正常串流普通电脑一样对虚拟机进行串流。

Looking-Glass

但是吧,明明虚拟机是开在宿主机上的,那有没有比网络更高效的传输方法呢?

Looking-Glass就是这样一个解决方案,它使用内存作为信息交换介质进行串流,并且作为一个KVMFR的兼容实现,就算虚拟机内的Looking-Glass客户端没有开启,也能够从Spice显示接口中获取到显示画面。

值得注意的是,Spice显示接口使用的Microsoft基本显示驱动没有实现将显示缓冲区内的画面直接向内存拷贝的接口,因此在这上面它无法工作(指通过内存串流,直接使用Spice协议获得画面是可以的),但是在我们直通给虚拟机的显卡上是可以工作的。

以及它只是一个KVMFR实现,键鼠等其他外围支持仍由Spice提供。

Looking-Glass仍在积极开发中,本文编写时版本为B6,你也可以直接参考其文档:
https://looking-glass.io/docs/B6/install/

Tips

文档中使用systemd-tmpfiles来提前创建缓冲区文件的方法在NixOS中可以这样实现:

systemd.tmpfiles.rules = [ 
  # Type Path               Mode UID     GID Age Argument
  "f /dev/shm/looking-glass 0660 ${user} kvm -"
];

因为需要保证使用Looking-Glass的用户可以读取,所以我选择在home-manager里定义,并从flake里一层一层把username传进去。

还有就是,上面也说过,我测试发现虽然Looking-glass的文档让你将QXL接口改为VGA,但是似乎使用QXL也不会有任何问题。

未完待须!

本文仓库

我自己使用的虚拟机Libvirt配置和Looking-Glass配置以及一些脚本可以在这个仓库里找到:
https://github.com/LostAttractor/gpu-passthrough

我的Flake(NixOS配置)可以在这里找到:
https://github.com/LostAttractor/flake

效果

IMG_3728
IMG_3729

参考

生命本没有意义,但如果任何事情因此而改变了,那便有了意义