Skip to content

Commit

Permalink
✨ Add NVIDIA clock frequency sensors.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Apr 20, 2024
1 parent 2fe588e commit 3c1c4b9
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 6 deletions.
4 changes: 4 additions & 0 deletions Exo.Core/Sensors/ISensor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ public readonly struct SensorUnit
public static readonly SensorUnit Celsius = new("°C");
public static readonly SensorUnit Fahrenheits = new("°F");
public static readonly SensorUnit RotationsPerMinute = new("RPM");
public static readonly SensorUnit Hertz = new("Hz");
public static readonly SensorUnit KiloHertz = new("kHz");
public static readonly SensorUnit MegaHertz = new("MHz");
public static readonly SensorUnit GigaHertz = new("GHz");

public string Symbol { get; }

Expand Down
10 changes: 10 additions & 0 deletions Exo.Devices.NVidia/LoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ public static partial void IlluminationZoneUnknownType
this ILogger logger,
byte illuminationZoneIndex
);

[LoggerMessage(EventId = 10201,
EventName = "GpuClockNotSupported",
Level = LogLevel.Warning,
Message = "Clock {Clock} is not supported. It will not be exposed as a sensor.")]
public static partial void GpuClockNotSupported
(
this ILogger logger,
NvApi.Gpu.PublicClock clock
);
}
49 changes: 49 additions & 0 deletions Exo.Devices.NVidia/NVidiaGpuDriver.ClockSensor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Exo.Sensors;

namespace Exo.Devices.NVidia;

public partial class NVidiaGpuDriver
{
private sealed class ClockSensor : GroupQueriedSensor, IPolledSensor<uint>
{
private static Guid GetGuidForClock(NvApi.Gpu.PublicClock clock)
=> clock switch
{
NvApi.Gpu.PublicClock.Graphics => GraphicsFrequencySensorId,
NvApi.Gpu.PublicClock.Memory => MemoryFrequencySensorId,
NvApi.Gpu.PublicClock.Processor => ProcessorFrequencySensorId,
NvApi.Gpu.PublicClock.Video => VideoFrequencySensorId,
_ => throw new InvalidOperationException("Unsupported clock.")
};

private uint _currentValue;
private readonly byte _clock;

public Guid SensorId => GetGuidForClock((NvApi.Gpu.PublicClock)_clock);

public ClockSensor(NvApi.PhysicalGpu gpu, NvApi.GpuClockFrequency clock)
: base(gpu)
{
_currentValue = (uint)clock.FrequencyInKiloHertz;
_clock = (byte)clock.Clock;
}

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

public SensorUnit Unit => SensorUnit.KiloHertz;

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;
}
}
68 changes: 63 additions & 5 deletions Exo.Devices.NVidia/NVidiaGpuDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ private abstract class GroupQueriedSensor
private static readonly Guid PowerSupplyThermalSensorId = new(0x73356F82, 0x4194, 0x46DF, 0x9F, 0x9F, 0x25, 0x19, 0x21, 0x01, 0x5F, 0x81);
private static readonly Guid BoardThermalSensorId = new(0x359E57BE, 0x577D, 0x46ED, 0xA0, 0x9A, 0xD3, 0xA4, 0x4B, 0x95, 0xCA, 0xE8);

private static readonly Guid GraphicsFrequencySensorId = new(0xCD415440, 0xFB17, 0x441D, 0xA6, 0x4F, 0x6C, 0x62, 0x73, 0x34, 0x90, 0x00);
private static readonly Guid MemoryFrequencySensorId = new(0xDE435007, 0xCCD1, 0x414E, 0x8C, 0xF1, 0x22, 0x0F, 0xCC, 0xE4, 0xD0, 0x99);
private static readonly Guid ProcessorFrequencySensorId = new(0x6E1311E3, 0xD1FB, 0x4555, 0x8C, 0xD9, 0xD0, 0xF3, 0xF6, 0x43, 0x31, 0xEB);
private static readonly Guid VideoFrequencySensorId = new(0xD9272343, 0x5AC4, 0x4CD8, 0x9F, 0x7A, 0x02, 0xE1, 0x30, 0xC1, 0x5F, 0xD3);

[DiscoverySubsystem<PciDiscoverySubsystem>]
[DeviceInterfaceClass(DeviceInterfaceClass.DisplayAdapter)]
[DeviceInterfaceClass(DeviceInterfaceClass.DisplayDeviceArrival)]
Expand Down Expand Up @@ -242,6 +247,9 @@ DeviceId deviceId
var thermalSensors = new NvApi.Gpu.ThermalSensor[3];
int sensorCount = foundGpu.GetThermalSettings(thermalSensors);

var clockFrequencies = new NvApi.GpuClockFrequency[32];
int clockFrequencyCount = foundGpu.GetClockFrequencies(NvApi.Gpu.ClockType.Current, clockFrequencies);

return new
(
new DriverCreationResult<SystemDevicePath>
Expand All @@ -257,7 +265,8 @@ DeviceId deviceId
@lock,
zoneControls,
lightingZones.DrainToImmutable(),
thermalSensors.AsSpan(0, sensorCount)
thermalSensors.AsSpan(0, sensorCount),
clockFrequencies.AsSpan(0, clockFrequencyCount)
)
)
);
Expand All @@ -276,7 +285,9 @@ DeviceId deviceId
private readonly ImmutableArray<FeatureSetDescription> _featureSets;
private readonly UtilizationWatcher _utilizationWatcher;
private readonly ThermalTargetSensor[] _thermalTargetSensors;
private readonly ClockSensor?[] _clockSensors;
private int _thermalGroupQueriedSensorCount;
private int _clockGroupQueriedSensorCount;
private readonly ILogger<NVidiaGpuDriver> _logger;

IReadOnlyCollection<ILightingZone> ILightingControllerFeature.LightingZones => _lightingZoneCollection;
Expand All @@ -303,7 +314,8 @@ private NVidiaGpuDriver
object @lock,
NvApi.Gpu.Client.IlluminationZoneControl[] zoneControls,
ImmutableArray<LightingZone> lightingZones,
ReadOnlySpan<NvApi.Gpu.ThermalSensor> thermalSensors
ReadOnlySpan<NvApi.Gpu.ThermalSensor> thermalSensors,
ReadOnlySpan<NvApi.GpuClockFrequency> clockFrequencies
) : base(friendlyName, configurationKey)
{
_logger = logger;
Expand All @@ -313,7 +325,7 @@ private NVidiaGpuDriver
_gpu = gpu;
_lock = @lock;
_utilizationWatcher = new(gpu);
var sensors = ImmutableArray.CreateBuilder<ISensor>();
var sensors = ImmutableArray.CreateBuilder<ISensor>(3 + thermalSensors.Length + clockFrequencies.Length);
sensors.Add(_utilizationWatcher.GraphicsSensor);
sensors.Add(_utilizationWatcher.FrameBufferSensor);
sensors.Add(_utilizationWatcher.VideoSensor);
Expand All @@ -323,6 +335,20 @@ 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];
for (int i = 0; i < clockFrequencies.Length; i++)
{
if (Enum.IsDefined(clockFrequencies[i].Clock))
{
sensors.Add(clockSensors[i] = new ClockSensor(_gpu, clockFrequencies[i]));
}
else
{
_logger.GpuClockNotSupported(clockFrequencies[i].Clock);
}
}
_clockSensors = clockSensors;
_sensors = sensors.DrainToImmutable();
_genericFeatures = FeatureSet.Create<IGenericDeviceFeature, NVidiaGpuDriver, IDeviceIdFeature>(this);
_displayAdapterFeatures = FeatureSet.Create<IDisplayAdapterDeviceFeature, NVidiaGpuDriver, IDisplayAdapterI2CBusProviderFeature>(this);
Expand Down Expand Up @@ -403,6 +429,10 @@ void ISensorsGroupedQueryFeature.AddSensor(IPolledSensor sensor)
{
_thermalGroupQueriedSensorCount++;
}
else if (s is ClockSensor)
{
_clockGroupQueriedSensorCount++;
}
}
}

Expand All @@ -416,12 +446,17 @@ void ISensorsGroupedQueryFeature.RemoveSensor(IPolledSensor sensor)
{
_thermalGroupQueriedSensorCount--;
}
else if (s is ClockSensor)
{
_clockGroupQueriedSensorCount--;
}
}
}

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

Expand All @@ -431,10 +466,9 @@ private void QueryThermalSensors()

Span<NvApi.Gpu.ThermalSensor> sensors = stackalloc NvApi.Gpu.ThermalSensor[3];

int thermalSensorCount;
try
{
thermalSensorCount = _gpu.GetThermalSettings(sensors);
int thermalSensorCount = _gpu.GetThermalSettings(sensors);

// 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 (thermalSensorCount == _thermalTargetSensors.Length)
Expand All @@ -449,4 +483,28 @@ private void QueryThermalSensors()
{
}
}

private void QueryClockSensors()
{
if (_clockGroupQueriedSensorCount == 0) return;

Span<NvApi.GpuClockFrequency> clockFrequencies = stackalloc NvApi.GpuClockFrequency[32];

try
{
int clockFrequencyCount = _gpu.GetClockFrequencies(NvApi.Gpu.ClockType.Current, clockFrequencies);

// 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 (clockFrequencyCount == _clockSensors.Length)
{
for (int i = 0; i < _clockSensors.Length; i++)
{
_clockSensors[i]?.OnValueRead(clockFrequencies[i].FrequencyInKiloHertz);
}
}
}
catch (Exception ex)
{
}
}
}
70 changes: 70 additions & 0 deletions Exo.Devices.NVidia/NvApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public static class Gpu
public static readonly delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.Client.IlluminationZoneControlQuery*, uint> ClientIllumZonesSetControl = (delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.Client.IlluminationZoneControlQuery*, uint>)QueryInterface(0x197d065e);
public static readonly delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.Client.UtilizationPeriodicCallbackSettings*, uint> ClientRegisterForUtilizationSampleUpdates = (delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.Client.UtilizationPeriodicCallbackSettings*, uint>)QueryInterface(0xadeeaf67);
public static readonly delegate* unmanaged[Cdecl]<nint, uint, NvApi.Gpu.ThermalSettings*, uint> GetThermalSettings = (delegate* unmanaged[Cdecl]<nint, uint, NvApi.Gpu.ThermalSettings*, uint>)QueryInterface(0xe3640a56);
public static readonly delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.ClockFrequencies*, uint> GetAllClockFrequencies = (delegate* unmanaged[Cdecl]<nint, NvApi.Gpu.ClockFrequencies*, uint>)QueryInterface(0xdcb616c3);
}

public static class System
Expand Down Expand Up @@ -286,6 +287,47 @@ internal struct IlluminationQuery
public uint Value;
}

public enum PublicClock
{
Graphics = 0,
Memory = 4,
Processor = 7,
Video = 8,
}

public enum ClockType
{
Current = 0,
Base = 1,
Boost = 2,
}

public struct ClockFrequency
{
private uint _flags;
public uint FrequencyInKiloHertz;

public bool IsPresent => (_flags & 1) != 0;
}

[InlineArray(32)]
internal struct ClockFrequencyArray
{
private ClockFrequency _element0;
}

public struct ClockFrequencies
{
public uint Version;
private uint _flags;
public ClockType ClockType
{
get => (ClockType)(_flags & 0xFU);
set => _flags = (_flags & ~0xFU) | ((uint)value & 0xFU);
}
public ClockFrequencyArray Domains;
}

public enum ThermalController
{
None = 0,
Expand Down Expand Up @@ -1060,6 +1102,22 @@ public unsafe int GetThermalSettings(Span<Gpu.ThermalSensor> thermalSensors)
return (int)thermalSettings.Count;
}

public unsafe int GetClockFrequencies(Gpu.ClockType clockType, Span<GpuClockFrequency> clockFrequencies)
{
var apiClockFrequencies = new Gpu.ClockFrequencies { Version = StructVersion<Gpu.ClockFrequencies>(3), ClockType = clockType };
ValidateResult(Functions.Gpu.GetAllClockFrequencies(_handle, &apiClockFrequencies));
int count = 0;
var domains = (ReadOnlySpan<Gpu.ClockFrequency>)apiClockFrequencies.Domains;
for (int i = 0; i < domains.Length; i++)
{
if (domains[i].IsPresent)
{
clockFrequencies[count++] = new GpuClockFrequency((Gpu.PublicClock)i, domains[i].FrequencyInKiloHertz);
}
}
return count;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void OnUtilizationUpdate(nint physicalGpuHandle, Gpu.Client.CallbackUtilizationData* data)
{
Expand Down Expand Up @@ -1150,4 +1208,16 @@ public GpuClientUtilizationData(DateTime dateTime, uint perTenThousandValue, Gpu
public uint PerTenThousandValue { get; }
public Gpu.Client.UtilizationDomain Domain { get; }
}

public readonly struct GpuClockFrequency
{
public GpuClockFrequency(Gpu.PublicClock clock, uint frequencyInKiloHertz)
{
Clock = clock;
FrequencyInKiloHertz = frequencyInKiloHertz;
}

public Gpu.PublicClock Clock { get; }
public uint FrequencyInKiloHertz { get; }
}
}
5 changes: 5 additions & 0 deletions Exo.Settings.Ui/GuidDatabases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ internal static class SensorDatabase
{ new(0x73356F82, 0x4194, 0x46DF, 0x9F, 0x9F, 0x25, 0x19, 0x21, 0x01, 0x5F, 0x81), "Power Supply Temperature" },
{ new(0x359E57BE, 0x577D, 0x46ED, 0xA0, 0x9A, 0xD3, 0xA4, 0x4B, 0x95, 0xCA, 0xE8), "Board Temperature" },

{ new(0xCD415440, 0xFB17, 0x441D, 0xA6, 0x4F, 0x6C, 0x62, 0x73, 0x34, 0x90, 0x00), "Graphics Frequency" },
{ new(0xDE435007, 0xCCD1, 0x414E, 0x8C, 0xF1, 0x22, 0x0F, 0xCC, 0xE4, 0xD0, 0x99), "Memory Frequency" },
{ new(0x6E1311E3, 0xD1FB, 0x4555, 0x8C, 0xD9, 0xD0, 0xF3, 0xF6, 0x43, 0x31, 0xEB), "Processor Frequency" },
{ new(0xD9272343, 0x5AC4, 0x4CD8, 0x9F, 0x7A, 0x02, 0xE1, 0x30, 0xC1, 0x5F, 0xD3), "Video Frequency" },

};

public static string? GetSensorDisplayName(Guid sensorId)
Expand Down
47 changes: 46 additions & 1 deletion Exo.Settings.Ui/ViewModels/SensorsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,50 @@ public NumberWithUnit(double value, string? symbol)
public double Value { get; }
public string? Symbol { get; }

public override string ToString() => Symbol is not null ? $"{Value:G3}\xA0{Symbol}" : Value.ToString("G3");
public override string ToString()
{
if (Symbol is not { } symbol) return Value.ToString("G3");

var value = Value;
if (value > 1000)
{
if (symbol == "Hz")
{
if (Value > 1_000_000_000)
{
value *= 0.000000001;
symbol = "GHz";
}
else if (Value > 1_000_000)
{
value *= 0.000001;
symbol = "MHz";
}
else
{
value *= 0.001;
symbol = "kHz";
}
}
else if (symbol == "kHz")
{
if (Value > 1000_000)
{
value *= 0.000001;
symbol = "GHz";
}
else
{
value *= 0.001;
symbol = "MHz";
}
}
else if (symbol == "MHz")
{
value *= 0.001;
symbol = "GHz";
}
}
return $"{value:G3}\xA0{symbol}";
}
}

0 comments on commit 3c1c4b9

Please sign in to comment.