[PATCH v2] tools/hvmloader: implement Intel IGD extended VBT support

Chuck Zmudzinski <[email protected]> Sun, 2 Aug 2026 01:08:10 -0400
Newsgroups org.xenproject.lists.xen-devel,org.nongnu.qemu-devel
Message-ID <[email protected]>
Modern Intel IGD devices do not work well with the current
implementation of support for the Intel IGD in hvmloader because
it lacks support for an extended video bios table (VBT).

Code 43 errors in Windows guests and failure of the guest screen
to light up are some of the problems that occur with the
current implementation.

To address this problem, this patch implements support for
Intel IGD devices with an extended VBT and OpRegion version 2
and higher which is required for most modern Intel IGD devices.

This patch also depends on compatible support in the device
model. If hvmloader detects the device model lacks such support,
it will fall back to the currently implemented protocol for
configuring the OpRegion to provide backward compatibiltiy for
systems that lack a device model with support for an extended VBT.

Support for an extended VBT is implemented in the newly introduced
function intel_opregion_setup() which is implemented in the new
file intel_opregion.c.

Major differences between this implementation and the current
implemntation that only supports older devices without an
extended VBT:

1. The current implemntation reserves a constant number of
   pages (3) in the E820 map for the OpRegion which is set by
   the IGD_OPREGION_PAGES macro in the current implementation.
   With OpRegion 2 and higher, the OpRegion can have an
   extended VBT that must be provided to the guest with the
   OpRegion. This means the size of the region is not fixed,
   so in this new implementation the IGD_OPREGION_PAGES constant
   is changed to a variable in e820.c, igd_opregion_e820_pages,
   that is set to its proper value based on the the size of the
   VBT. In this new implemntation, the size of the ACPI NVS region
   reserved for the OpRegion in the E820 map is equal to the value
   of the igd_opregion_e820_pages variable instead of being set
   to the constant value determined by IGD_OPREGION_PAGES.

2. The current implemntation provides the guest with access
   to the unmodified OpRegion on the host via memory mapping
   from the host to the guest. This is insufficient for
   OpRegion 2 and higher because some devices will require
   modifications to the OpRegion for proper operation in the
   guest. So this new implementation provides hvmloader with a
   copy of the host's OpRegion that hvmloader can modify as
   needed for proper operation. Mapping the OpRegion from the
   host to the guest is only used temporarily during setup of
   the OpRegion by hvmloader and once hvmloader has a copy of
   the OpRegion and the extended VBT, the device model removes
   the host mapping and hvmloader configures the guest to use
   the guest's possibly modified copy of the OpRegion instead.

3. The current implementation lacks useful debugging information
   for the more recent devices. This new implementation provides
   useful debugging output from hvmloader, such as the detected
   host OpRegion version and address, the values for rvda, rvds,
   and the guest OpRegion address when the guest_loglvl is set
   to all/all.

Link: https://lore.kernel.org/kvm/[email protected]/
Link: https://lore.kernel.org/kvm/[email protected]/
Signed-off-by: Chuck Zmudzinski <[email protected]>
---
The companion patchset for the device model is available here:

https://lore.kernel.org/xen-devel/[email protected]/

There is an undocumented setting that works in the xl.cfg(5) domain
configuration file, firmware_override, that makes it possible to use
a patched version of hvmloader alongside an installation of unpatched
upstream Xen or a version of Xen packaged by a distro. So one can
download the source for one's installed version of Xen, apply this
patch and build just hvmloader and then install the patched version of
hvmloader with a different filename, such as hvmloader-igd-testing, into
the same directory where hvmloader is installed (usually something like
/usr/libexec/xen/boot) and then one can configure a guest to use the patched
version of hvmloader with one's installed version of Xen by adding a line
like this to the domain xl.cfg file:

firmware_override = 'hvmloader-igd-testing'

The compatible patch for the device model is part of a larger patchset
that fixes many of the problems that currently affect the feature of
Intel IGD passthrough to Xen HVM guests. This patch should be considered
as a companion patch to that patchset for the device model. Do not try
to test this patch with a real Intel IGD device without also applying
the patchset for the device model because without those patches, the
guest will most likely fail to start if an Intel IGD is passed through
to the guest.

There are different requirements to support OpRegion version 2.0
and OpRegion version 2.1+, with support for OpRegion 2 the more
difficult case because it always requires modifications to the OpRegion
for proper operation in the guest. For some details about OpRegion
2 and higher and the extended VBT, see the links in the commit message.

Changes in v2:
  - Correct the name of the new function in the commit message
    opregion_setup() -> intel_opregion_setup()

  - Add a link to the companion patchset for the device model

  - Describe how to use the firmware_override setting in xl.cfg(5)
    to simplify testing of this patch.

  - Correct a logical flaw that in case the size of the extended VBT
    is <= 2 pages, an extra, unnecessary page would be allocated in
    the memory hole. This correction is in the intel_opregion.c file.

    This code:

    /* Update the number of pages we need for the E820 map */
    igd_opregion_e820_pages = pages_needed;

    /*
     * So far we have allocated vbt_pages_needed
     * and we will likely need to allocate more
     * pages to fully contain OpRegion + VBT.
     */
    if ( pages_needed > vbt_pages_needed )
        igd_opregion_pgbase = mem_hole_alloc
                              (pages_needed - vbt_pages_needed);

    Is replaced with this code:

    /*
     * So far we have allocated igd_opregion_e820_pages
     * and we will likely need to allocate more
     * pages to fully contain OpRegion + VBT.
     */
    if ( pages_needed > igd_opregion_e820_pages )
        igd_opregion_pgbase = mem_hole_alloc
                              (pages_needed - igd_opregion_e820_pages);

    /* Update the number of pages we need for the E820 map */
    igd_opregion_e820_pages = pages_needed;

 tools/firmware/hvmloader/Makefile         |   1 +
 tools/firmware/hvmloader/config.h         |  15 +-
 tools/firmware/hvmloader/e820.c           |   4 +-
 tools/firmware/hvmloader/intel_opregion.c | 297 ++++++++++++++++++++++
 tools/firmware/hvmloader/pci.c            |  10 +-
 5 files changed, 313 insertions(+), 14 deletions(-)
 create mode 100644 tools/firmware/hvmloader/intel_opregion.c

diff --git a/tools/firmware/hvmloader/Makefile b/tools/firmware/hvmloader/Makefile
index 21de721..ed42915 100644
--- a/tools/firmware/hvmloader/Makefile
+++ b/tools/firmware/hvmloader/Makefile
@@ -35,6 +35,7 @@ OBJS += smp.o cacheattr.o xenbus.o vnuma.o
 OBJS += e820.o pci.o pir.o ctype.o
 OBJS += hvm_param.o
 OBJS += ovmf.o seabios.o
+OBJS += intel_opregion.o
 ifeq ($(debug),y)
 OBJS += tests.o
 endif
diff --git a/tools/firmware/hvmloader/config.h b/tools/firmware/hvmloader/config.h
index c159db3..bd3c0f9 100644
--- a/tools/firmware/hvmloader/config.h
+++ b/tools/firmware/hvmloader/config.h
@@ -7,9 +7,6 @@
 enum virtual_vga { VGA_none, VGA_std, VGA_cirrus, VGA_pt };
 extern enum virtual_vga virtual_vga;
 
-extern unsigned long igd_opregion_pgbase;
-#define IGD_OPREGION_PAGES 3
-
 struct bios_config {
     const char *name;
 
@@ -43,6 +40,18 @@ extern struct bios_config ovmf_config;
 
 #define PAGE_SHIFT 12
 #define PAGE_SIZE  (1ul << PAGE_SHIFT)
+#define IGD_OPREGION_PAGES 3
+#define IGD_OPREGION_SIZE ((IGD_OPREGION_PAGES - 1) << PAGE_SHIFT)
+#define IGD_OPREGION_RVDA 0x3ba
+#define IGD_OPREGION_RVDS 0x3c2
+#define IGD_OPREGION_VERSION 0x16
+#define IGD_OPREGION_MASK 0xfff
+#define IGD_OPREGION2_SUPPORT_MASK 0x1
+#define IGD_OPREGION_SIGNATURE "IntelGraphicsMem"
+#define IGD_VBT_SIGNATURE "$VBT"
+extern unsigned long igd_opregion_pgbase;
+extern uint32_t igd_opregion_e820_pages;
+void intel_opregion_setup(uint32_t vga_devfn);
 
 extern uint8_t ioapic_version;
 
diff --git a/tools/firmware/hvmloader/e820.c b/tools/firmware/hvmloader/e820.c
index 86d3954..97a234e 100644
--- a/tools/firmware/hvmloader/e820.c
+++ b/tools/firmware/hvmloader/e820.c
@@ -243,11 +243,11 @@ int build_e820_table(struct e820entry *e820,
         nr++;
 
         e820[nr].addr = igd_opregion_base;
-        e820[nr].size = IGD_OPREGION_PAGES * PAGE_SIZE;
+        e820[nr].size = igd_opregion_e820_pages * PAGE_SIZE;
         e820[nr].type = E820_NVS;
         nr++;
 
-        e820[nr].addr = igd_opregion_base + IGD_OPREGION_PAGES * PAGE_SIZE;
+        e820[nr].addr = igd_opregion_base + igd_opregion_e820_pages * PAGE_SIZE;
         e820[nr].size = (uint32_t)-e820[nr].addr;
         e820[nr].type = E820_RESERVED;
         nr++;
diff --git a/tools/firmware/hvmloader/intel_opregion.c b/tools/firmware/hvmloader/intel_opregion.c
new file mode 100644
index 0000000..59cb2c3
--- /dev/null
+++ b/tools/firmware/hvmloader/intel_opregion.c
@@ -0,0 +1,297 @@
+/*
+ * intel_opregion.c: HVM Intel OpRegion setup.
+ *
+ * Leendert van Doorn, [email protected]
+ * Copyright (c) 2005, International Business Machines Corporation.
+ *
+ * Copyright (c) 2006, Keir Fraser, XenSource Inc.
+ *
+ * Copyright (c) 2026, Charles Zmudzinski.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU General Public License,
+ * version 2, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "util.h"
+#include "config.h"
+#include "pci_regs.h"
+
+unsigned long igd_opregion_pgbase = 0;
+uint32_t igd_opregion_e820_pages = IGD_OPREGION_PAGES;
+
+static bool verify_opregion(const uint32_t addr)
+{
+    const char *opregion_signature = IGD_OPREGION_SIGNATURE;
+    if ( memcmp((const void *)addr, (const void *)opregion_signature, 16) )
+        return false;
+    return true;
+}
+
+static bool verify_vbt(const uint32_t addr)
+{
+    const char *vbt_signature = IGD_VBT_SIGNATURE;
+    if ( memcmp((const void *)addr, (const void *)vbt_signature, 4) )
+        return false;
+    return true;
+}
+
+void intel_opregion_setup(uint32_t vga_devfn)
+{
+    uint32_t igd_guest_opregion;
+    uint32_t pages_needed; /* for OpRegion + VBT */
+    void *opregion_scratch;
+    void *vbt_scratch;
+    void *vbt_source;
+    /*
+     * absolute value in the host/guest except
+     * as noted in the comments
+     */
+    static unsigned long rvda_host;
+    static unsigned long rvda_guest;
+
+    igd_opregion_pgbase = mem_hole_alloc(IGD_OPREGION_PAGES);
+    /*
+     * Tentative value for the number of pages to reserve
+     * in the E820 map for the OpRegion and VBT.
+     *
+     * This will be the final value for the E820 map if
+     * the device model lacks support for OpRegion 2 or
+     * if the host OpRegion version is < 2 or if we never
+     * allocate more pages in the E820 map for the VBT.
+     */
+    igd_opregion_e820_pages = IGD_OPREGION_PAGES;
+
+    /*
+     * Read the value the device model is initialized with.
+     * If the device model supports OpRegion 2, it will
+     * return the host IGD OpRegion address. If not, it
+     * will return 0. If the device model does not support
+     * OpRegion 2, the device model expects us to give it
+     * the address to which it will map the OpRegion in the
+     * guest and then expects us to do nothing more to setup
+     * the OpRegion, so that is all we will do in that case.
+     */
+    const uint32_t igd_host_opregion = pci_readl(vga_devfn,
+                                                 PCI_INTEL_OPREGION);
+    if ( !igd_host_opregion ) {
+        printf("device model lacks extended VBT "
+               "support. Continuing with legacy support only\n");
+        /*
+         * Write the the OpRegion offset to give the OpRegion
+         * address to the device model. The device model will trap
+         * and map the OpRegion at the give address.
+         */
+        pci_writel(vga_devfn, PCI_INTEL_OPREGION,
+                   igd_opregion_pgbase << PAGE_SHIFT);
+        return;
+    } else {
+        printf("host OpRegion address: 0x%x\n",
+               igd_host_opregion);
+    }
+
+    const uint32_t igd_host_opregion_page_offset =
+                   igd_host_opregion & IGD_OPREGION_MASK;
+    igd_guest_opregion = (igd_opregion_pgbase << PAGE_SHIFT) |
+                          igd_host_opregion_page_offset;
+
+    /*
+     * We know at this point the device model supports
+     * OpRegion 2.
+     *
+     * Indicate to the device model that we support
+     * OpRegion 2 by setting the least significant bit
+     * of the address we give to the device model.
+     * The device model will notice this bit set and
+     * respond appropriately to our writes to the
+     * register where the OpRegion address is stored.
+     */
+    pci_writel(vga_devfn, PCI_INTEL_OPREGION,
+               (igd_opregion_pgbase << PAGE_SHIFT) |
+                IGD_OPREGION2_SUPPORT_MASK);
+
+    printf("guest OpRegion tentative "
+           "address: 0x%x\n", igd_guest_opregion);
+
+    if ( !verify_opregion(igd_guest_opregion) ) {
+        printf("error: IGD OpRegion signature "
+               "not found.\n");
+        BUG();
+    }
+
+    opregion_scratch = scratch_alloc(IGD_OPREGION_SIZE, 0);
+    memcpy(opregion_scratch, (const void *)igd_guest_opregion,
+           IGD_OPREGION_SIZE);
+
+    /* Read OpRegion version, rvda_host, and rvds */
+    const uint16_t version = *(uint16_t *)(opregion_scratch +
+                                           IGD_OPREGION_VERSION);
+    printf("OpRegion version: 0x%x\n", version);
+    if ( version >= 0x0200 ) {
+        rvda_host = *(unsigned long *)(opregion_scratch +
+                                       IGD_OPREGION_RVDA);
+        /* It is convenient to make rvda_host absolute */
+        if ( version > 0x0200 )
+            rvda_host += igd_host_opregion;
+        printf("host VBT address: 0x%lx\n", rvda_host);
+    } else {
+        printf("guest OpRegion address: 0x%x\n", igd_guest_opregion);
+        rvda_host = 0;
+    }
+    const uint32_t rvda_host_page_offset = rvda_host &
+                                           IGD_OPREGION_MASK;
+    const uint32_t rvds = *(uint32_t *)(opregion_scratch +
+                                        IGD_OPREGION_RVDS);
+    const uint32_t rvds_page_offset = rvds & IGD_OPREGION_MASK;
+    printf("VBT size: 0x%x\n", rvds);
+
+    if ( !rvds || !rvda_host ) {
+        printf("guest OpRegion address: 0x%x\n", igd_guest_opregion);
+        rvda_host = 0;
+    }
+    /*
+     * Write rvda_host as 2 successive 32-bit values
+     * to communicate location of the VBT to the device
+     * model. If rvda_host is not 0, The device model
+     * unmaps the OpRegion and eventually maps the VBT
+     * after we also write the guest address where the
+     * VBT will be mapped.
+     *
+     * If we send rvda_host = 0 to the device model, it
+     * will assume we do not need OpRegion 2 support and
+     * it will not unmap the OpRegion.
+     */
+    pci_writel(vga_devfn, PCI_INTEL_OPREGION,
+               (uint32_t)(rvda_host & 0xfffffffful));
+    unsigned long rvda_host_upper_32 = (uint64_t)rvda_host >> 32;
+    pci_writel(vga_devfn, PCI_INTEL_OPREGION,
+               (uint32_t)rvda_host_upper_32);
+
+    /* In this case, we use the mapped OpRegion */
+    if ( !rvda_host )
+        return;
+
+    /*
+     * Update the number of pages the device model
+     * needs to map for us to get a copy of the VBT.
+     *
+     * N.B.: Here, igd_opregion_pgbase is really the page
+     * base of the location where the device model will
+     * map the VBT.
+     */
+    uint32_t vbt_pages_needed = rvds >> PAGE_SHIFT;
+    if ( rvds & IGD_OPREGION_MASK )
+        vbt_pages_needed++;
+    if ( vbt_pages_needed > igd_opregion_e820_pages ) {
+        igd_opregion_pgbase = mem_hole_alloc
+                              (vbt_pages_needed - igd_opregion_e820_pages);
+        igd_opregion_e820_pages = vbt_pages_needed;
+    }
+
+    /*
+     * Write the location where the device model is to
+     * map the VBT in the guest with the 12 least
+     * significant bits encoded as the number of pages
+     * for the device model to map (vbt_pages_needed).
+     */
+    pci_writel(vga_devfn, PCI_INTEL_OPREGION,
+               ((igd_opregion_pgbase << PAGE_SHIFT) | vbt_pages_needed));
+
+    /*
+     * When the VBT is mapped from the host, the page offset
+     * of the VBT will be the same as on the host
+     */
+    rvda_guest = (igd_opregion_pgbase << PAGE_SHIFT) |
+                  rvda_host_page_offset;
+    if ( !verify_vbt(rvda_guest) ) {
+        printf("error: VBT signature not found.\n");
+        BUG();
+    }
+
+    vbt_source = (void *)rvda_guest;
+    vbt_scratch = scratch_alloc(rvds, 0);
+    memcpy(vbt_scratch, vbt_source, rvds);
+
+    /* Compute how many pages we need for OpRegion + VBT */
+    pages_needed = (IGD_OPREGION_SIZE + rvds) >> PAGE_SHIFT;
+    if ( (IGD_OPREGION_SIZE + rvds) & IGD_OPREGION_MASK )
+        pages_needed++;
+
+    /*
+     * So far we have allocated igd_opregion_e820_pages
+     * and we will likely need to allocate more
+     * pages to fully contain OpRegion + VBT.
+     */
+    if ( pages_needed > igd_opregion_e820_pages )
+        igd_opregion_pgbase = mem_hole_alloc
+                              (pages_needed - igd_opregion_e820_pages);
+
+    /* Update the number of pages we need for the E820 map */
+    igd_opregion_e820_pages = pages_needed;
+
+    /*
+     * Compute the final igd_guest_opregion value and
+     * keep the same offset as on the host if doing so
+     * will not push us across another page boundary.
+     */
+    igd_guest_opregion = igd_opregion_pgbase << PAGE_SHIFT;
+    if ( (igd_host_opregion_page_offset + rvds_page_offset) <= PAGE_SIZE )
+        igd_guest_opregion |= igd_host_opregion_page_offset;
+    printf("guest OpRegion address: 0x%x\n", igd_guest_opregion);
+
+    /* The device model will unmap the VBT */
+    pci_writel(vga_devfn, PCI_INTEL_OPREGION, igd_guest_opregion);
+
+    /*
+     * After unmapping we need to populate the memory hole.
+     * If the unmapping failed this will crash the guest.
+     *
+     * We could try to use the mapped VBT with our copy of the
+     * OpRegion, but it is probably better to BUG() if the
+     * device model failed to unmap the VBT.
+     */
+    if ( verify_vbt(rvda_guest) )
+        BUG();
+    mem_hole_populate_ram(igd_opregion_pgbase,
+                          igd_opregion_e820_pages);
+
+    /*
+     * After unmapping we are free to shift the VBT by
+     * an arbitrary number of bytes. For efficient use
+     * of memory and to keep the memory map simple,
+     * place the VBT contiguous after the OpRegion.
+     */
+    rvda_guest = igd_guest_opregion + IGD_OPREGION_SIZE;
+    printf("guest VBT address: 0x%lx\n", rvda_guest);
+
+    /*
+     * Until now, rvda_guest has been an absolute address
+     * in the guest. We need to translate it to a relative
+     * address if OpRegion version > 0x0200 and in that case
+     * we also verify it is contiguous with the OpRegion.
+     */
+    if ( version > 0x0200 ) {
+        rvda_guest -= igd_guest_opregion;
+        printf("guest rvda (relative): 0x%lx\n", rvda_guest);
+        BUG_ON(rvda_guest != IGD_OPREGION_SIZE);
+    }
+
+    /*
+     * Write the correct rvda_guest value to the
+     * guest copy of the OpRegion and copy the scratch
+     * buffers to the correct address in our E820 region.
+     */
+    *(unsigned long *)(opregion_scratch + IGD_OPREGION_RVDA) = rvda_guest;
+    memcpy((void *)(igd_guest_opregion + IGD_OPREGION_SIZE),
+           (const void *)vbt_scratch, rvds);
+    memcpy((void *)igd_guest_opregion,
+           (const void *)opregion_scratch, IGD_OPREGION_SIZE);
+}
diff --git a/tools/firmware/hvmloader/pci.c b/tools/firmware/hvmloader/pci.c
index c41c8d9..07a37e5 100644
--- a/tools/firmware/hvmloader/pci.c
+++ b/tools/firmware/hvmloader/pci.c
@@ -43,7 +43,6 @@ uint64_t pci_hi_mem_start = 0, pci_hi_mem_end = 0;
 #define BAR_RELOC_THRESH GB(1)
 
 enum virtual_vga virtual_vga = VGA_none;
-unsigned long igd_opregion_pgbase = 0;
 
 /* Check if the specified range conflicts with any reserved device memory. */
 static bool check_overlap_all(uint64_t start, uint64_t size)
@@ -190,14 +189,7 @@ void pci_setup(void)
                 virtual_vga = VGA_pt;
                 if ( vendor_id == 0x8086 )
                 {
-                    igd_opregion_pgbase = mem_hole_alloc(IGD_OPREGION_PAGES);
-                    /*
-                     * Write the the OpRegion offset to give the opregion
-                     * address to the device model. The device model will trap 
-                     * and map the OpRegion at the give address.
-                     */
-                    pci_writel(vga_devfn, PCI_INTEL_OPREGION,
-                               igd_opregion_pgbase << PAGE_SHIFT);
+                    intel_opregion_setup(vga_devfn);
                 }
             }
             break;
-- 
2.52.0