Skip to content

Commit

Permalink
✨ Add NVIDIA fan speed sensors based on NvAPI_GPU_ClientFanCoolersGet…
Browse files Browse the repository at this point in the history
…Status.
  • Loading branch information
hexawyz committed Apr 20, 2024
1 parent ecd4bc5 commit 553838b
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 14 deletions.
47 changes: 47 additions & 0 deletions Exo.Devices.NVidia/NVidiaGpuDriver.FanCoolerSensor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Exo.Sensors;

namespace Exo.Devices.NVidia;

public partial class NVidiaGpuDriver
{
private sealed class FanCoolerSensor : GroupQueriedSensor, IPolledSensor<uint>
{
private static Guid GetGuidForFan(uint fanId)
=> fanId switch
{
1 => Fan1SpeedSensorId,
2 => Fan2SpeedSensorId,
_ => throw new InvalidOperationException("Unsupported fan.")
};

private uint _currentValue;
private readonly byte _fanId;

public Guid SensorId => GetGuidForFan(_fanId);

public FanCoolerSensor(NvApi.PhysicalGpu gpu, NvApi.GpuFanStatus fanStatus)
: base(gpu)
{
_currentValue = fanStatus.SpeedInRotationsPerMinute;
_fanId = (byte)fanStatus.FanId;
}

public uint? ScaleMinimumValue => null;
public uint? ScaleMaximumValue => null;

public SensorUnit Unit => SensorUnit.RotationsPerMinute;

public ValueTask<uint> GetValueAsync(CancellationToken cancellationToken)
=> ValueTask.FromResult(GroupedQueryMode == GroupedQueryMode.Enabled ? _currentValue : QueryValue());

public bool TryGetLastValue(out uint lastValue)
{
lastValue = _currentValue;
return true;
}

private uint QueryValue() => throw new NotSupportedException("Non-grouped queries are not supported for this sensor.");

public void OnValueRead(uint value) => _currentValue = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ namespace Exo.Devices.NVidia;

public partial class NVidiaGpuDriver
{
private sealed class FanSensor : IPolledSensor<uint>
private sealed class LegacyFanSensor : IPolledSensor<uint>
{
private readonly NvApi.PhysicalGpu _gpu;

public Guid SensorId => FanSpeedSensorId;
public Guid SensorId => LegacyFanSpeedSensorId;

public FanSensor(NvApi.PhysicalGpu gpu)
public LegacyFanSensor(NvApi.PhysicalGpu gpu)
{
_gpu = gpu;
}
Expand Down
76 changes: 67 additions & 9 deletions Exo.Devices.NVidia/NVidiaGpuDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ private abstract class GroupQueriedSensor
new(0x82658966, 0x39CD, 0x40B7, 0xBA, 0x7C, 0x87, 0xB2, 0x7F, 0xE4, 0xAA, 0x99),
];

private static readonly Guid FanSpeedSensorId = new(0x3428225A, 0x6BE4, 0x44AF, 0xB1, 0xCD, 0x80, 0x0A, 0x55, 0xF9, 0x43, 0x2F);
private static readonly Guid LegacyFanSpeedSensorId = new(0x3428225A, 0x6BE4, 0x44AF, 0xB1, 0xCD, 0x80, 0x0A, 0x55, 0xF9, 0x43, 0x2F);

private static readonly Guid Fan1SpeedSensorId = new(0xFCF6D2E1, 0x9048, 0x4E87, 0xAC, 0x9F, 0x91, 0xE2, 0x50, 0xE1, 0x21, 0x8B);
private static readonly Guid Fan2SpeedSensorId = new(0xBE0F57CB, 0xCD6D, 0x4422, 0xA0, 0x24, 0xB2, 0x8F, 0xF4, 0x25, 0xE9, 0x03);

private static readonly Guid GraphicsUtilizationSensorId = new(0x005F94DD, 0x09F5, 0x46D3, 0x99, 0x02, 0xE1, 0x5D, 0x6A, 0x19, 0xD8, 0x24);
private static readonly Guid FrameBufferUtilizationSensorId = new(0xBF9AAD1D, 0xE013, 0x4178, 0x97, 0xB3, 0x42, 0x20, 0xD2, 0x6C, 0xBE, 0x71);
Expand Down Expand Up @@ -246,15 +249,24 @@ DeviceId deviceId
}
}

bool hasTachReading;
var fanStatuses = new NvApi.GpuFanStatus[32];
int fanStatusCount = 0;
try
{
fanStatusCount = foundGpu.GetFanCoolersStatus(fanStatuses);
}
catch
{
}

bool hasTachReading = false;
try
{
_ = foundGpu.GetTachReading();
hasTachReading = true;
}
catch
{
hasTachReading = false;
}

var thermalSensors = new NvApi.Gpu.ThermalSensor[3];
Expand All @@ -279,6 +291,7 @@ DeviceId deviceId
zoneControls,
lightingZones.DrainToImmutable(),
hasTachReading,
fanStatuses.AsSpan(0, fanStatusCount),
thermalSensors.AsSpan(0, sensorCount),
clockFrequencies.AsSpan(0, clockFrequencyCount)
)
Expand All @@ -298,8 +311,10 @@ DeviceId deviceId
private readonly IReadOnlyCollection<ILightingZone> _lightingZoneCollection;
private readonly ImmutableArray<FeatureSetDescription> _featureSets;
private readonly UtilizationWatcher _utilizationWatcher;
private readonly FanCoolerSensor?[] _fanCoolerSensors;
private readonly ThermalTargetSensor[] _thermalTargetSensors;
private readonly ClockSensor?[] _clockSensors;
private int _fanCoolerGroupQueriedSensorCount;
private int _thermalGroupQueriedSensorCount;
private int _clockGroupQueriedSensorCount;
private readonly ILogger<NVidiaGpuDriver> _logger;
Expand Down Expand Up @@ -329,6 +344,7 @@ private NVidiaGpuDriver
NvApi.Gpu.Client.IlluminationZoneControl[] zoneControls,
ImmutableArray<LightingZone> lightingZones,
bool hasTachReading,
ReadOnlySpan<NvApi.GpuFanStatus> fanCoolerStatuses,
ReadOnlySpan<NvApi.Gpu.ThermalSensor> thermalSensors,
ReadOnlySpan<NvApi.GpuClockFrequency> clockFrequencies
) : base(friendlyName, configurationKey)
Expand All @@ -346,7 +362,7 @@ private NVidiaGpuDriver
sensors.Add(_utilizationWatcher.VideoSensor);
if (hasTachReading)
{
sensors.Add(new FanSensor(gpu));
sensors.Add(new LegacyFanSensor(gpu));
}
// Nowadays, only the GPU thermal target would be returned. Usage of yet another undocumented API will be required. (To be done later)
_thermalTargetSensors = new ThermalTargetSensor[thermalSensors.Length];
Expand All @@ -355,19 +371,28 @@ private NVidiaGpuDriver
sensors.Add(_thermalTargetSensors[i] = new(_gpu, thermalSensors[i], (byte)i));
}
// Process clocks while filtering out unsupported clocks.
var clockSensors = new ClockSensor?[clockFrequencies.Length];
_clockSensors = new ClockSensor?[clockFrequencies.Length];
for (int i = 0; i < clockFrequencies.Length; i++)
{
if (Enum.IsDefined(clockFrequencies[i].Clock))
{
sensors.Add(clockSensors[i] = new ClockSensor(_gpu, clockFrequencies[i]));
sensors.Add(_clockSensors[i] = new ClockSensor(_gpu, clockFrequencies[i]));
}
else
{
_logger.GpuClockNotSupported(clockFrequencies[i].Clock);
}
}
_clockSensors = clockSensors;
_fanCoolerSensors = new FanCoolerSensor?[fanCoolerStatuses.Length];
for (int i = 0; i < fanCoolerStatuses.Length; i++)
{
var fanCoolerStatus = fanCoolerStatuses[i];
// This API is not documented, so we don't really know all the details. Only fans with an ID that has been seen in the wild will be exposed as sensors. (Similar as for clocks above)
if (fanCoolerStatus.FanId is 1 or 2)
{
sensors.Add(_fanCoolerSensors[i] = new(_gpu, fanCoolerStatus));
}
}
_sensors = sensors.DrainToImmutable();
_genericFeatures = FeatureSet.Create<IGenericDeviceFeature, NVidiaGpuDriver, IDeviceIdFeature>(this);
_displayAdapterFeatures = FeatureSet.Create<IDisplayAdapterDeviceFeature, NVidiaGpuDriver, IDisplayAdapterI2CBusProviderFeature>(this);
Expand Down Expand Up @@ -444,7 +469,11 @@ void ISensorsGroupedQueryFeature.AddSensor(IPolledSensor sensor)
if (!s.IsGroupQueryEnabled)
{
s.IsGroupQueryEnabled = true;
if (s is ThermalTargetSensor)
if (s is FanCoolerSensor)
{
_fanCoolerGroupQueriedSensorCount++;
}
else if (s is ThermalTargetSensor)
{
_thermalGroupQueriedSensorCount++;
}
Expand All @@ -461,7 +490,11 @@ void ISensorsGroupedQueryFeature.RemoveSensor(IPolledSensor sensor)
if (s.IsGroupQueryEnabled)
{
s.IsGroupQueryEnabled = false;
if (s is ThermalTargetSensor)
if (s is FanCoolerSensor)
{
_fanCoolerGroupQueriedSensorCount--;
}
else if (s is ThermalTargetSensor)
{
_thermalGroupQueriedSensorCount--;
}
Expand All @@ -474,11 +507,36 @@ void ISensorsGroupedQueryFeature.RemoveSensor(IPolledSensor sensor)

ValueTask ISensorsGroupedQueryFeature.QueryValuesAsync(CancellationToken cancellationToken)
{
QueryFanCoolerSensors();
QueryThermalSensors();
QueryClockSensors();
return ValueTask.CompletedTask;
}

private void QueryFanCoolerSensors()
{
if (_fanCoolerGroupQueriedSensorCount == 0) return;

Span<NvApi.GpuFanStatus> fanStatuses = stackalloc NvApi.GpuFanStatus[32];

try
{
int fanStatusCount = _gpu.GetFanCoolersStatus(fanStatuses);

// The number of sensors is not supposed to change, and it will probably never happen, but we don't want to throw an exception here, as other sensor systems have to be queried.
if (fanStatusCount == _fanCoolerSensors.Length)
{
for (int i = 0; i < fanStatusCount; i++)
{
_fanCoolerSensors[i]?.OnValueRead(fanStatuses[i].SpeedInRotationsPerMinute);
}
}
}
catch (Exception ex)
{
}
}

private void QueryThermalSensors()
{
if (_thermalGroupQueriedSensorCount == 0) return;
Expand Down
80 changes: 78 additions & 2 deletions Exo.Devices.NVidia/NvApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public static class Gpu
public static readonly delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.ClockFrequencies*, uint> GetAllClockFrequencies = (delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.ClockFrequencies*, uint>)QueryInterface(0xdcb616c3);
public static readonly delegate* unmanaged[Cdecl]<nint, uint*, uint> GetTachReading = (delegate* unmanaged[Cdecl]<nint, uint*, uint>)QueryInterface(0x5f608315);
public static readonly delegate* unmanaged[Cdecl]<nint, uint, NvApi.Gpu.CoolerSettings*, uint> GetCoolerSettings = (delegate* unmanaged[Cdecl]<nint, uint, NvApi.Gpu.CoolerSettings*, uint>)QueryInterface(0xda141340);
//public static readonly delegate* unmanaged[Cdecl]<nint, uint, NvApi.Gpu.CoolerSettings*, uint> ClientFanCoolersGetInfo = (delegate* unmanaged[Cdecl]<nint, uint, NvApi.Gpu.CoolerSettings*, uint>)QueryInterface(0xfb85b01e);
public static readonly delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.Client.FanCoolersStatus*, uint> ClientFanCoolersGetStatus = (delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.Client.FanCoolersStatus*, uint>)QueryInterface(0x35aed5e8);
}

public static class System
Expand Down Expand Up @@ -880,6 +882,46 @@ internal unsafe struct UtilizationPeriodicCallbackSettings
public delegate* unmanaged[Cdecl]<nint, CallbackUtilizationData*, void> Callback;
private readonly ByteArray64 _reserved;
}

internal struct FanCoolerStatus
{
public uint FanId;
public uint SpeedInRotationsPerMinute;
public uint MinimumPower;
public uint MaximumPower;
public uint CurrentPower;
public uint Unknown05;
public uint Unknown06;
public uint Unknown07;
public uint Unknown08;
public uint Unknown09;
public uint Unknown10;
public uint Unknown11;
public uint Unknown12;
}

[InlineArray(32)]
internal struct FanCoolerStatusArray
{
private FanCoolerStatus _element0;
}

internal struct FanCoolersStatus
{
public uint Version;
public uint Count;

public uint Unknown0;
public uint Unknown1;
public uint Unknown2;
public uint Unknown3;
public uint Unknown4;
public uint Unknown5;
public uint Unknown6;
public uint Unknown7;

public FanCoolerStatusArray FanCoolers;
}
}
}

Expand Down Expand Up @@ -1194,7 +1236,7 @@ public unsafe int GetThermalSettings(Span<Gpu.ThermalSensor> thermalSensors)
var thermalSettings = new Gpu.ThermalSettings { Version = StructVersion<Gpu.ThermalSettings>(Gpu.ThermalSettingsVersion) };
ValidateResult(Functions.Gpu.GetThermalSettings(_handle, 15, &thermalSettings));
if (thermalSettings.Count > 3) throw new InvalidOperationException("Invalid thermal reading count.");
((ReadOnlySpan<Gpu.ThermalSensor>)thermalSettings.Sensors).CopyTo(thermalSensors);
((ReadOnlySpan<Gpu.ThermalSensor>)thermalSettings.Sensors)[..(int)thermalSettings.Count].CopyTo(thermalSensors);
return (int)thermalSettings.Count;
}

Expand All @@ -1211,7 +1253,7 @@ public unsafe int GetCoolerSettings(Span<Gpu.CoolerInformation> coolers)
var coolerSettings = new Gpu.CoolerSettings { Version = StructVersion<Gpu.CoolerSettings>(Gpu.CoolerSettingsVersion) };
ValidateResult(Functions.Gpu.GetCoolerSettings(_handle, (uint)Gpu.CoolerTarget.All, &coolerSettings));
if (coolerSettings.Count > 3) throw new InvalidOperationException("Invalid cooler count.");
((ReadOnlySpan<Gpu.CoolerInformation>)coolerSettings.Coolers).CopyTo(coolers);
((ReadOnlySpan<Gpu.CoolerInformation>)coolerSettings.Coolers)[..(int)coolerSettings.Count].CopyTo(coolers);
return (int)coolerSettings.Count;
}

Expand All @@ -1238,6 +1280,21 @@ public unsafe uint GetTachReading()
return reading;
}

public unsafe int GetFanCoolersStatus(Span<GpuFanStatus> fanCoolers)
{
var status = new Gpu.Client.FanCoolersStatus { Version = StructVersion<Gpu.Client.FanCoolersStatus>(1) };
ValidateResult(Functions.Gpu.ClientFanCoolersGetStatus(_handle, &status));
if (status.Count > 32) throw new InvalidOperationException("Invalid fan cooler count.");
if (fanCoolers.Length < status.Count) throw new ArgumentException("Provided storage is not large enough.");
var items = ((ReadOnlySpan<Gpu.Client.FanCoolerStatus>)status.FanCoolers)[..(int)status.Count];
for (int i = 0; i < status.Count; i++)
{
var item = items[i];
fanCoolers[i] = new GpuFanStatus(item.FanId, item.SpeedInRotationsPerMinute, item.MinimumPower, item.MaximumPower, item.CurrentPower);
}
return (int)status.Count;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void OnUtilizationUpdate(nint physicalGpuHandle, Gpu.Client.CallbackUtilizationData* data)
{
Expand Down Expand Up @@ -1340,4 +1397,23 @@ public GpuClockFrequency(Gpu.PublicClock clock, uint frequencyInKiloHertz)
public Gpu.PublicClock Clock { get; }
public uint FrequencyInKiloHertz { get; }
}

public readonly struct GpuFanStatus
{
public GpuFanStatus(uint fanId, uint speedInRotationsPerMinute, uint minimumPower, uint maximumPower, uint currentPower)
{
FanId = fanId;
SpeedInRotationsPerMinute = speedInRotationsPerMinute;
MinimumPower = minimumPower;
MaximumPower = maximumPower;
CurrentPower = currentPower;
}

// Unsure whether this is an index or an enumeration like ThermalTarget, etc. Must be conservative when using that value.
public uint FanId { get; }
public uint SpeedInRotationsPerMinute { get; }
public uint MinimumPower { get; }
public uint MaximumPower { get; }
public uint CurrentPower { get; }
}
}
2 changes: 2 additions & 0 deletions Exo.Settings.Ui/GuidDatabases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ internal static class SensorDatabase
{ new(0x582060CE, 0xE985, 0x41F0, 0x95, 0x2B, 0x36, 0x7F, 0xDD, 0x3A, 0x5B, 0x40), "3.3V Power" },

{ new(0x3428225A, 0x6BE4, 0x44AF, 0xB1, 0xCD, 0x80, 0x0A, 0x55, 0xF9, 0x43, 0x2F), "Fan Speed" },
{ new(0xFCF6D2E1, 0x9048, 0x4E87, 0xAC, 0x9F, 0x91, 0xE2, 0x50, 0xE1, 0x21, 0x8B), "Fan 1 Speed" },
{ new(0xBE0F57CB, 0xCD6D, 0x4422, 0xA0, 0x24, 0xB2, 0x8F, 0xF4, 0x25, 0xE9, 0x03), "Fan 2 Speed" },

{ new(0x005F94DD, 0x09F5, 0x46D3, 0x99, 0x02, 0xE1, 0x5D, 0x6A, 0x19, 0xD8, 0x24), "Graphics" },
{ new(0xBF9AAD1D, 0xE013, 0x4178, 0x97, 0xB3, 0x42, 0x20, 0xD2, 0x6C, 0xBE, 0x71), "Frame Buffer" },
Expand Down

0 comments on commit 553838b

Please sign in to comment.