Changchun Master Li

[Windows10 in Gentoo Linux] intel 12代 uhd770 核显直通

2025-11-02

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

第4步,先编译tools,再编译efi

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的使用方式

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章