Add IGCL telemetry sensors (temperature, clock, voltage) for Intel integrated GPUs (#2218)

* fix: skip integrated GPU in IGCL enumeration to prevent duplicate hardware entries

IGCL's ctlEnumerateDevices returns all Intel graphics devices including
the integrated GPU, but IntelDiscreteGpu was created for every device
without filtering. This caused the iGPU to appear twice: once from the
IGCL path (as IntelDiscreteGpu) and once from the D3D path (as
IntelIntegratedGpu), with overlapping sensors.

Check the CTL_ADAPTER_PROPERTIES_FLAG_INTEGRATED flag on each device's
graphics_adapter_properties and skip integrated adapters, since they are
already handled by IntelIntegratedGpu via D3D enumeration.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add IGCL-based telemetry sensors for Intel integrated GPUs

Use Intel Graphics Control Library (IGCL) to expose temperature, clock
frequency, and voltage sensors for the integrated GPU via the safe
user-mode ctlPowerTelemetryGet API. Each sensor is only activated if
the driver reports it as supported, so this degrades gracefully on
hardware/drivers that don't expose specific telemetry items.

This mirrors the approach already used by IntelDiscreteGpu, bringing
parity between discrete and integrated Intel GPU monitoring.

Depends on: fix/intel-igpu-deduplication

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
kyranmcdonnell
2026-02-14 12:10:25 +01:00
committed by GitHub
co-authored by Cursor
parent 1564a69c52
commit 9158b5ac75
2 changed files with 131 additions and 3 deletions
@@ -1,6 +1,8 @@
using System;
using System;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using LibreHardwareMonitor.Interop;
using LibreHardwareMonitor.PawnIo;
using Microsoft.Win32;
@@ -20,21 +22,48 @@ internal class IntelIntegratedGpu : GenericGpu
private readonly long[] _nodeUsagePrevValue;
private readonly Sensor _powerSensor;
private readonly Sensor _sharedMemoryUsage;
private readonly Sensor _gtCoresTemperature;
private readonly Sensor _gpuClockFrequency;
private readonly Sensor _gpuVoltage;
private uint _lastEnergyConsumed;
private DateTime _lastEnergyTime;
private readonly IntelMsr _pawnModule;
private readonly IntelGcl.ctl_device_adapter_handle_t? _igclHandle;
public IntelIntegratedGpu(Cpu.IntelCpu intelCpu, string deviceId, D3DDisplayDevice.D3DDeviceInfo deviceInfo, ISettings settings)
: base(GetName(deviceId),
new Identifier("gpu-intel-integrated", deviceId.ToString(CultureInfo.InvariantCulture)),
settings)
{
_pawnModule = new IntelMsr();
_deviceId = deviceId;
_igclHandle = FindIgclHandle();
if (_igclHandle.HasValue && TryReadTelemetry(_igclHandle.Value, out IntelGcl.ctl_power_telemetry_t probeTelemetry))
{
if (probeTelemetry.gpuCurrentTemperature.bSupported)
{
_gtCoresTemperature = new Sensor("GPU Core", 0, SensorType.Temperature, this, settings);
ActivateSensor(_gtCoresTemperature);
}
if (probeTelemetry.gpuCurrentClockFrequency.bSupported)
{
_gpuClockFrequency = new Sensor("GPU Core", 0, SensorType.Clock, this, settings);
ActivateSensor(_gpuClockFrequency);
}
if (probeTelemetry.gpuVoltage.bSupported)
{
_gpuVoltage = new Sensor("GPU Core", 0, SensorType.Voltage, this, settings);
ActivateSensor(_gpuVoltage);
}
}
int memorySensorIndex = 0;
if (deviceInfo.GpuDedicatedLimit > 0)
@@ -82,6 +111,23 @@ internal class IntelIntegratedGpu : GenericGpu
public override void Update()
{
// Update IGCL telemetry (temperature, clock, voltage).
if (_igclHandle.HasValue && (_gtCoresTemperature != null || _gpuClockFrequency != null || _gpuVoltage != null))
{
if (TryReadTelemetry(_igclHandle.Value, out IntelGcl.ctl_power_telemetry_t telemetry))
{
UpdateSensorFromTelemetry(telemetry.gpuCurrentTemperature, _gtCoresTemperature);
UpdateSensorFromTelemetry(telemetry.gpuCurrentClockFrequency, _gpuClockFrequency);
UpdateSensorFromTelemetry(telemetry.gpuVoltage, _gpuVoltage);
}
else
{
if (_gtCoresTemperature != null) _gtCoresTemperature.Value = null;
if (_gpuClockFrequency != null) _gpuClockFrequency.Value = null;
if (_gpuVoltage != null) _gpuVoltage.Value = null;
}
}
if (D3DDisplayDevice.GetDeviceInfoByIdentifier(_deviceId, out D3DDisplayDevice.D3DDeviceInfo deviceInfo))
{
if (_dedicatedMemoryUsage != null)
@@ -139,6 +185,88 @@ internal class IntelIntegratedGpu : GenericGpu
_pawnModule.Close();
}
private static bool TryReadTelemetry(IntelGcl.ctl_device_adapter_handle_t handle, out IntelGcl.ctl_power_telemetry_t telemetry)
{
telemetry = new IntelGcl.ctl_power_telemetry_t
{
Size = (uint)Marshal.SizeOf<IntelGcl.ctl_power_telemetry_t>(),
Version = 1,
psu = new IntelGcl.ctl_psu_info_t[IntelGcl.CTL_PSU_COUNT],
fanSpeed = new IntelGcl.ctl_oc_telemetry_item_t[IntelGcl.CTL_FAN_COUNT]
};
int result = IntelGcl.ctlPowerTelemetryGet(handle, ref telemetry);
return result == (int)IntelGcl.ctl_result_t.CTL_RESULT_SUCCESS;
}
private static void UpdateSensorFromTelemetry(IntelGcl.ctl_oc_telemetry_item_t telemetryItem, Sensor sensor)
{
if (sensor == null)
return;
if (telemetryItem.bSupported)
{
double value = GetTelemetryValue(telemetryItem);
sensor.Value = double.IsNaN(value) ? null : (float)value;
}
else
{
sensor.Value = null;
}
}
private static double GetTelemetryValue(IntelGcl.ctl_oc_telemetry_item_t item)
{
return item.type switch
{
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_FLOAT => item.value.datafloat,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_DOUBLE => item.value.datadouble,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_UINT32 => item.value.datau32,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_INT32 => item.value.data32,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_UINT64 => item.value.datau64,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_INT64 => item.value.data64,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_UINT16 => item.value.datau16,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_INT16 => item.value.data16,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_UINT8 => item.value.datau8,
IntelGcl.ctl_data_type_t.CTL_DATA_TYPE_INT8 => item.value.data8,
_ => double.NaN
};
}
private static IntelGcl.ctl_device_adapter_handle_t? FindIgclHandle()
{
if (!IntelGcl.IsInitialized)
return null;
IntelGcl.ctl_device_adapter_handle_t[] handles = IntelGcl.GetDeviceHandles();
foreach (IntelGcl.ctl_device_adapter_handle_t handle in handles)
{
var properties = new IntelGcl.ctl_device_adapter_properties_t
{
Size = (uint)Marshal.SizeOf<IntelGcl.ctl_device_adapter_properties_t>(),
Version = 2
};
int result = IntelGcl.ctlGetDeviceProperties(handle, ref properties);
if (result != (int)IntelGcl.ctl_result_t.CTL_RESULT_SUCCESS)
continue;
if (properties.device_type != IntelGcl.ctl_device_type_t.CTL_DEVICE_TYPE_GRAPHICS)
continue;
// Use the IGCL integrated-adapter flag — the same method IntelGpuGroup uses
// to filter iGPUs out of the discrete GPU enumeration path.
if ((properties.graphics_adapter_properties & (uint)IntelGcl.ctl_adapter_properties_flag_t.CTL_ADAPTER_PROPERTIES_FLAG_INTEGRATED) != 0)
{
return handle;
}
}
return null;
}
private static string GetName(string deviceId)
{
string path = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\" + D3DDisplayDevice.GetActualDeviceIdentifier(deviceId);
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
<Platforms>x64;x86;ARM64</Platforms>