2018年硕士毕业的时候,我根本想不到2025年除了少数AI产业相关的程序员还在努力突破着机器的上限,挑战着未来的图灵奖,而多数计算机相关行业会进入寒冬。
失业之后,失去了office,为了不无司可归我一直努力把家打造成soho,毕竟做程序员,有水电网就可以工作,所以我在折腾家里的主机,理想情况下,主机可以作为一个headless computer来使用,igd passthrough则可以帮我们最大可能灵活利用核显,解耦host图形界面与host图形软件栈。灵活、稳定、性能,是开发者的“蒙代尔不可能三角”,只能尽量做好三者的平衡。
https://blog.74ls74.org/2025/04/20/20250420_Windows10_in_Gentoo_Linux/
在上一篇文章里,有评论质疑“手搓PVE”“直接装wsl不就得了”。很多事情不能一概而论,完美并非无以复加,而是无可删减,获得了方便的副产品则是失去了灵活性,不管是debian还是windows10,pve还是wsl,都是打包好的产品,可玩性还是略有不足,pve中文社区,到处流传着以讹传讹的配置,信息含量越来越低。
我觉得 Linux 的 GUI 就是垃圾,gnome49发布之后, 也许有一天Wayland真的可以一统江湖,到那时也许会改变我对于 Linux 图形界面的看法。言归正传,我主机cpu型号为12700k,核显为uhd770,玩游戏很羸弱,但平时上网看视频写代码很够用。
本次带来的是intel 12代 核显 uhd770 在Gentoo Linux直通,也就是pve论坛里讨论热烈的 igd passthrough。这样一来,host os (gentoo) 可以不被任何图形界面污染,便于维护的同时稳定可靠,guest os 可以通过高性能的 igd passthrough来体验图形界面的新特性(比如win10或Ubuntu25.10,win7就别想了uhd770不支持legacy vbios,也没有uhd770驱动程序)。
igd passthrough 原理 核显直通本质上是一件蛮复杂的事情。核显不像独显,不“独立”,不支持热插拔,没有板载rom,共享主存,天生耦合。
bios 在 UEFI 出现之前,bios是被固化在主板rom上的程序,用来帮助计算机初始化系统,老黄历了,可以无视
legacy vbios bios在计算机启动早期显示图形化界面,调试信息的,需要初始化显卡,显卡板载存储上的程序就是vbios,老黄历了,可以无视
oprom 对于独立显卡 dgpu,PCIe 的配置空间的 rombar 会映射到显卡板载 SPI FLASH,现代 UEFI 系统启动之后在 DEX 阶段,主板 UEFI 固件会从 rombar 加载 GPU 的 oprom,对于显卡来说,oprom需要包含 GOP (Graphics Output Protocol) driver。
但 igpu 不同于 dgpu,igpu 核显没有 SPI FLASH,现代 UEFI 系统的核显 gop 是随主板 UEFI firmware 一起发布的,我们可以用 UEFITool 或 UEFI BIOS Updater 从主板厂商发布的bios文件里提取 gop。很重要
SR-IOV 单根虚拟化,intel 也叫 gvt-g,一个 gpu 拆成多个 vgpu 来用,和 igd passthrough (gpu整个被vm独占)是两个东西,不要看花眼了
gvt-d 基于 IOMMU,通过 vfio,支持加载 gop,将整个物理 GPU 独占分配给某个虚拟机,很重要
一、host UEFI 设置 打开IOMMU、vt-d
不要打开 CSM(legacy bios)
二、Host Linux kernel 配置 gvt-d 除了打开 iommu、vfio、kvm等特性(参考上一篇),还要打开 gvt-d 支持
1 2 3 4 5 6 7 Device Drivers ---> VFIO Non-Privileged userspace driver framework ---> VFIO support for PCI devices ---> <*> Generic VFIO support for any PCI device [*] Generic VFIO PCI support for VGA devices [*] Generic VFIO PCI extensions for Intel graphics (GVT-d) < > VFIO support for VIRTIO NET PCI VF devices
i915 核显在系统i915驱动初始化时会分配cpu内存作为显存,偶尔会导致vfio模块有问题(我没太研究),因此需要阻止开机时 host 加载 i915 驱动,因此需要更新bootloader的内核参数。
把 i915 驱动加到 blacklist中,避免在 kernel 启动时初始化核显
参考 https://3os.org/infrastructure/proxmox/gpu-passthrough/igpu-passthrough-to-vm/#proxmox-configuration-for-igpu-full-passthrough
图方便的话可以直接去掉host的intel i915显卡驱动,让host为uhd770加载vfio
1 2 3 4 Device Drivers ---> Graphics support ---> Direct Rendering Manager (XFree86 4.1.0 and higher DRI support) ---> < > Intel 8xx/9xx/G3x/G4x/HD Graphics
kernel 参数就可以写的简单一些了
1 CONFIG_CMDLINE="root=/dev/nvme0n1p2 intel_iommu=on iommu=pt single vfio_iommu_type1.allow_unsafe_interrupts=1"
三、提取gop driver,合成核显oprom 这一步略有些复杂,参考这个项目
https://github.com/tomitamoeko/VfioIgdPkg
此项目汇总了intel 的patch https://eci.intel.com/docs/3.3/components/kvm-hypervisor.html?highlight=igd
第1步,下载edk2 1 2 3 4 $ git clone -b "stable/202508" https://github.com/tianocore/edk2.git edk2/ $ cd edk2 $ git submodule update --init $ source edksetup.sh
第2步,下载VfioIgdPkg 1 2 3 $ git clone https://github.com/tomitamoeko/VfioIgdPkg.git $ cd VfioIgdPkg $ . add-to-ovmfpkg.sh
第3步,配置Conf/target.txt 根据edk2文档 https://github.com/tianocore/tianocore.github.io/wiki/Common-instructions 修改一下conf
1 2 3 ACTIVE_PLATFORM = OvmfPkg/OvmfPkgX64.dsc TARGET_ARCH = X64 TOOL_CHAIN_TAG = GCC5
1 2 $ make -C BaseTools $ build
成功编译之后,找到这两个东西
1 2 Build/OvmfX64/DEBUG_GCC5/X64/PlatformGopPolicy.efi Build/OvmfX64/DEBUG_GCC5/X64/IgdAssignmentDxe.efi
我们还需要主板固件集成gop driver,根据操作使用UBU提取就可以
第5步,efirom工具 把这些efi文件合成一个rom
1 ./BaseTools/BinWrappers/PosixLike/EfiRom -e IntelGopDriver.efi IgdAssignmentDxe.efi PlatformGopPolicy.efi -f 0x8086 -i 0xffff -o 1081.rom
这样就得到了oprom,把oprom作为参数传入romfile,就可以正常驱动核显了
四、qemu虚拟机 不得不说在 qemu10 发布前,很多 pve 对的 qemu 的 patch 使 qemu 在直通方面非常成熟。
qemu10 在 changlog 中官方声明支持了 12代核显直通,现在 qemu10 会对 intel igd passthrough 的支持已经非常好了。从 igd-assign 文档可以看到,跟pve-qemu的参数legacy-igd类似,qemu 增加的新参数 x-igd-legacy-mode,用来开启legacy vbios模式。
我qemu版本是10.1.1 直接编译
1 2 3 $ git clone git@github.com:qemu/qemu.git $ ./configure --target-list=x86_64-softmmu --with-suffix="kvm" --libexecdir=/usr/lib/kvm --enable-libusb --without-default-features --enable-kvm --enable-vnc --enable-pixman --enable-slirp --enable-tools $ make
q35 和 pc/i440fx q35和i440fx配置上区别不大,直接贴上来吧
i440fx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 qemu-system-x86_64 \ -machine pc,accel=kvm \ -cpu host,host-phys-bits-limit=39 \ -smp 8 -m 16G \ \ -drive if=pflash,format=raw,readonly=on,file=pve-edk2-firmware/OVMF_CODE_4M.fd \ -drive if=pflash,format=raw,file=pve-edk2-firmware/OVMF_VARS_4M.fd \ \ -drive file=win10_igd.qcow2 \ \ -device vfio-pci,host=0000:00:02.0,romfile=1081.rom,multifunction=on,x-igd-lpc=on,x-vga=on \ -device vfio-pci,host=0000:00:1f.3 \ -device vfio-pci,host=0000:00:14.3 \ -device vfio-pci,host=0000:00:14.2 \ -device vfio-pci,host=0000:00:14.0 \ \ -nodefaults \ -vga none \ -netdev bridge,id=n0,br=br0,helper=/media/data/qemu/build/qemu-bridge-helper \ -device virtio-net-pci,netdev=n0,mq=on,vectors=8 \ -rtc base=localtime \ -display none
q35
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 qemu-system-x86_64 \ -device vfio-pci,host=0000:00:02.0,id=hostpci0,romfile=1081.rom \ -set device.hostpci0.bus=pcie.0 \ -set device.hostpci0.addr=0x02.0 \ -set device.hostpci0.x-igd-legacy-mode=off \ -set device.hostpci0.x-vga=on \ \ -device vfio-pci,host=0000:00:1f.3 \ -device vfio-pci,host=0000:00:14.3 \ -device vfio-pci,host=0000:00:14.2 \ -device vfio-pci,host=0000:00:14.0 \ \ -smp 8 -m 16G \ -machine q35,kernel-irqchip=on \ -cpu host,host-phys-bits-limit=39 \ -device intel-iommu,intremap=off,aw-bits=39,caching-mode=on \ -drive file=win10_igd.qcow2 \ -enable-kvm \ -drive if=pflash,format=raw,readonly=on,file=pve-edk2-firmware/OVMF_CODE_4M.fd \ -drive if=pflash,format=raw,file=pve-edk2-firmware/OVMF_VARS_4M.fd \ -rtc base=localtime \ -vga none \ -device e1000,netdev=n0 -netdev user,id=n0 \ -display none
x-igd-legacy-mode 选 off 就行了,不需要打开它,gop不需要这个古董参数,不需要vbt!不需要设置opregion!你需要的是打开x-vga,这样可以拥有物理端口的输出。
启动的时候你可以看到 qemu 提示 OpRegion detected 就意味着gop自动配置opregion成功了
1 qemu-system-x86_64: -device vfio-pci,host=0000:00:02.0,id=hostpci0,romfile=pc-12-13-14-q10.rom: info: OpRegion detected on Intel display 4680.
q35和i440fx唯一的区别可能是打开x-igd-lpc的i440fx可以看到uefi系统启动界面,q35会自动初始化这个lpc总线,但q35无法显示uefi启动界面,这可能是 q35 lpc 的 bug
报错stolen reserved area outside stolen memory error 描述 使用 linux 6.13 作为 guest os,会报 reserved area的错误
1 [ 7.217518] i915 0000:00:02.0: [drm] *ERROR* Stolen reserved area [mem 0x80600000-0x807fffff] outside stolen memory [mem 0xbc500000-0xbe4fffff]
可以定位到 source/drivers/gpu/drm/i915/gem/i915_gem_stolen.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 /* * Initialize i915->dsm.reserved to contain the reserved space within the Data * Stolen Memory. This is a range on the top of DSM that is reserved, not to * be used by driver, so must be excluded from the region passed to the * allocator later. In the spec this is also called as WOPCM. * * Our expectation is that the reserved space is at the top of the stolen * region, as it has been the case for every platform, and *never* at the * bottom, so the calculation here can be simplified. */ static int init_reserved_stolen(struct drm_i915_private *i915) { struct intel_uncore *uncore = &i915->uncore; resource_size_t reserved_base, stolen_top; resource_size_t reserved_size; int ret = 0; stolen_top = i915->dsm.stolen.end + 1; reserved_base = stolen_top; reserved_size = 0; if (GRAPHICS_VER(i915) >= 11) { icl_get_stolen_reserved(i915, uncore, &reserved_base, &reserved_size); } else if ... i915->dsm.reserved = DEFINE_RES_MEM(reserved_base, reserved_size); if (!resource_contains(&i915->dsm.stolen, &i915->dsm.reserved)) { drm_err(&i915->drm, "Stolen reserved area %pR outside stolen memory %pR\n", &i915->dsm.reserved, &i915->dsm.stolen); ret = -EINVAL; goto bail_out; } return 0; bail_out: i915->dsm.reserved = DEFINE_RES_MEM(reserved_base, 0); return ret; }
reserved area(2M)是data stolen memory(32M) 中需要保留的一段空间,不能其他代码覆盖使用
reserved area不匹配会被默认设置为0,只要没有别的代码写这个地址,后果倒不是很严重
继续调查一下 icl_get_stolen_reserved 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static void icl_get_stolen_reserved(struct drm_i915_private *i915, struct intel_uncore *uncore, resource_size_t *base, resource_size_t *size) { u64 reg_val = intel_uncore_read64(uncore, GEN6_STOLEN_RESERVED); drm_dbg(&i915->drm, "GEN6_STOLEN_RESERVED = 0x%016llx\n", reg_val); ... switch (reg_val & GEN8_STOLEN_RESERVED_SIZE_MASK) { case GEN8_STOLEN_RESERVED_1M: *size = 1024 * 1024; break; case GEN8_STOLEN_RESERVED_2M: *size = 2 * 1024 * 1024; break; case GEN8_STOLEN_RESERVED_4M: *size = 4 * 1024 * 1024; break; case GEN8_STOLEN_RESERVED_8M: *size = 8 * 1024 * 1024; break; default: *size = 8 * 1024 * 1024; MISSING_CASE(reg_val & GEN8_STOLEN_RESERVED_SIZE_MASK); } if (HAS_LMEMBAR_SMEM_STOLEN(i915)) /* the base is initialized to stolen top so subtract size to get base */ *base -= *size; else *base = reg_val & GEN11_STOLEN_RESERVED_ADDR_MASK; }
uhd770 共享cpu内存,是没有 LMEMBAR 的
因此 *base 的值来自于 reg_val,来自于0x1082c0这个地址
1 #define GEN6_STOLEN_RESERVED _MMIO(0x1082C0)
怀疑 vfio没有mock好这个pcie配置空间地址的数据,因此,这里 0x80600000 是 host 真实的 reserved area 起始地址,而 0xbc500000 是 guest 系统的地址。
挂上i915驱动看一下host内存布局可以印证这点
1 2 3 4 5 6 7 $ cat /proc/iomem ... 77fff000-77ffffff : System RAM 78000000-7bffffff : Reserved 7c600000-7c7fffff : Reserved 7d000000-807fffff : Reserved 7e800000-807fffff : Graphics Stolen Memory
lspci 可以看到0xf0上的值刚好是0x1082C0地址上的值,都是0x80600087
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # lspci -s 00:02.0 -xxx 00:02.0 VGA compatible controller: Intel Corporation AlderLake-S GT1 (rev 0c) 00: 86 80 80 46 00 04 10 00 0c 00 00 03 10 00 00 00 10: 04 00 00 fd 60 00 00 00 0c 00 00 00 40 00 00 00 20: 01 50 00 00 00 00 00 00 00 00 00 00 43 10 94 86 30: 00 00 00 00 40 00 00 00 00 00 00 00 ff 01 00 00 40: 09 70 0c 01 0c 00 00 00 00 00 00 00 00 00 00 00 50: c1 01 00 00 39 40 00 00 00 00 00 00 00 00 00 00 60: 00 00 01 00 00 00 00 00 00 00 00 00 01 00 00 00 70: 10 ac 92 00 00 80 00 10 00 00 00 00 00 00 00 00 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a0: 00 00 00 00 00 00 00 00 00 00 00 00 05 d0 00 01 b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0: 01 00 80 7e 00 00 00 00 01 00 d9 fe 00 00 00 00 d0: 01 00 22 00 03 00 00 00 00 00 00 00 00 00 00 00 e0: 00 00 00 00 00 00 00 00 00 80 00 00 00 00 00 00 f0: 87 00 60 80 00 00 00 00 00 00 00 00 18 40 63 76
很遗憾在12th gen intel core processors datasheet volume 2 of 2上没有找到相关说明 PRM上也没有公开
https://www.intel.com/content/www/us/en/docs/graphics-for-linux/developer-reference/1-0/tiger-lake.html
但大致可以猜到这是UEFI设置的,gop driver通过这个数来初始化内存布局
workaround 如果要fix的话这里需要专门为核显直通打 patch
首先来到 https://github.com/tomitamoeko/VfioIgdPkg.git
IgdAssignmentDxe/IgdAssignment.c 这个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 index d1214e8..613a7ee 100644 --- a/IgdAssignmentDxe/IgdAssignment.c +++ b/IgdAssignmentDxe/IgdAssignment.c @@ -427,6 +427,17 @@ SetupStolenMemory ( goto FreeStolenMemory; } +/* + EFI_PHYSICAL_ADDRESS SRA = ((Address & 0xFFFFFF00000ULL) | 0x7ULL) + (EFI_PHYSICAL_ADDRESS)(Size - SIZE_1MB); + Status = PciIo->Pci.Write ( + PciIo, + EfiPciIoWidthUint64, + 0xf0, + 1, // Count + &SRA + ); +*/ + DEBUG ((DEBUG_INFO, "%a: %a: stolen memory @ 0x%Lx, size %d MB\n", __FUNCTION__, GetPciName (PciInfo), Address, Size / SIZE_1MB)); return EFI_SUCCESS;
根据guest vm分配的stolen memory address去计算并设置0xf0的值,重新build生成oprom
再修改qemu,设置0x1082C0为mirror quirk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c index 4bfa2e0fcd..0d94356ea5 100644 --- a/hw/vfio/igd.c +++ b/hw/vfio/igd.c @@ -112,6 +112,8 @@ static int igd_gen(VFIOPCIDevice *vdev) #define IGD_GMCH 0x50 /* Graphics Control Register */ #define IGD_BDSM 0x5c /* Base Data of Stolen Memory */ #define IGD_BDSM_GEN11 0xc0 /* Base Data of Stolen Memory of gen 11 and later */ +#define IGD_SRAM_GEN11 0xf0 #define IGD_GMCH_VGA_DISABLE BIT(1) #define IGD_GMCH_GEN6_GMS_SHIFT 3 /* SNB_GMCH in i915 */ @@ -455,11 +457,13 @@ static bool vfio_pci_igd_override_gms(int gen, uint32_t gms, uint32_t *gmch) #define IGD_GGC_MMIO_OFFSET 0x108040 #define IGD_BDSM_MMIO_OFFSET 0x1080C0 +#define IGD_SRAM_MMIO_OFFSET 0x1082C0 void vfio_probe_igd_bar0_quirk(VFIOPCIDevice *vdev, int nr) { - VFIOQuirk *ggc_quirk, *bdsm_quirk; - VFIOConfigMirrorQuirk *ggc_mirror, *bdsm_mirror; + VFIOQuirk *ggc_quirk, *bdsm_quirk, *sram_quirk; + VFIOConfigMirrorQuirk *ggc_mirror, *bdsm_mirror, *sram_mirror; int gen; if (!vfio_pci_is(vdev, PCI_VENDOR_ID_INTEL, PCI_ANY_ID) || @@ -508,6 +512,26 @@ void vfio_probe_igd_bar0_quirk(VFIOPCIDevice *vdev, int nr) 1); QLIST_INSERT_HEAD(&vdev->bars[nr].quirks, bdsm_quirk, next); + sram_quirk = vfio_quirk_alloc(1); + sram_mirror = sram_quirk->data = g_malloc0(sizeof(*sram_mirror)); + sram_mirror->mem = sram_quirk->mem; + sram_mirror->vdev = vdev; + sram_mirror->bar = nr; + sram_mirror->offset = IGD_SRAM_MMIO_OFFSET; + sram_mirror->config_offset = IGD_SRAM_GEN11; + + memory_region_init_io(sram_mirror->mem, OBJECT(vdev), + &vfio_generic_mirror_quirk, sram_mirror, + "vfio-igd-sram-quirk", (gen < 11) ? 4 : 8); + memory_region_add_subregion_overlap(vdev->bars[nr].region.mem, + sram_mirror->offset, sram_mirror->mem, + 1); + + QLIST_INSERT_HEAD(&vdev->bars[nr].quirks, sram_quirk, next); } static bool vfio_pci_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp) @@ -650,6 +674,11 @@ static bool vfio_pci_igd_config_quirk(VFIOPCIDevice *vdev, Error **errp) } } + pci_set_quad(pdev->config + IGD_SRAM_GEN11, 0); + pci_set_quad(pdev->wmask + IGD_SRAM_GEN11, ~0); + pci_set_quad(vdev->emulated_config_bits + IGD_SRAM_GEN11, ~0); /* * Request reserved memory for stolen memory via fw_cfg. VM firmware * must allocate a 1MB aligned reserved memory region below 4GB with
这个报错就没有了
summary 把主机连上两套KVM(keyboard、video、mouse),一套直通核显,另一套直通独显,就可以联机打局域网cs1.6了,这非常像80年代哑终端Dumb Terminal的使用方式