Add VRAM memory and bandwidth sensors for Intel discrete GPUs (#2067)

- Add GPU Memory Total, Used, Free sensors (SmallData type, in MB)
- Add GPU Memory load sensor (percentage)
- Add GPU Memory Read/Write bandwidth sensors (Throughput type, in B/s)
- Use D3DDisplayDevice API to query VRAM usage via Windows kernel
- Match Intel discrete GPUs using PCI vendor/device IDs
- Include fallback detection for Intel GPUs if initial matching fails
- Bandwidth sensors read from Intel GCL telemetry with unit conversion
This commit is contained in:
David Renelt
2025-11-23 13:45:55 +01:00
committed by GitHub
parent 045eca30c9
commit 4c9e9f5dfd
@@ -38,9 +38,20 @@ internal sealed class IntelDiscreteGpu : GenericGpu
private readonly Sensor _voltageCore;
private readonly Sensor _voltageMemory;
// Memory sensors
private readonly Sensor _memoryFree;
private readonly Sensor _memoryTotal;
private readonly Sensor _memoryUsed;
private readonly Sensor _memoryLoad;
// Bandwidth sensors
private readonly Sensor _memoryBandwidthRead;
private readonly Sensor _memoryBandwidthWrite;
// Timestamps
private double _currentTimestamp = double.NaN;
private string _deviceId;
private string _d3dDeviceId;
// Intel GCL properties and data
private readonly IntelGcl.ctl_device_adapter_handle_t _handle;
@@ -54,6 +65,8 @@ internal sealed class IntelDiscreteGpu : GenericGpu
private double _lastRenderComputeActivityCounter = double.NaN;
private double _lastTimestamp = double.NaN;
private double _lastTotalCardEnergyReading = double.NaN;
private double _lastVramReadBandwidthCounter = double.NaN;
private double _lastVramWriteBandwidthCounter = double.NaN;
private IntelGcl.ctl_device_adapter_properties_t _properties;
// Telemetry data
@@ -69,6 +82,9 @@ internal sealed class IntelDiscreteGpu : GenericGpu
if (!InitializeDevice())
return;
// Try to get D3D device identifier for memory monitoring
_d3dDeviceId = GetD3DDeviceId();
// Initialize temperature sensors
_temperatureGpuCore = new Sensor("GPU Core", 0, SensorType.Temperature, this, settings);
_temperatureMemory = new Sensor("GPU Memory", 1, SensorType.Temperature, this, settings);
@@ -90,6 +106,16 @@ internal sealed class IntelDiscreteGpu : GenericGpu
_loadRenderCompute = new Sensor("GPU Render/Compute", 1, SensorType.Load, this, settings);
_loadMedia = new Sensor("GPU Media", 2, SensorType.Load, this, settings);
// Initialize memory sensors
_memoryFree = new Sensor("GPU Memory Free", 0, SensorType.SmallData, this, settings);
_memoryUsed = new Sensor("GPU Memory Used", 1, SensorType.SmallData, this, settings);
_memoryTotal = new Sensor("GPU Memory Total", 2, SensorType.SmallData, this, settings);
_memoryLoad = new Sensor("GPU Memory", 3, SensorType.Load, this, settings);
// Initialize bandwidth sensors
_memoryBandwidthRead = new Sensor("GPU Memory Read", 0, SensorType.Throughput, this, settings);
_memoryBandwidthWrite = new Sensor("GPU Memory Write", 1, SensorType.Throughput, this, settings);
// Initialize fan sensors based on available fans
int fanCount = (int)GetFanCount();
_fans = new Sensor[fanCount];
@@ -164,6 +190,34 @@ internal sealed class IntelDiscreteGpu : GenericGpu
return false;
}
private string GetD3DDeviceId()
{
// Try to find matching D3D device using PCI device ID
string[] deviceIdentifiers = D3DDisplayDevice.GetDeviceIdentifiers();
if (deviceIdentifiers == null || deviceIdentifiers.Length == 0)
return null;
// Intel vendor ID is 0x8086
string vendorPattern = $"VEN_{_properties.pci_vendor_id:X}";
string devicePattern = $"DEV_{_properties.pci_device_id:X}";
foreach (string deviceIdentifier in deviceIdentifiers)
{
// Check if this device matches Intel vendor and device IDs
if (deviceIdentifier.IndexOf(vendorPattern, StringComparison.OrdinalIgnoreCase) != -1 &&
deviceIdentifier.IndexOf(devicePattern, StringComparison.OrdinalIgnoreCase) != -1)
{
// Verify it's a valid D3D device by trying to get device info
if (D3DDisplayDevice.GetDeviceInfoByIdentifier(deviceIdentifier, out D3DDisplayDevice.D3DDeviceInfo deviceInfo))
{
return deviceIdentifier;
}
}
}
return null;
}
public override void Update()
{
if (!IsValid)
@@ -196,6 +250,19 @@ internal sealed class IntelDiscreteGpu : GenericGpu
UpdateUtilizationFromActivityCounter(_telemetry.renderComputeActivityCounter, ref _lastRenderComputeActivityCounter, _loadRenderCompute);
UpdateUtilizationFromActivityCounter(_telemetry.mediaActivityCounter, ref _lastMediaActivityCounter, _loadMedia);
// Try to get D3D device ID if we haven't found it yet
if (string.IsNullOrEmpty(_d3dDeviceId))
{
_d3dDeviceId = GetD3DDeviceId();
}
// Update VRAM memory sensors (using D3D API)
UpdateMemorySensors();
// Update VRAM bandwidth sensors
UpdateBandwidthFromCounter(_telemetry.vramReadBandwidth, ref _lastVramReadBandwidthCounter, _memoryBandwidthRead);
UpdateBandwidthFromCounter(_telemetry.vramWriteBandwidth, ref _lastVramWriteBandwidthCounter, _memoryBandwidthWrite);
// Update fan sensors
UpdateFanSpeeds(_fans);
}
@@ -425,4 +492,97 @@ internal sealed class IntelDiscreteGpu : GenericGpu
lastActivityReading = currentActivity;
}
private void UpdateMemorySensors()
{
if (string.IsNullOrEmpty(_d3dDeviceId))
{
// Fallback: Try to find any Intel discrete GPU
string[] deviceIdentifiers = D3DDisplayDevice.GetDeviceIdentifiers();
if (deviceIdentifiers != null)
{
foreach (string deviceId in deviceIdentifiers)
{
if (deviceId.IndexOf("VEN_8086", StringComparison.OrdinalIgnoreCase) != -1)
{
if (D3DDisplayDevice.GetDeviceInfoByIdentifier(deviceId, out D3DDisplayDevice.D3DDeviceInfo testInfo))
{
if (testInfo.GpuDedicatedLimit > 0 && !testInfo.Integrated)
{
_d3dDeviceId = deviceId;
break;
}
}
}
}
}
if (string.IsNullOrEmpty(_d3dDeviceId))
return;
}
if (D3DDisplayDevice.GetDeviceInfoByIdentifier(_d3dDeviceId, out D3DDisplayDevice.D3DDeviceInfo deviceInfo))
{
// Get dedicated video memory (VRAM) usage
ulong totalBytes = deviceInfo.GpuDedicatedLimit;
ulong usedBytes = deviceInfo.GpuDedicatedUsed;
ulong freeBytes = totalBytes > usedBytes ? totalBytes - usedBytes : 0;
if (totalBytes > 0)
{
// Convert bytes to MB for display
_memoryTotal.Value = totalBytes / (1024.0f * 1024.0f);
ActivateSensor(_memoryTotal);
_memoryUsed.Value = usedBytes / (1024.0f * 1024.0f);
ActivateSensor(_memoryUsed);
_memoryFree.Value = freeBytes / (1024.0f * 1024.0f);
ActivateSensor(_memoryFree);
// Calculate load percentage
_memoryLoad.Value = (float)((double)usedBytes / totalBytes * 100.0);
ActivateSensor(_memoryLoad);
}
}
}
private void UpdateBandwidthFromCounter(IntelGcl.ctl_oc_telemetry_item_t bandwidthItem, ref double lastBandwidthReading, Sensor bandwidthSensor)
{
if (!IsValid || bandwidthSensor == null)
return;
// If the telemetry item directly provides bandwidth value (not a counter)
if (bandwidthItem.bSupported)
{
double bandwidthValue = GetTelemetryValue(bandwidthItem);
if (!double.IsNaN(bandwidthValue) && bandwidthValue >= 0)
{
// Bandwidth is typically in GB/s or MB/s, convert to B/s for Throughput sensor
// Check the units to determine if conversion is needed
if (bandwidthItem.units == IntelGcl.ctl_units_t.CTL_UNITS_BANDWIDTH_MBPS)
{
// Convert MB/s to B/s (multiply by 1024*1024)
bandwidthValue = bandwidthValue * 1024.0 * 1024.0;
}
else if (bandwidthItem.units == IntelGcl.ctl_units_t.CTL_UNITS_MEM_SPEED_GBPS)
{
// Convert GB/s to B/s (multiply by 1024*1024*1024)
bandwidthValue = bandwidthValue * 1024.0 * 1024.0 * 1024.0;
}
bandwidthSensor.Value = (float)bandwidthValue;
ActivateSensor(bandwidthSensor);
}
else
{
bandwidthSensor.Value = null;
}
}
else
{
bandwidthSensor.Value = null;
}
}
}