gpu: make device discovery non-fatal and refresh it on unknown LUIDs (#2573)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-09 13:57:10 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent c78f1c64d8
commit fa3abe8c3c
13 changed files with 648 additions and 161 deletions
+55 -37
View File
@@ -16,6 +16,7 @@
package cfgmgr32
import (
"errors"
"fmt"
"unsafe"
@@ -46,47 +47,64 @@ func GetDevicesInstanceIDs(deviceID string) ([]Device, error) {
deviceInstanceIDs := win32.ParseMultiSz(listBuffer)
devices := make([]Device, 0, len(deviceInstanceIDs))
var errs []error
// Devices whose properties can't be read are skipped. The remaining devices
// are returned together with an error describing the skipped ones.
for _, deviceInstanceID := range deviceInstanceIDs {
var devNode *windows.Handle
err = CMLocateDevNode(&devNode, deviceInstanceID)
device, err := getDevice(deviceInstanceID)
if err != nil {
return nil, err
errs = append(errs, fmt.Errorf("device %s: %w", windows.UTF16ToString(deviceInstanceID), err))
continue
}
var (
busNumber uint32
deviceAddress uint32
propType uint32
)
propLen := uint32(4)
err = CMGetDevNodeProperty(devNode, DEVPKEYDeviceBusNumber, &propType, unsafe.Pointer(&busNumber), &propLen)
if err != nil {
return nil, err
}
if propType != DEVPROP_TYPE_UINT32 {
return nil, fmt.Errorf("unexpected property type: 0x%08X", propType)
}
err = CMGetDevNodeProperty(devNode, DEVPKEYDeviceAddress, &propType, unsafe.Pointer(&deviceAddress), &propLen)
if err != nil {
return nil, err
}
if propType != DEVPROP_TYPE_UINT32 {
return nil, fmt.Errorf("unexpected property type: 0x%08X", propType)
}
devices = append(devices, Device{
InstanceID: windows.UTF16ToString(deviceInstanceID),
BusNumber: win32.UINT(busNumber),
DeviceNumber: win32.UINT(deviceAddress >> 16),
FunctionNumber: win32.UINT(deviceAddress & 0xFFFF),
})
devices = append(devices, device)
}
return devices, nil
return devices, errors.Join(errs...)
}
func getDevice(deviceInstanceID []uint16) (Device, error) {
var devInst uint32
if err := CMLocateDevNode(&devInst, deviceInstanceID); err != nil {
return Device{}, err
}
busNumber, err := getDevNodePropertyUint32(devInst, DEVPKEYDeviceBusNumber)
if err != nil {
return Device{}, fmt.Errorf("bus number: %w", err)
}
deviceAddress, err := getDevNodePropertyUint32(devInst, DEVPKEYDeviceAddress)
if err != nil {
return Device{}, fmt.Errorf("device address: %w", err)
}
return Device{
InstanceID: windows.UTF16ToString(deviceInstanceID),
BusNumber: win32.UINT(busNumber),
DeviceNumber: win32.UINT(deviceAddress >> 16),
FunctionNumber: win32.UINT(deviceAddress & 0xFFFF),
}, nil
}
func getDevNodePropertyUint32(devInst uint32, propKey *DEVPROPKEY) (uint32, error) {
var (
value uint32
propType uint32
)
propLen := uint32(unsafe.Sizeof(value))
if err := CMGetDevNodeProperty(devInst, propKey, &propType, unsafe.Pointer(&value), &propLen); err != nil {
return 0, err
}
if propType != DEVPROP_TYPE_UINT32 {
return 0, fmt.Errorf("unexpected property type: 0x%08X", propType)
}
return value, nil
}
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package cfgmgr32_test
import (
"strings"
"testing"
"github.com/prometheus-community/windows_exporter/internal/headers/cfgmgr32"
"github.com/prometheus-community/windows_exporter/internal/headers/gdi32"
"github.com/stretchr/testify/require"
)
func TestGetDevicesInstanceIDs(t *testing.T) {
t.Parallel()
gpus, err := gdi32.GetGPUDevices()
require.NoError(t, err)
var tested bool
for _, gpu := range gpus {
// Only test physical render adapters, which are enumerated on the PCI bus.
// Microsoft adapters (VEN_1414), like the Microsoft Basic Render Driver, are virtual
// and not always flagged as software device.
if gpu.IsSoftwareDevice() ||
strings.HasPrefix(gpu.DeviceID, `PCI\VEN_1414&`) ||
gpu.AdapterType&gdi32.D3DKMT_ADAPTERTYPE_RENDER_SUPPORTED == 0 ||
gpu.AdapterType&gdi32.D3DKMT_ADAPTERTYPE_PARAVIRTUALIZED != 0 {
continue
}
devices, err := cfgmgr32.GetDevicesInstanceIDs(gpu.DeviceID)
require.NoError(t, err)
require.NotEmpty(t, devices)
var found bool
for _, device := range devices {
require.NotEmpty(t, device.InstanceID)
if device.BusNumber == gpu.BusNumber && device.DeviceNumber == gpu.DeviceNumber && device.FunctionNumber == gpu.FunctionNumber {
found = true
}
}
require.True(t, found, "no device instance matches the PCI address of %s", gpu.DeviceID)
tested = true
}
if !tested {
t.Skip("no hardware GPU found")
}
}
+4 -3
View File
@@ -62,7 +62,8 @@ func CMGetDeviceIDList(filter *win32.LPWSTR, buf []uint16) error {
return nil
}
func CMLocateDevNode(devInst **windows.Handle, deviceID []uint16) error {
// CMLocateDevNode stores the DEVINST (a DWORD) of the device node in devInst.
func CMLocateDevNode(devInst *uint32, deviceID []uint16) error {
ret, _, _ := procCMLocateDevNodeW.Call(
uintptr(unsafe.Pointer(devInst)),
uintptr(unsafe.Pointer(&deviceID[0])),
@@ -76,9 +77,9 @@ func CMLocateDevNode(devInst **windows.Handle, deviceID []uint16) error {
return nil
}
func CMGetDevNodeProperty(devInst *windows.Handle, propKey *DEVPROPKEY, propType *uint32, buf unsafe.Pointer, bufLen *uint32) error {
func CMGetDevNodeProperty(devInst uint32, propKey *DEVPROPKEY, propType *uint32, buf unsafe.Pointer, bufLen *uint32) error {
ret, _, _ := procCMGetDevNodePropertyW.Call(
uintptr(unsafe.Pointer(devInst)),
uintptr(devInst),
uintptr(unsafe.Pointer(propKey)),
uintptr(unsafe.Pointer(propType)),
uintptr(buf),
+19 -6
View File
@@ -34,6 +34,8 @@ const (
KMTQAITYPE_ADAPTERADDRESS = 6
// KMTQAITYPE_ADAPTERREGISTRYINFO pPrivateDriverData points to a D3DKMT_ADAPTERREGISTRYINFO structure that contains registry information about the graphics adapter.
KMTQAITYPE_ADAPTERREGISTRYINFO = 8
// KMTQAITYPE_ADAPTERTYPE pPrivateDriverData points to a D3DKMT_ADAPTERTYPE structure that specifies the type of graphics adapter. Supported starting with Windows 8 (WDDM 1.2).
KMTQAITYPE_ADAPTERTYPE = 15
// KMTQAITYPE_PHYSICALADAPTERDEVICEIDS pPrivateDriverData points to a D3DKMT_QUERY_DEVICE_IDS structure that specifies the device ID(s) of the physical adapters. Supported starting with Windows 10 (WDDM 2.0).
KMTQAITYPE_PHYSICALADAPTERDEVICEIDS = 31
// KMTQAITYPE_DRIVERVERSION pPrivateDriverData points to a D3DKMT_DRIVERVERSION value that indicates the WDDM version of the display miniport driver.
@@ -103,6 +105,18 @@ func GetGPUDevice(hAdapter D3DKMT_HANDLE) (GPUDevice, error) {
gpuDevice.AdapterString = windows.UTF16ToString(info.AdapterString[:])
var adapterType D3DKMT_ADAPTERTYPE
query.queryType = KMTQAITYPE_ADAPTERTYPE
query.pPrivateDriverData = unsafe.Pointer(&adapterType)
query.privateDriverDataSize = uint32(unsafe.Sizeof(adapterType))
if err := D3DKMTQueryAdapterInfo(&query); err != nil {
return gpuDevice, fmt.Errorf("D3DKMTQueryAdapterInfo (adapter type) failed: %w", err)
}
gpuDevice.AdapterType = adapterType
var deviceIDs D3DKMT_QUERY_DEVICE_IDS
query.queryType = KMTQAITYPE_PHYSICALADAPTERDEVICEIDS
@@ -118,6 +132,9 @@ func GetGPUDevice(hAdapter D3DKMT_HANDLE) (GPUDevice, error) {
return gpuDevice, nil
}
// GetGPUDevices returns all GPU adapters known to the graphics kernel.
// If a single adapter can't be queried, the remaining adapters are still
// returned, together with an error describing the failed adapters.
func GetGPUDevices() ([]GPUDevice, error) {
gpuDevices := make([]GPUDevice, 0, 2)
@@ -177,15 +194,11 @@ func GetGPUDevices() ([]GPUDevice, error) {
}()
}
if len(errs) > 0 {
return gpuDevices, errors.Join(errs...)
}
if len(gpuDevices) == 0 {
return gpuDevices, ErrNoGPUDevices
errs = append(errs, ErrNoGPUDevices)
}
return gpuDevices, nil
return gpuDevices, errors.Join(errs...)
}
func formatPNPDeviceID(deviceIDs D3DKMT_QUERY_DEVICE_IDS) string {
+7
View File
@@ -27,4 +27,11 @@ func TestGetGPUDevices(t *testing.T) {
require.NoError(t, err, "Failed to get GPU devices")
require.NotNil(t, devices)
// The adapter type flags differ between systems, e.g. the Microsoft Basic Render Driver
// is not flagged as software device on Windows Server 2022. Log them for diagnostics.
for _, device := range devices {
t.Logf("adapter %q: device ID %s, adapter type 0b%b, software device %t",
device.AdapterString, device.DeviceID, uint32(device.AdapterType), device.IsSoftwareDevice())
}
}
+28
View File
@@ -95,6 +95,7 @@ type GPUDevice struct {
BusNumber win32.UINT
DeviceNumber win32.UINT
FunctionNumber win32.UINT
AdapterType D3DKMT_ADAPTERTYPE
}
// D3DKMT_NODE_PERFDATA is the output of KMTQAITYPE_NODEPERFDATA.
@@ -163,3 +164,30 @@ type D3DKMT_GPUVERSION struct {
BiosVersion [32]uint16
GpuArchitecture [32]uint16
}
// IsSoftwareDevice reports whether the adapter is a software device,
// e.g. the Microsoft Basic Render Driver.
func (d GPUDevice) IsSoftwareDevice() bool {
return d.AdapterType&D3DKMT_ADAPTERTYPE_SOFTWARE_DEVICE != 0
}
// D3DKMT_ADAPTERTYPE is the bitfield returned by KMTQAITYPE_ADAPTERTYPE.
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_adaptertype
type D3DKMT_ADAPTERTYPE uint32
const (
D3DKMT_ADAPTERTYPE_RENDER_SUPPORTED D3DKMT_ADAPTERTYPE = 1 << 0
D3DKMT_ADAPTERTYPE_DISPLAY_SUPPORTED D3DKMT_ADAPTERTYPE = 1 << 1
D3DKMT_ADAPTERTYPE_SOFTWARE_DEVICE D3DKMT_ADAPTERTYPE = 1 << 2
D3DKMT_ADAPTERTYPE_POST_DEVICE D3DKMT_ADAPTERTYPE = 1 << 3
D3DKMT_ADAPTERTYPE_HYBRID_DISCRETE D3DKMT_ADAPTERTYPE = 1 << 4
D3DKMT_ADAPTERTYPE_HYBRID_INTEGRATED D3DKMT_ADAPTERTYPE = 1 << 5
D3DKMT_ADAPTERTYPE_INDIRECT_DISPLAY_DEVICE D3DKMT_ADAPTERTYPE = 1 << 6
D3DKMT_ADAPTERTYPE_PARAVIRTUALIZED D3DKMT_ADAPTERTYPE = 1 << 7
D3DKMT_ADAPTERTYPE_ACG_SUPPORTED D3DKMT_ADAPTERTYPE = 1 << 8
D3DKMT_ADAPTERTYPE_SET_TIMINGS_FROM_VIDPN D3DKMT_ADAPTERTYPE = 1 << 9
D3DKMT_ADAPTERTYPE_DETACHABLE D3DKMT_ADAPTERTYPE = 1 << 10
D3DKMT_ADAPTERTYPE_COMPUTE_ONLY D3DKMT_ADAPTERTYPE = 1 << 11
D3DKMT_ADAPTERTYPE_PROTOTYPE D3DKMT_ADAPTERTYPE = 1 << 12
D3DKMT_ADAPTERTYPE_RUNTIME_POWER_MANAGEMENT D3DKMT_ADAPTERTYPE = 1 << 13
)