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:
DXGADAPTER::InitializeParavirtualizedAdapter sends KMTQAITYPE_PHYSICALADAPTERDEVICEIDS (Type 31) to the host via DXG_GUEST_VIRTUALGPU_VMBUS::VmBusSendQueryAdapterInfo
- The host's
DxgkQueryAdapterInfoImpl handles Type 31 by reading real PCI device IDs from PhysicalAdapterArray[index] → [+0x40] → offsets 0x460..0x474
- The host returns: VendorId=0x10DE, DeviceId=0x2684, SubVendorId=0x1462, SubDeviceId=0x5103, Revision=0xA1
- 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:
SAdapterDesc::SAdapterDesc (dxgi.dll) calls DisplayConfigGetDeviceInfo with a private type (-21) and a 2056-byte output structure
- The guest
dxgkrnl.sys handles this query and fills the structure
- VendorId (offset 209) and DeviceId (offset 210) are populated correctly from NVIDIA's paravirtualized adapter data
- Revision (offset 213) is left as zero — the handler does not read from
DXGADAPTER+436
SAdapterDesc::FillInDesc copies these values into the adapter descriptor
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
- Set up a Hyper-V VM with GPU-P (GPU Partitioning) using an NVIDIA GPU
- 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")
- 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().
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()returnsRevision = 0for 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
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
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.
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
DisplayConfigGetDeviceInfohandler does not consult the fields populated byInitializeParavirtualizedAdapter.Root cause analysis
We performed a full reverse-engineering trace through
dxgkrnl.sys(host, with PDB) anddxgi.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:
DXGADAPTER::InitializeParavirtualizedAdaptersendsKMTQAITYPE_PHYSICALADAPTERDEVICEIDS(Type 31) to the host viaDXG_GUEST_VIRTUALGPU_VMBUS::VmBusSendQueryAdapterInfoDxgkQueryAdapterInfoImplhandles Type 31 by reading real PCI device IDs fromPhysicalAdapterArray[index] → [+0x40] → offsets 0x460..0x474DXGADAPTER+420..436Confirmed empirically — calling
D3DKMTQueryAdapterInfowith 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:
SAdapterDesc::SAdapterDesc(dxgi.dll) callsDisplayConfigGetDeviceInfowith a private type (-21) and a 2056-byte output structuredxgkrnl.syshandles this query and fills the structureDXGADAPTER+436SAdapterDesc::FillInDesccopies these values into the adapter descriptorCDXGIBaseAdapter::GetDescreturns the descriptor withRevision = 0The disconnect
The
DisplayConfigGetDeviceInfohandler (type -21) in the guestdxgkrnl.syspopulates VendorId and DeviceId from the correct NVIDIA source, but does not read SubSysId, SubVendorId, SubDeviceId, or Revision fromDXGADAPTER+428..436whereInitializeParavirtualizedAdapterstored 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.RevisionorSubSysIdwill get incorrect data on GPU-P VMs. This affects:Reproduction
Suggested fix
The
DisplayConfigGetDeviceInfohandler (type -21) indxgkrnl.sysshould readSubSysIdandRevisionfrom the values stored byDXGADAPTER::InitializeParavirtualizedAdapter(offsets +428..+436 in the DXGADAPTER object), which are already correctly populated from the host via VMBusKMTQAITYPE_PHYSICALADAPTERDEVICEIDSquery.Workaround
Scan the target process heap for
DXGI_ADAPTER_DESCstructures (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 readsGetDesc().