Skip to content

[Hyper-V] [GPU-P] DXGI_ADAPTER_DESC returns Revision=0 and SubSysId=0 for partitioned GPU #40119

@K4leri

Description

@K4leri

I dk where i should post my issue cause microsoft ban my country from everywhere thats why i m doing it here. I hope u can message it to main devs on DXGI


Summary

When using GPU Partitioning (GPU-P) in Hyper-V, IDXGIAdapter::GetDesc() returns Revision = 0 for the partitioned GPU, even though the guest kernel (dxgkrnl.sys) correctly receives and stores the real PCI Revision ID from the host via VMBus.

This causes applications that check the GPU revision (e.g., CS2/Source 2 engine) to misidentify the hardware and disable features.

Environment

  • Host OS: Windows 11 Pro build 26200
  • Guest OS: Windows Server 2022 build 25295
  • GPU: NVIDIA RTX 4090 (PCI VendorId=0x10DE, DeviceId=0x2684, Revision=0xA1)
  • Driver: NVIDIA 595.79
  • GPU-P configured via: ExHyperV 1.4.1

Expected behavior

IDXGIAdapter::GetDesc() in the guest VM should return the real GPU's Revision ID (0xA1), consistent with VendorId and DeviceId which are already correctly reported.

Actual behavior

DXGI_ADAPTER_DESC:
  Description: NVIDIA GeForce RTX 4090
  VendorId:    0x10DE  ✅ correct
  DeviceId:    0x2684  ✅ correct
  SubSysId:    0x0000  ❌ should be non-zero
  Revision:    0x0000  ❌ should be 0xA1

Data flow diagram

The diagram below shows how PCI device identifiers travel from the host GPU to the application inside a GPU-P guest VM. Two independent paths exist in the guest kernel — Path A delivers the correct Revision, but Path B (the one DXGI actually uses) does not read it.

 ┌──────────────────────────────────────────────────────────────────────────────────┐
 │                                    HOST                                          │
 │                                                                                  │
 │   ┌───────────────────┐        ┌──────────────────────────────────────────┐      │
 │   │   NVIDIA RTX 4090 │        │  dxgkrnl.sys (host)                      │      │
 │   │   PCI Config:     │───────>│  DxgkQueryAdapterInfoImpl                │      │
 │   │   VEN  = 0x10DE   │ reads  │  PhysicalAdapterArray[0]                 │      │
 │   │   DEV  = 0x2684   │        │    → VendorId  = 0x10DE                  │      │
 │   │   REV  = 0xA1  ✅ │        │    → DeviceId  = 0x2684                 │      │
 │   └───────────────────┘        │    → Revision  = 0xA1  ✅               │      │
 │                                └──────────────┬───────────────────────────┘     │
 │                                               │                                 │
 └───────────────────────────────────────────────┼─────────────────────────────────┘
                                                 │  VMBus
                                    Type 31 query│& response
                                                 │
 ┌───────────────────────────────────────────────┼─────────────────────────────────┐
 │                                  GUEST VM     │                                 │
 │                                               ▼                                 │
 │   ┌─────────────────────────────────────────────────────────────────────────┐   │
 │   │  dxgkrnl.sys (guest)                                                    │   │
 │   │                                                                         │   │
 │   │   DXGADAPTER object                                                     │   │
 │   │   ┌──────────────────────────────────────────────────────────────────┐  │   │
 │   │   │  +420: VendorId    = 0x10DE  ✅                                 │  │   │
 │   │   │  +424: DeviceId    = 0x2684  ✅                                 │  │   │
 │   │   │  +428: SubVendorId = 0x1462  ✅                                 │  │   │
 │   │   │  +432: SubDeviceId = 0x5103  ✅                                 │  │   │
 │   │   │  +436: Revision    = 0xA1    ✅  ◄── stored correctly           │  │   │
 │   │   └──────────────────────────────────────────────────────────────────┘  │   │
 │   │        │                                          │                     │   │
 │   │        │ PATH A                                   │ PATH B              │   │
 │   │        │ D3DKMTQueryAdapterInfo                   │ DisplayConfig       │   │
 │   │        │ Type 31                                  │ GetDeviceInfo       │   │
 │   │        │                                          │ (type -21)          │   │
 │   │        ▼                                          ▼                     │   │
 │   │   ┌──────────────┐                    ┌────────────────────────┐        │   │
 │   │   │  Reads from  │                    │  Fills 2056-byte       │        │   │
 │   │   │  DXGADAPTER  │                    │  response struct:      │        │   │
 │   │   │  +420..+436  │                    │                        │        │   │
 │   │   │              │                    │  VendorId = 0x10DE ✅  │       │   │
 │   │   │  Returns:    │                    │  DeviceId = 0x2684 ✅  │       │   │
 │   │   │  Rev = 0xA1  │                    │  SubSysId = 0x0000 ❌  │       │   │
 │   │   │  ✅ CORRECT  │                    │  Revision = 0x0000 ❌  │       │   │
 │   │   └──────┬───────┘                    │                        │        │   │
 │   │          │                             │  ⚠ Does NOT read from │       │   │
 │   │          │                             │  DXGADAPTER +428..436 │        │   │
 │   │          │                             └───────────┬────────────┘       │   │
 │   └──────────┼─────────────────────────────────────────┼────────────────────┘   │
 │              │                                         │                        │
 │              ▼                                         ▼                        │
 │   ┌──────────────────────┐              ┌──────────────────────────────┐        │
 │   │  D3DKMTQuery...      │              │  dxgi.dll                    │        │
 │   │  (usermode callers)  │              │  SAdapterDesc::FillInDesc()  │        │
 │   │                      │              │  CDXGIBaseAdapter::GetDesc() │        │
 │   │  Rev = 0xA1 ✅       │              │                              │        │
 │   │  (nobody uses this)  │              │  DXGI_ADAPTER_DESC:          │        │
 │   └──────────────────────┘              │    VendorId = 0x10DE ✅      │        │
 │                                         │    DeviceId = 0x2684 ✅      │        │
 │                                         │    SubSysId = 0x0000 ❌      │        │
 │                                         │    Revision = 0x0000 ❌      │        │
 │                                         └──────────────┬───────────────┘        │
 │                                                        │                        │
 │                                                        ▼                        │
 │                                         ┌──────────────────────────────┐        │
 │                                         │  Application (e.g. CS2)      │        │
 │                                         │                              │        │
 │                                         │  IDXGIAdapter::GetDesc()     │        │
 │                                         │  → Revision = 0x0000         │        │
 │                                         └──────────────────────────────┘        │
 │                                                                                 │
 └─────────────────────────────────────────────────────────────────────────────────┘

The bug: Path A and Path B read from different sources inside the same kernel object. Path A correctly returns the host GPU's Revision. Path B — which is the only path DXGI uses — returns zero because the DisplayConfigGetDeviceInfo handler does not consult the fields populated by InitializeParavirtualizedAdapter.

Root cause analysis

We performed a full reverse-engineering trace through dxgkrnl.sys (host, with PDB) and dxgi.dll (guest, with PDB) using IDA Pro. The data flow has a disconnect:

Path A: VMBus query (works correctly)

During adapter initialization, the guest kernel queries the host for physical adapter device IDs:

  1. DXGADAPTER::InitializeParavirtualizedAdapter sends KMTQAITYPE_PHYSICALADAPTERDEVICEIDS (Type 31) to the host via DXG_GUEST_VIRTUALGPU_VMBUS::VmBusSendQueryAdapterInfo
  2. The host's DxgkQueryAdapterInfoImpl handles Type 31 by reading real PCI device IDs from PhysicalAdapterArray[index] → [+0x40] → offsets 0x460..0x474
  3. The host returns: VendorId=0x10DE, DeviceId=0x2684, SubVendorId=0x1462, SubDeviceId=0x5103, Revision=0xA1
  4. The guest stores these at DXGADAPTER+420..436

Confirmed empirically — calling D3DKMTQueryAdapterInfo with Type 31 from userspace on the guest returns Revision=0xA1.

Path B: DXGI adapter descriptor (broken)

When DXGI enumerates adapters, it takes a completely different code path:

  1. SAdapterDesc::SAdapterDesc (dxgi.dll) calls DisplayConfigGetDeviceInfo with a private type (-21) and a 2056-byte output structure
  2. The guest dxgkrnl.sys handles this query and fills the structure
  3. VendorId (offset 209) and DeviceId (offset 210) are populated correctly from NVIDIA's paravirtualized adapter data
  4. Revision (offset 213) is left as zero — the handler does not read from DXGADAPTER+436
  5. SAdapterDesc::FillInDesc copies these values into the adapter descriptor
  6. CDXGIBaseAdapter::GetDesc returns the descriptor with Revision = 0

The disconnect

The DisplayConfigGetDeviceInfo handler (type -21) in the guest dxgkrnl.sys populates VendorId and DeviceId from the correct NVIDIA source, but does not read SubSysId, SubVendorId, SubDeviceId, or Revision from DXGADAPTER+428..436 where InitializeParavirtualizedAdapter stored them after the VMBus query.

These fields remain zero in the DisplayConfig response, so DXGI sees Revision = 0.

Impact

Any application that checks DXGI_ADAPTER_DESC.Revision or SubSysId will get incorrect data on GPU-P VMs. This affects:

  • Applications that use Revision for GPU feature detection or driver-specific workarounds
  • Applications that use SubSysId to identify specific GPU SKUs (e.g., OEM variants)
  • Diagnostic and profiling tools that report hardware details

Reproduction

  1. Set up a Hyper-V VM with GPU-P (GPU Partitioning) using an NVIDIA GPU
  2. Inside the VM, run the following Python script. It queries the same adapter through both paths and shows the discrepancy:
"""Reproduction: GPU-P Revision bug — kernel has correct value, DXGI returns 0"""
import ctypes, ctypes.wintypes as wt

gdi32 = ctypes.windll.gdi32

class LUID(ctypes.Structure):
    _fields_ = [("LowPart", wt.DWORD), ("HighPart", wt.LONG)]
class GUID(ctypes.Structure):
    _fields_ = [("Data1", ctypes.c_ulong), ("Data2", ctypes.c_ushort),
                ("Data3", ctypes.c_ushort), ("Data4", ctypes.c_ubyte * 8)]
class D3DKMT_ADAPTERINFO(ctypes.Structure):
    _fields_ = [("hAdapter", wt.UINT), ("AdapterLuid", LUID),
                ("NumOfSources", wt.ULONG), ("bPrecisePresentRegionsPreferred", wt.BOOL)]
class D3DKMT_ENUMADAPTERS(ctypes.Structure):
    _fields_ = [("NumAdapters", wt.ULONG), ("Adapters", D3DKMT_ADAPTERINFO * 16)]
class D3DKMT_QUERYADAPTERINFO(ctypes.Structure):
    _fields_ = [("hAdapter", wt.UINT), ("Type", wt.UINT),
                ("pPrivateDriverData", ctypes.c_void_p), ("PrivateDriverDataSize", wt.UINT)]
class PHYSICAL_IDS(ctypes.Structure):
    _fields_ = [("PhysicalAdapterIndex", wt.UINT), ("VendorId", wt.UINT),
                ("DeviceId", wt.UINT), ("SubVendorId", wt.UINT),
                ("SubDeviceId", wt.UINT), ("Revision", wt.UINT), ("BusType", wt.UINT)]
class DXGI_ADAPTER_DESC(ctypes.Structure):
    _fields_ = [("Description", ctypes.c_wchar * 128),
                ("VendorId", wt.UINT), ("DeviceId", wt.UINT),
                ("SubSysId", wt.UINT), ("Revision", wt.UINT),
                ("DedicatedVideoMemory", ctypes.c_size_t),
                ("DedicatedSystemMemory", ctypes.c_size_t),
                ("SharedSystemMemory", ctypes.c_size_t), ("AdapterLuid", LUID)]

def get_vtable_func(obj, index, restype, *argtypes):
    vt = ctypes.cast(obj, ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p)))[0]
    return ctypes.WINFUNCTYPE(restype, *argtypes)(vt[index])

# --- Path A: kernel value ---
print("PATH A: D3DKMTQueryAdapterInfo Type 31 (kernel stored value)")
e = D3DKMT_ENUMADAPTERS(); e.NumAdapters = 16
gdi32.D3DKMTEnumAdapters(ctypes.byref(e))
for i in range(e.NumAdapters):
    a = e.Adapters[i]
    if not a.hAdapter: continue
    ids = PHYSICAL_IDS(); ids.PhysicalAdapterIndex = 0
    q = D3DKMT_QUERYADAPTERINFO(a.hAdapter, 31, ctypes.addressof(ids), ctypes.sizeof(ids))
    if gdi32.D3DKMTQueryAdapterInfo(ctypes.byref(q)) == 0 and ids.VendorId:
        print(f"  Adapter {i}: Vendor=0x{ids.VendorId:04X} Device=0x{ids.DeviceId:04X} Revision=0x{ids.Revision:04X}")

# --- Path B: DXGI value ---
print("\nPATH B: IDXGIAdapter::GetDesc() (what applications see)")
ctypes.windll.ole32.CoInitializeEx(None, 0)
dxgi = ctypes.WinDLL("dxgi")
IID = GUID(0x770aae40, 0x766f, 0x4d46, (ctypes.c_ubyte*8)(0x9d,0xba,0xbc,0x0b,0x74,0xdb,0x89,0x86))
factory = ctypes.c_void_p()
dxgi.CreateDXGIFactory1(ctypes.byref(IID), ctypes.byref(factory))
EnumAdapters = get_vtable_func(factory, 7, ctypes.c_long, ctypes.c_void_p, wt.UINT, ctypes.POINTER(ctypes.c_void_p))
idx = 0
while True:
    adapter = ctypes.c_void_p()
    if EnumAdapters(factory, idx, ctypes.byref(adapter)) != 0: break
    GetDesc = get_vtable_func(adapter, 8, ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(DXGI_ADAPTER_DESC))
    desc = DXGI_ADAPTER_DESC()
    if GetDesc(adapter, ctypes.byref(desc)) == 0:
        print(f"  Adapter {idx}: {desc.Description}  Vendor=0x{desc.VendorId:04X} "
              f"Device=0x{desc.DeviceId:04X} SubSys=0x{desc.SubSysId:08X} Revision=0x{desc.Revision:04X}")
    get_vtable_func(adapter, 2, ctypes.c_ulong, ctypes.c_void_p)(adapter)
    idx += 1
get_vtable_func(factory, 2, ctypes.c_ulong, ctypes.c_void_p)(factory)

print("\nBUG: Path A returns correct Revision, Path B returns 0x0000")
  1. Expected output showing the discrepancy:
PATH A: D3DKMTQueryAdapterInfo Type 31 (kernel stored value)
  Adapter 0: Vendor=0x10DE Device=0x2684 Revision=0x00A1

PATH B: IDXGIAdapter::GetDesc() (what applications see)
  Adapter 0: NVIDIA GeForce RTX 4090  Vendor=0x10DE Device=0x2684 SubSys=0x00000000 Revision=0x0000

BUG: Path A returns correct Revision, Path B returns 0x0000

Suggested fix

The DisplayConfigGetDeviceInfo handler (type -21) in dxgkrnl.sys should read SubSysId and Revision from the values stored by DXGADAPTER::InitializeParavirtualizedAdapter (offsets +428..+436 in the DXGADAPTER object), which are already correctly populated from the host via VMBus KMTQAITYPE_PHYSICALADAPTERDEVICEIDS query.

Workaround

Scan the target process heap for DXGI_ADAPTER_DESC structures (pattern: VendorId + DeviceId + SubSysId=0 + Revision=0) and patch the Revision field in memory after DXGI initialization. This is a data-only patch (no code modification) and must be applied before the application reads GetDesc().

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions