diff --git a/Exo.Devices.NVidia/NVidiaGpuDriver.FanCoolerSensor.cs b/Exo.Devices.NVidia/NVidiaGpuDriver.FanCoolerSensor.cs new file mode 100644 index 00000000..e5ed6961 --- /dev/null +++ b/Exo.Devices.NVidia/NVidiaGpuDriver.FanCoolerSensor.cs @@ -0,0 +1,47 @@ +using Exo.Sensors; + +namespace Exo.Devices.NVidia; + +public partial class NVidiaGpuDriver +{ + private sealed class FanCoolerSensor : GroupQueriedSensor, IPolledSensor + { + 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 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; + } +} diff --git a/Exo.Devices.NVidia/NVidiaGpuDriver.FanSensor.cs b/Exo.Devices.NVidia/NVidiaGpuDriver.LegacyFanSensor.cs similarity index 73% rename from Exo.Devices.NVidia/NVidiaGpuDriver.FanSensor.cs rename to Exo.Devices.NVidia/NVidiaGpuDriver.LegacyFanSensor.cs index 44a2dcc8..5337b154 100644 --- a/Exo.Devices.NVidia/NVidiaGpuDriver.FanSensor.cs +++ b/Exo.Devices.NVidia/NVidiaGpuDriver.LegacyFanSensor.cs @@ -4,13 +4,13 @@ namespace Exo.Devices.NVidia; public partial class NVidiaGpuDriver { - private sealed class FanSensor : IPolledSensor + private sealed class LegacyFanSensor : IPolledSensor { 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; } diff --git a/Exo.Devices.NVidia/NVidiaGpuDriver.cs b/Exo.Devices.NVidia/NVidiaGpuDriver.cs index 11ba8165..96cf1b05 100644 --- a/Exo.Devices.NVidia/NVidiaGpuDriver.cs +++ b/Exo.Devices.NVidia/NVidiaGpuDriver.cs @@ -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); @@ -246,7 +249,17 @@ 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(); @@ -254,7 +267,6 @@ DeviceId deviceId } catch { - hasTachReading = false; } var thermalSensors = new NvApi.Gpu.ThermalSensor[3]; @@ -279,6 +291,7 @@ DeviceId deviceId zoneControls, lightingZones.DrainToImmutable(), hasTachReading, + fanStatuses.AsSpan(0, fanStatusCount), thermalSensors.AsSpan(0, sensorCount), clockFrequencies.AsSpan(0, clockFrequencyCount) ) @@ -298,8 +311,10 @@ DeviceId deviceId private readonly IReadOnlyCollection _lightingZoneCollection; private readonly ImmutableArray _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 _logger; @@ -329,6 +344,7 @@ private NVidiaGpuDriver NvApi.Gpu.Client.IlluminationZoneControl[] zoneControls, ImmutableArray lightingZones, bool hasTachReading, + ReadOnlySpan fanCoolerStatuses, ReadOnlySpan thermalSensors, ReadOnlySpan clockFrequencies ) : base(friendlyName, configurationKey) @@ -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]; @@ -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(this); _displayAdapterFeatures = FeatureSet.Create(this); @@ -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++; } @@ -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--; } @@ -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 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; diff --git a/Exo.Devices.NVidia/NvApi.cs b/Exo.Devices.NVidia/NvApi.cs index 7c430eae..6d23a0ff 100644 --- a/Exo.Devices.NVidia/NvApi.cs +++ b/Exo.Devices.NVidia/NvApi.cs @@ -66,6 +66,8 @@ public static class Gpu public static readonly delegate* unmanaged[Cdecl] GetAllClockFrequencies = (delegate* unmanaged[Cdecl])QueryInterface(0xdcb616c3); public static readonly delegate* unmanaged[Cdecl] GetTachReading = (delegate* unmanaged[Cdecl])QueryInterface(0x5f608315); public static readonly delegate* unmanaged[Cdecl] GetCoolerSettings = (delegate* unmanaged[Cdecl])QueryInterface(0xda141340); + //public static readonly delegate* unmanaged[Cdecl] ClientFanCoolersGetInfo = (delegate* unmanaged[Cdecl])QueryInterface(0xfb85b01e); + public static readonly delegate* unmanaged[Cdecl] ClientFanCoolersGetStatus = (delegate* unmanaged[Cdecl])QueryInterface(0x35aed5e8); } public static class System @@ -880,6 +882,46 @@ internal unsafe struct UtilizationPeriodicCallbackSettings public delegate* unmanaged[Cdecl] 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; + } } } @@ -1194,7 +1236,7 @@ public unsafe int GetThermalSettings(Span thermalSensors) var thermalSettings = new Gpu.ThermalSettings { Version = StructVersion(Gpu.ThermalSettingsVersion) }; ValidateResult(Functions.Gpu.GetThermalSettings(_handle, 15, &thermalSettings)); if (thermalSettings.Count > 3) throw new InvalidOperationException("Invalid thermal reading count."); - ((ReadOnlySpan)thermalSettings.Sensors).CopyTo(thermalSensors); + ((ReadOnlySpan)thermalSettings.Sensors)[..(int)thermalSettings.Count].CopyTo(thermalSensors); return (int)thermalSettings.Count; } @@ -1211,7 +1253,7 @@ public unsafe int GetCoolerSettings(Span coolers) var coolerSettings = new Gpu.CoolerSettings { Version = StructVersion(Gpu.CoolerSettingsVersion) }; ValidateResult(Functions.Gpu.GetCoolerSettings(_handle, (uint)Gpu.CoolerTarget.All, &coolerSettings)); if (coolerSettings.Count > 3) throw new InvalidOperationException("Invalid cooler count."); - ((ReadOnlySpan)coolerSettings.Coolers).CopyTo(coolers); + ((ReadOnlySpan)coolerSettings.Coolers)[..(int)coolerSettings.Count].CopyTo(coolers); return (int)coolerSettings.Count; } @@ -1238,6 +1280,21 @@ public unsafe uint GetTachReading() return reading; } + public unsafe int GetFanCoolersStatus(Span fanCoolers) + { + var status = new Gpu.Client.FanCoolersStatus { Version = StructVersion(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)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) { @@ -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; } + } } diff --git a/Exo.Settings.Ui/GuidDatabases.cs b/Exo.Settings.Ui/GuidDatabases.cs index 51cdc434..d591ed87 100644 --- a/Exo.Settings.Ui/GuidDatabases.cs +++ b/Exo.Settings.Ui/GuidDatabases.cs @@ -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" },