Repository navigation
feat: FPS counter card with sizes, PC stats and session-menu settings #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fc72112
3537d81
1b23911
945a768
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace ConsoleMode.Native; | ||
|
|
||
| /// <summary> | ||
| /// GPU temperature, engine clock and VRAM size straight from the display driver (D3DKMT, WDDM 2.4+), | ||
| /// the source of Task Manager's GPU temperature: no vendor library, driver or admin needed. | ||
| /// Drivers that don't report a value leave it 0, read here as null. | ||
| /// </summary> | ||
| public static class GpuKmt | ||
| { | ||
| private const int KmtqaiGetSegmentSize = 3; | ||
| private const int KmtqaiNodePerfData = 61; | ||
| private const int KmtqaiAdapterPerfData = 62; | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct OpenAdapterFromLuid | ||
| { | ||
| public uint LuidLow; | ||
| public int LuidHigh; | ||
| public uint Adapter; | ||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct CloseAdapter | ||
| { | ||
| public uint Adapter; | ||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct QueryAdapterInfo | ||
| { | ||
| public uint Adapter; | ||
| public int Type; | ||
| public nint Data; | ||
| public uint DataSize; | ||
| } | ||
|
|
||
| // d3dkmthk.h: D3DKMT_SEGMENTSIZEINFO, D3DKMT_NODE_PERFDATA, D3DKMT_ADAPTER_PERFDATA (ULONGLONGs are 8-aligned). | ||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct SegmentSizeInfo | ||
| { | ||
| public ulong DedicatedVideoMemorySize; | ||
| public ulong DedicatedSystemMemorySize; | ||
| public ulong SharedSystemMemorySize; | ||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct NodePerfData | ||
| { | ||
| public uint NodeOrdinal; | ||
| public uint PhysicalAdapterIndex; | ||
| public ulong Frequency; | ||
| public ulong MaxFrequency; | ||
| public ulong MaxFrequencyOc; | ||
| public uint Voltage; | ||
| public uint VoltageMax; | ||
| public uint VoltageMaxOc; | ||
| public ulong MaxTransitionLatency; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The native Technical details# Node performance payload size
## Affected sites
- `src/ConsoleMode/Native/GpuKmt.cs:49-60` — `NodePerfData` omits `Reserved`.
- `src/ConsoleMode/Native/GpuKmt.cs:124` — `DataSize` is derived from the undersized struct.
## Required outcome
- Submit a `NodePerfData` buffer with the documented native size and field layout.
## Reference
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_node_perfdata |
||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct AdapterPerfData | ||
| { | ||
| public uint PhysicalAdapterIndex; | ||
| public ulong MemoryFrequency; | ||
| public ulong MaxMemoryFrequency; | ||
| public ulong MaxMemoryFrequencyOc; | ||
| public ulong MemoryBandwidth; | ||
| public ulong PcieBandwidth; | ||
| public uint FanRpm; | ||
| public uint Power; | ||
| public uint Temperature; | ||
| public byte PowerStateOverride; | ||
| } | ||
|
|
||
| [DllImport("gdi32.dll")] | ||
| private static extern int D3DKMTOpenAdapterFromLuid(ref OpenAdapterFromLuid open); | ||
|
|
||
| [DllImport("gdi32.dll")] | ||
| private static extern int D3DKMTCloseAdapter(ref CloseAdapter close); | ||
|
|
||
| [DllImport("gdi32.dll")] | ||
| private static extern int D3DKMTQueryAdapterInfo(ref QueryAdapterInfo query); | ||
|
|
||
| public readonly record struct Reading(double? TemperatureC, double? ClockMhz, double? DedicatedMb); | ||
|
|
||
| /// <summary>One read of the adapter (LUID as in the GPU performance counters) and its engine <paramref name="node"/>.</summary> | ||
| public static Reading Read(uint luidLow, int luidHigh, uint node) | ||
| { | ||
| var open = new OpenAdapterFromLuid { LuidLow = luidLow, LuidHigh = luidHigh }; | ||
| try | ||
| { | ||
| if (D3DKMTOpenAdapterFromLuid(ref open) != 0) return default; | ||
| } | ||
| catch (EntryPointNotFoundException) | ||
| { | ||
| return default; | ||
| } | ||
| try | ||
| { | ||
| var adapter = new AdapterPerfData(); | ||
| var engine = new NodePerfData { NodeOrdinal = node }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Technical details# Physical adapter selection
## Affected sites
- `src/ConsoleMode/Services/HardwareStats.cs:27, 60-61` — the selected adapter key includes the `phys_N` component, but the index is not exposed to the KMT reader.
- `src/ConsoleMode/Native/GpuKmt.cs:65, 103` — the performance payloads leave `PhysicalAdapterIndex` at zero.
## Required outcome
- Pass the selected physical adapter index to both adapter- and node-performance queries.
## Reference
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_node_perfdata
- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_adapter_perfdata |
||
| var segments = new SegmentSizeInfo(); | ||
| double? temperature = Query(open.Adapter, KmtqaiAdapterPerfData, ref adapter) && adapter.Temperature > 0 | ||
| ? adapter.Temperature / 10.0 : null; | ||
| double? clock = Query(open.Adapter, KmtqaiNodePerfData, ref engine) && engine.Frequency > 0 | ||
| ? engine.Frequency / 1_000_000.0 : null; | ||
| double? dedicated = Query(open.Adapter, KmtqaiGetSegmentSize, ref segments) && segments.DedicatedVideoMemorySize > 0 | ||
| ? segments.DedicatedVideoMemorySize / (1024.0 * 1024.0) : null; | ||
| return new Reading(temperature, clock, dedicated); | ||
| } | ||
| finally | ||
| { | ||
| var close = new CloseAdapter { Adapter = open.Adapter }; | ||
| D3DKMTCloseAdapter(ref close); | ||
| } | ||
| } | ||
|
|
||
| private static unsafe bool Query<T>(uint adapter, int type, ref T data) where T : unmanaged | ||
| { | ||
| fixed (T* p = &data) | ||
| { | ||
| var query = new QueryAdapterInfo { Adapter = adapter, Type = type, Data = (nint)p, DataSize = (uint)sizeof(T) }; | ||
| return D3DKMTQueryAdapterInfo(ref query) == 0; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace ConsoleMode.Native; | ||
|
|
||
| /// <summary> | ||
| /// Windows performance counters (PDH), the same source Task Manager reads. Counter paths are the | ||
| /// English names (PdhAddEnglishCounter), so they work on a Windows in any language. | ||
| /// </summary> | ||
| public sealed class PerfCounters : IDisposable | ||
| { | ||
| private const uint PdhFmtDouble = 0x00000200; | ||
| private const uint PdhFmtNoCap100 = 0x00008000; | ||
| private const int PdhMoreData = unchecked((int)0x800007D2); | ||
|
|
||
| [DllImport("pdh.dll", CharSet = CharSet.Unicode)] | ||
| private static extern int PdhOpenQueryW(string? dataSource, nint userData, out nint query); | ||
|
|
||
| [DllImport("pdh.dll", CharSet = CharSet.Unicode)] | ||
| private static extern int PdhAddEnglishCounterW(nint query, string path, nint userData, out nint counter); | ||
|
|
||
| [DllImport("pdh.dll")] | ||
| private static extern int PdhCollectQueryData(nint query); | ||
|
|
||
| [DllImport("pdh.dll")] | ||
| private static extern int PdhGetFormattedCounterValue(nint counter, uint format, out uint type, out FmtCounterValue value); | ||
|
|
||
| [DllImport("pdh.dll", CharSet = CharSet.Unicode)] | ||
| private static extern int PdhGetFormattedCounterArrayW(nint counter, uint format, ref uint bufferSize, out uint itemCount, nint buffer); | ||
|
|
||
| [DllImport("pdh.dll")] | ||
| private static extern int PdhCloseQuery(nint query); | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct FmtCounterValue | ||
| { | ||
| public uint Status; | ||
| public double Value; | ||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct FmtCounterValueItem | ||
| { | ||
| public nint Name; | ||
| public FmtCounterValue Value; | ||
| } | ||
|
|
||
| private nint _query; | ||
|
|
||
| public PerfCounters() | ||
| { | ||
| if (PdhOpenQueryW(null, 0, out _query) != 0) _query = 0; | ||
| } | ||
|
|
||
| /// <summary>0 when the counter doesn't exist on this machine (old Windows, no GPU driver…).</summary> | ||
| public nint Add(string path) => | ||
| _query != 0 && PdhAddEnglishCounterW(_query, path, 0, out var counter) == 0 ? counter : 0; | ||
|
|
||
| /// <summary>Takes a sample; rates (usage %) need two, so the first read after this returns null.</summary> | ||
| public void Collect() | ||
| { | ||
| if (_query != 0) PdhCollectQueryData(_query); | ||
| } | ||
|
|
||
| public double? Read(nint counter) | ||
| { | ||
| if (counter == 0) return null; | ||
| return PdhGetFormattedCounterValue(counter, PdhFmtDouble | PdhFmtNoCap100, out _, out var value) == 0 && value.Status == 0 | ||
| ? value.Value : null; | ||
| } | ||
|
|
||
| /// <summary>Every instance of a wildcard counter (…(*)\…) with its value.</summary> | ||
| public List<(string Instance, double Value)> ReadAll(nint counter) | ||
| { | ||
| var result = new List<(string, double)>(); | ||
| if (counter == 0) return result; | ||
| uint size = 0; | ||
| if (PdhGetFormattedCounterArrayW(counter, PdhFmtDouble | PdhFmtNoCap100, ref size, out _, 0) != PdhMoreData || size == 0) | ||
| return result; | ||
| var buffer = Marshal.AllocHGlobal((int)size); | ||
| try | ||
| { | ||
| if (PdhGetFormattedCounterArrayW(counter, PdhFmtDouble | PdhFmtNoCap100, ref size, out var count, buffer) != 0) | ||
| return result; | ||
| var itemSize = Marshal.SizeOf<FmtCounterValueItem>(); | ||
| for (var i = 0; i < count; i++) | ||
| { | ||
| var item = Marshal.PtrToStructure<FmtCounterValueItem>(buffer + i * itemSize); | ||
| if (item.Value.Status == 0 && Marshal.PtrToStringUni(item.Name) is { } name) | ||
| result.Add((name, item.Value.Value)); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| Marshal.FreeHGlobal(buffer); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| if (_query == 0) return; | ||
| PdhCloseQuery(_query); | ||
| _query = 0; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace ConsoleMode.Native; | ||
|
|
||
| public static class SystemMemory | ||
| { | ||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct MemoryStatusEx | ||
| { | ||
| public uint Length; | ||
| public uint MemoryLoad; | ||
| public ulong TotalPhys; | ||
| public ulong AvailPhys; | ||
| public ulong TotalPageFile; | ||
| public ulong AvailPageFile; | ||
| public ulong TotalVirtual; | ||
| public ulong AvailVirtual; | ||
| public ulong AvailExtendedVirtual; | ||
| } | ||
|
|
||
| [DllImport("kernel32.dll")] | ||
| private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx status); | ||
|
|
||
| /// <summary>RAM in use and installed, in MB; nulls if Windows doesn't answer.</summary> | ||
| public static (double? UsedMb, double? TotalMb) Read() | ||
| { | ||
| var status = new MemoryStatusEx { Length = (uint)Marshal.SizeOf<MemoryStatusEx>() }; | ||
| if (!GlobalMemoryStatusEx(ref status) || status.TotalPhys == 0) return (null, null); | ||
| const double mb = 1024 * 1024; | ||
| return ((status.TotalPhys - status.AvailPhys) / mb, status.TotalPhys / mb); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These performance-query enum values are each one too high:
NODEPERFDATAis 60 andADAPTERPERFDATAis 61 in the Windows enum, so these calls request the wrong payload types and the GPU clock/temperature reads will fail. Please use the documented values (or named constants) before relying on these readings.Technical details