@@ -274,12 +274,21 @@ const qemuGlobalConfTemplate = `# This file is automatically generated by domain
274274 driver = "virtio-gpu-pci"
275275{{- end }}
276276{{- end }}
277+ {{- if .HasIntelIGPU }}
278+ [device "pci.2"]
279+ driver = "pcie-root-port"
280+ port = "12"
281+ chassis = "2"
282+ bus = "pcie.0"
283+ addr = "0x1b"
284+ {{- else }}
277285[device "pci.2"]
278286 driver = "pcie-root-port"
279287 port = "12"
280288 chassis = "2"
281289 bus = "pcie.0"
282290 addr = "0x2"
291+ {{- end }}
283292
284293[device "usb"]
285294 driver = "qemu-xhci"
@@ -434,6 +443,9 @@ const qemuPCIPassthruTemplate = `
434443{{- if .Xopregion }}
435444 x-igd-opregion = "on"
436445{{- end}}
446+ {{- if .Romfile }}
447+ romfile = "{{.Romfile}}"
448+ {{- end}}
437449`
438450
439451const qemuSerialTemplate = `
@@ -492,12 +504,13 @@ const qemuSwtpmTemplate = `
492504 tpmdev = "tpm0"
493505`
494506
495- // Context for qemuGlobalConfTemplate .
507+ // Context for qemuGlobalConfContext .
496508type tQemuGlobalConfContext struct {
497509 Machine string
498510 VirtualizationMode string
499511 BootLoaderSettingsFile string
500512 BootOrder string // Boot order: "usb" (prioritize USB), "nousb" (deprioritize USB), or "" (default)
513+ HasIntelIGPU bool // true when Intel iGPU is in the passthrough list; moves USB root port to slot 0x1b
501514 types.DomainConfig
502515 types.DomainStatus
503516}
@@ -513,6 +526,7 @@ type tQemuPCIPassthruContext struct {
513526 PciShortAddr string
514527 Xvga bool
515528 Xopregion bool
529+ Romfile string // non-empty path to GOP ROM (e.g. /persist/gop/3ea0.rom); omitted if empty
516530 Bus string
517531 Addr string
518532}
@@ -1420,6 +1434,7 @@ func multifunctionDevGroup(pcis []pciDevice) multifunctionDevs {
14201434type pciAssignmentsTemplateFiller struct {
14211435 multifunctionDevices multifunctionDevs
14221436 file io.Writer
1437+ domainUUID string
14231438}
14241439
14251440func (f * pciAssignmentsTemplateFiller ) pciEBridge (pciID int , pciWOFunction string ) error {
@@ -1443,17 +1458,48 @@ func (f *pciAssignmentsTemplateFiller) do(pciAssignments []pciDevice) error {
14431458 Xvga : pa .isVGA (),
14441459 Xopregion : false ,
14451460 }
1446- if vendor , err := pa .vid (); err == nil {
1447- // check for Intel vendor
1448- if vendor == "0x8086" {
1449- if pciPTContext .Xvga {
1450- // we set opregion for Intel vga
1451- // https://github.com/qemu/qemu/blob/stable-5.0/docs/igd-assign.txt#L91-L96
1452- pciPTContext .Xopregion = true
1461+
1462+ isIntelIGPU := false
1463+ if vendor , err := pa .vid (); err == nil && vendor == "0x8086" {
1464+ if pciPTContext .Xvga {
1465+ // Enable OpRegion passthrough for Intel iGPU — writes etc/igd-opregion to fw_cfg.
1466+ // https://github.com/qemu/qemu/blob/stable-5.0/docs/igd-assign.txt#L91-L96
1467+ pciPTContext .Xopregion = true
1468+ isIntelIGPU = true
1469+ logrus .Infof ("iGPU passthrough: detected Intel GPU at host PCI %s for domain %s" ,
1470+ pa .ioBundle .PciLong , f .domainUUID )
1471+ if devid , err := pa .devid (); err == nil {
1472+ pciPTContext .Romfile = gopRomPath (devid )
1473+ if pciPTContext .Romfile == igdRomPath {
1474+ logrus .Infof ("iGPU passthrough: no proprietary GOP ROM in %s for device %s, " +
1475+ "using bundled VfioIgdPkg ROM (OS display works; no pre-OS UEFI framebuffer) for domain %s" ,
1476+ gopRomDir , devid , f .domainUUID )
1477+ } else {
1478+ logrus .Infof ("iGPU passthrough: using proprietary GOP ROM %s for domain %s" ,
1479+ pciPTContext .Romfile , f .domainUUID )
1480+ }
1481+ } else {
1482+ logrus .Warnf ("iGPU passthrough: could not read device ID for %s (domain %s): %v" ,
1483+ pa .ioBundle .PciLong , f .domainUUID , err )
1484+ pciPTContext .Romfile = igdRomPath
14531485 }
14541486 }
14551487 }
14561488
1489+ if isIntelIGPU {
1490+ // Intel iGPU must appear at guest BDF 00:02.0 for VBIOS/GOP ROM initialization.
1491+ // Place directly on the root bus — no pcie-root-port wrapper.
1492+ // The USB root port was moved from 0x2 to 0x1b in qemuGlobalConfTemplate.
1493+ logrus .Infof ("iGPU passthrough: placing device %s at guest BDF 00:02.0 for domain %s" ,
1494+ pa .ioBundle .PciLong , f .domainUUID )
1495+ pciPTContext .Bus = "pcie.0"
1496+ pciPTContext .Addr = "0x2"
1497+ if err := tQemuPCIPassthru .Execute (f .file , pciPTContext ); err != nil {
1498+ return logError ("can't write iGPU passthrough to config file (%v)" , err )
1499+ }
1500+ continue
1501+ }
1502+
14571503 pciLongWoFunc , err := pa .pciLongWOFunction ()
14581504 if err != nil {
14591505 logrus .Warnf ("retrieving pci address without function failed: %v" , err )
@@ -1524,6 +1570,55 @@ func (f *virtNetworkTemplateFiller) do(virtualNetworks []virtualNetwork,
15241570 return nil
15251571}
15261572
1573+ // gopRomDir is where proprietary Intel GOP Option ROMs can be placed for display output.
1574+ // ROMs are named by PCI device ID, e.g. /persist/gop/3ea0.rom for device 0x3ea0.
1575+ // The 0x-prefixed form (0x3ea0.rom) is also accepted as a fallback.
1576+ const gopRomDir = "/persist/gop"
1577+
1578+ // igdRomPath is the bundled open-source IGD Option ROM built from VfioIgdPkg.
1579+ // It provides IgdAssignmentDxe which sets ASLS and BDSM (including Gen11+ 64-bit
1580+ // BDSM at PCI config 0xC0) without requiring a proprietary Intel GOP driver.
1581+ const igdRomPath = "/usr/lib/xen/boot/igd.rom"
1582+
1583+ // gopRomPath returns the path to a proprietary GOP ROM for the given PCI device ID
1584+ // (as read from sysfs, e.g. "0x3ea0") if one exists in gopRomDir, otherwise returns
1585+ // igdRomPath. A proprietary ROM provides the Intel GOP driver which enables the UEFI
1586+ // framebuffer (pre-OS display: GRUB, bootloader, UEFI shell). The bundled ROM provides
1587+ // only IgdAssignmentDxe: ASLS/BDSM are set correctly so the guest OS display driver
1588+ // works, but there is no display output before the OS driver loads.
1589+ func gopRomPath (devid string ) string {
1590+ name := strings .TrimPrefix (strings .ToLower (strings .TrimSpace (devid )), "0x" )
1591+ for _ , filename := range []string {name + ".rom" , "0x" + name + ".rom" } {
1592+ path := filepath .Join (gopRomDir , filename )
1593+ if _ , err := os .Stat (path ); err == nil {
1594+ return path
1595+ }
1596+ }
1597+ return igdRomPath
1598+ }
1599+
1600+ // detectIntelIGPU returns true if any adapter in the passthrough list is an Intel VGA device.
1601+ func detectIntelIGPU (adapters []types.IoAdapter , aa * types.AssignableAdapters ) bool {
1602+ if aa == nil {
1603+ return false
1604+ }
1605+ for _ , adapter := range adapters {
1606+ for _ , ib := range aa .LookupIoBundleAny (adapter .Name ) {
1607+ if ib == nil || ib .PciLong == "" || ib .UsbAddr != "" {
1608+ continue
1609+ }
1610+ dev := pciDevice {ioBundle : * ib }
1611+ if ! dev .isVGA () {
1612+ continue
1613+ }
1614+ if vendor , err := dev .vid (); err == nil && vendor == "0x8086" {
1615+ return true
1616+ }
1617+ }
1618+ }
1619+ return false
1620+ }
1621+
15271622// CreateDomConfig creates a domain config (a qemu config file,
15281623// typically named something like xen-%d.cfg)
15291624func (ctx KvmContext ) CreateDomConfig (domainName string ,
@@ -1548,12 +1643,14 @@ func (ctx KvmContext) CreateDomConfig(domainName string,
15481643 // 4. Default (BOOT_ORDER_UNSPECIFIED) - no boot order modification
15491644 // By the time we get here, config.BootOrder has the final resolved value.
15501645 bootOrder := bootOrderToFwCfgString (config .BootOrder )
1646+ hasIntelIGPU := detectIntelIGPU (config .IoAdapterList , aa )
15511647
15521648 qemuConfContext := tQemuGlobalConfContext {
15531649 Machine : ctx .devicemodel ,
15541650 VirtualizationMode : virtualizationMode ,
15551651 BootLoaderSettingsFile : bootLoaderSettingsFile ,
15561652 BootOrder : bootOrder ,
1653+ HasIntelIGPU : hasIntelIGPU ,
15571654 DomainConfig : config ,
15581655 DomainStatus : status ,
15591656 }
@@ -1702,6 +1799,7 @@ func (ctx KvmContext) CreateDomConfig(domainName string,
17021799 pciAssignmentsFiller := pciAssignmentsTemplateFiller {
17031800 multifunctionDevices : multifunctionDevices ,
17041801 file : file ,
1802+ domainUUID : config .UUIDandVersion .UUID .String (),
17051803 }
17061804 err = pciAssignmentsFiller .do (pciAssignments )
17071805 if err != nil {
0 commit comments