Skip to content

Commit ddccd54

Browse files
committed
Fix GPU Stats not cycling through multiple GPUs
1 parent 7884f42 commit ddccd54

5 files changed

Lines changed: 220 additions & 58 deletions

File tree

src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs

Lines changed: 97 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
1616
private const string GpuEngineCategoryName = "GPU Engine";
1717
private const string UtilizationPercentageCounter = "Utilization Percentage";
1818

19-
private static readonly CompositeFormat TemperatureFormat = CompositeFormat.Parse("{0:0.} \u00B0C");
19+
private static readonly CompositeFormat TemperatureFormat = CompositeFormat.Parse("{0:0.} °C");
2020

2121
// Instance-name key tokens
2222
private const string KeyPid = "pid";
2323
private const string KeyLuid = "luid";
24-
private const string KeyPhys = "phys";
2524
private const string KeyEngineType = "engtype";
2625

2726
// Engine type filter
@@ -34,8 +33,12 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
3433
// Batch read via category - single kernel transition per tick
3534
private readonly PerformanceCounterCategory? _gpuEngineCategory;
3635

37-
// Discovered physical GPU IDs
38-
private readonly HashSet<int> _knownPhysIds = [];
36+
// Friendly adapter names (and software flag) keyed by LUID, resolved via DXGI.
37+
private readonly Dictionary<long, GpuAdapterNames.AdapterInfo> _adaptersByLuid;
38+
39+
// LUIDs we've already turned into a _stats entry, or deliberately skipped
40+
// (e.g. software adapters). Used to discover GPUs at most once each.
41+
private readonly HashSet<long> _knownLuids = [];
3942

4043
private readonly List<Data> _stats = [];
4144

@@ -48,7 +51,7 @@ public sealed class Data
4851
{
4952
public string? Name { get; set; }
5053

51-
public int PhysId { get; set; }
54+
public long LuidKey { get; set; }
5255

5356
public float Usage { get; set; }
5457

@@ -60,12 +63,12 @@ public sealed class Data
6063
public GPUStats()
6164
{
6265
_gpuEngineCategory = CreatePerformanceCounterCategory(GpuEngineCategoryName);
66+
_adaptersByLuid = GpuAdapterNames.GetByLuid();
6367

64-
GetGPUPerfCounters();
65-
LoadGPUsFromCounters();
68+
DiscoverGPUsFromCounters();
6669
}
6770

68-
public void GetGPUPerfCounters()
71+
public void DiscoverGPUsFromCounters()
6972
{
7073
if (_gpuEngineCategory is null)
7174
{
@@ -74,21 +77,12 @@ public void GetGPUPerfCounters()
7477

7578
try
7679
{
77-
// There are really 4 different things we should be tracking the usage
78-
// of. Similar to how the instance name ends with `3D`, the following
79-
// suffixes are important.
80-
//
81-
// * `3D`
82-
// * `VideoEncode`
83-
// * `VideoDecode`
84-
// * `VideoProcessing`
85-
//
86-
// We could totally put each of those sets of counters into their own
87-
// set. That's what we should do, so that we can report the sum of those
88-
// numbers as the total utilization, and then have them broken out in
89-
// the card template and in the details metadata.
90-
_knownPhysIds.Clear();
91-
80+
// The old Dev Home code keyed GPUs by the "phys_N" token in the
81+
// instance name, assuming it enumerated physical adapters. On modern
82+
// Windows that token is effectively always "phys_0" - even on machines
83+
// with multiple discrete GPUs - so every adapter collapsed into a
84+
// single bucket and Prev/Next GPU had nothing to cycle through. The
85+
// real per-adapter identifier is the LUID, so we key on that instead.
9286
var instanceNames = _gpuEngineCategory.GetInstanceNames();
9387

9488
foreach (var instanceName in instanceNames)
@@ -98,15 +92,9 @@ public void GetGPUPerfCounters()
9892
continue;
9993
}
10094

101-
var counterKey = instanceName;
102-
103-
// skip these values
104-
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
105-
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
106-
107-
if (int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
95+
if (TryGetLuidKey(instanceName, out var luidKey))
10896
{
109-
_knownPhysIds.Add(phys);
97+
AddGpuIfNew(luidKey);
11098
}
11199
}
112100
}
@@ -116,23 +104,6 @@ public void GetGPUPerfCounters()
116104
}
117105
}
118106

119-
public void LoadGPUsFromCounters()
120-
{
121-
// The old dev home code tracked GPU stats by querying WMI for the list
122-
// of GPUs, and then matching them up with the performance counter IDs.
123-
//
124-
// We can't use WMI here, because it drags in a dependency on
125-
// Microsoft.Management.Infrastructure, which is not compatible with
126-
// AOT.
127-
//
128-
// For now, we'll just use the indices as the GPU names.
129-
_stats.Clear();
130-
foreach (var id in _knownPhysIds)
131-
{
132-
_stats.Add(new Data() { PhysId = id, Name = GpuNamePrefix + id });
133-
}
134-
}
135-
136107
public void GetData()
137108
{
138109
if (_gpuEngineCategory is null)
@@ -152,8 +123,8 @@ public void GetData()
152123

153124
var utilizationData = categoryData[UtilizationPercentageCounter];
154125

155-
// Accumulate usage per physical GPU
156-
var gpuUsage = new Dictionary<int, float>();
126+
// Accumulate usage per physical GPU, keyed by adapter LUID.
127+
var gpuUsage = new Dictionary<long, float>();
157128
var currentSamples = new Dictionary<string, CounterSample>();
158129

159130
foreach (InstanceData instance in utilizationData.Values)
@@ -164,15 +135,16 @@ public void GetData()
164135
continue;
165136
}
166137

167-
var counterKey = instanceName;
168-
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
169-
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
170-
171-
if (!int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
138+
if (!TryGetLuidKey(instanceName, out var luidKey))
172139
{
173140
continue;
174141
}
175142

143+
// A GPU can register counter instances after we were constructed
144+
// (e.g. a discrete GPU coming out of an idle low-power state), so
145+
// keep discovering here rather than only in the constructor.
146+
AddGpuIfNew(luidKey);
147+
176148
var sample = instance.Sample;
177149
currentSamples[instanceName] = sample;
178150

@@ -181,7 +153,7 @@ public void GetData()
181153
try
182154
{
183155
var cookedValue = CounterSampleCalculator.ComputeCounterValue(prevSample, sample);
184-
gpuUsage[phys] = gpuUsage.GetValueOrDefault(phys) + cookedValue;
156+
gpuUsage[luidKey] = gpuUsage.GetValueOrDefault(luidKey) + cookedValue;
185157
}
186158
catch (Exception)
187159
{
@@ -196,7 +168,7 @@ public void GetData()
196168
// Update stats
197169
foreach (var gpu in _stats)
198170
{
199-
var sum = gpuUsage.TryGetValue(gpu.PhysId, out var usage) ? usage : 0f;
171+
var sum = gpuUsage.TryGetValue(gpu.LuidKey, out var usage) ? usage : 0f;
200172
gpu.Usage = sum / 100;
201173
lock (gpu.GpuChartValues)
202174
{
@@ -210,6 +182,29 @@ public void GetData()
210182
}
211183
}
212184

185+
private void AddGpuIfNew(long luidKey)
186+
{
187+
if (!_knownLuids.Add(luidKey))
188+
{
189+
return;
190+
}
191+
192+
_adaptersByLuid.TryGetValue(luidKey, out var info);
193+
194+
// Hide software adapters (e.g. the Microsoft Basic Render Driver / WARP):
195+
// they report 3D engine activity but aren't a GPU the user cares about.
196+
if (info.IsSoftware)
197+
{
198+
return;
199+
}
200+
201+
var name = string.IsNullOrEmpty(info.Description)
202+
? GpuNamePrefix + _stats.Count
203+
: info.Description;
204+
205+
_stats.Add(new Data() { LuidKey = luidKey, Name = name });
206+
}
207+
213208
internal string CreateGPUImageUrl(int gpuChartIndex)
214209
{
215210
if (_stats.Count <= gpuChartIndex)
@@ -292,7 +287,51 @@ internal string GetGPUTemperature(int gpuActiveIndex)
292287
return string.Format(CultureInfo.InvariantCulture, TemperatureFormat.Format, temperature);
293288
}
294289

295-
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
290+
private static bool TryGetLuidKey(string instanceName, out long luidKey)
291+
{
292+
// Instance names look like:
293+
// pid_1234_luid_0x00000000_0x0001766D_phys_0_eng_0_engtype_3D
294+
// We only need the LUID; advance past the pid token, then read the luid.
295+
var counterKey = instanceName;
296+
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
297+
var luid = GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
298+
299+
return TryParseLuidKey(luid, out luidKey);
300+
}
301+
302+
private static bool TryParseLuidKey(string luid, out long luidKey)
303+
{
304+
luidKey = 0;
305+
306+
// The luid token is "0x{HighPart}_0x{LowPart}", matching DXGI's
307+
// LUID.HighPart / LUID.LowPart so the key lines up with GpuAdapterNames.
308+
var separator = luid.IndexOf('_');
309+
if (separator < 0)
310+
{
311+
return false;
312+
}
313+
314+
if (!TryParseHex(luid.AsSpan(0, separator), out var high) ||
315+
!TryParseHex(luid.AsSpan(separator + 1), out var low))
316+
{
317+
return false;
318+
}
319+
320+
luidKey = ((long)high << 32) | low;
321+
return true;
322+
}
323+
324+
private static bool TryParseHex(ReadOnlySpan<char> token, out uint value)
325+
{
326+
if (token.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
327+
{
328+
token = token[2..];
329+
}
330+
331+
return uint.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
332+
}
333+
334+
private static string GetKeyValueFromCounterKey(string key, ref string counterKey)
296335
{
297336
if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
298337
{
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using Microsoft.CmdPal.Common;
8+
using Windows.Win32;
9+
using Windows.Win32.Graphics.Dxgi;
10+
11+
namespace CoreWidgetProvider.Helpers;
12+
13+
/// <summary>
14+
/// Resolves friendly GPU adapter names (and whether an adapter is a software
15+
/// renderer) keyed by adapter LUID, using DXGI.
16+
///
17+
/// The "GPU Engine" performance counters identify each physical adapter by its
18+
/// LUID, but not by name, so we enumerate DXGI adapters once and match them up.
19+
/// We can't use WMI for this (it isn't AOT-compatible), but DXGI via CsWin32 is.
20+
/// </summary>
21+
internal static class GpuAdapterNames
22+
{
23+
// DXGI_ADAPTER_FLAG_SOFTWARE - marks the Microsoft Basic Render Driver (WARP).
24+
private const uint DxgiAdapterFlagSoftware = 0x2;
25+
26+
internal readonly record struct AdapterInfo(string Description, bool IsSoftware);
27+
28+
/// <summary>
29+
/// Enumerates the system's DXGI adapters and returns their descriptions keyed
30+
/// by LUID. The key matches <see cref="GPUStats"/>'s LUID parsing:
31+
/// <c>(HighPart &lt;&lt; 32) | LowPart</c>. Returns an empty map on any failure;
32+
/// callers fall back to generic names.
33+
/// </summary>
34+
internal static unsafe Dictionary<long, AdapterInfo> GetByLuid()
35+
{
36+
var adapters = new Dictionary<long, AdapterInfo>();
37+
38+
IDXGIFactory1* factory = null;
39+
40+
try
41+
{
42+
if (PInvoke.CreateDXGIFactory1(IDXGIFactory1.IID_Guid, out var factoryPtr).Failed || factoryPtr is null)
43+
{
44+
return adapters;
45+
}
46+
47+
factory = (IDXGIFactory1*)factoryPtr;
48+
49+
for (uint index = 0; ; index++)
50+
{
51+
IDXGIAdapter1* adapter = null;
52+
53+
// EnumAdapters1 returns DXGI_ERROR_NOT_FOUND once we walk past the
54+
// last adapter, which surfaces here as a failed HRESULT.
55+
if (factory->EnumAdapters1(index, &adapter).Failed || adapter is null)
56+
{
57+
break;
58+
}
59+
60+
try
61+
{
62+
DXGI_ADAPTER_DESC1 desc = default;
63+
if (adapter->GetDesc1(&desc).Failed)
64+
{
65+
continue;
66+
}
67+
68+
var luidKey = ((long)(uint)desc.AdapterLuid.HighPart << 32) | desc.AdapterLuid.LowPart;
69+
var isSoftware = ((uint)desc.Flags & DxgiAdapterFlagSoftware) != 0;
70+
var description = desc.Description.ToString().TrimEnd('\0');
71+
72+
adapters[luidKey] = new AdapterInfo(description, isSoftware);
73+
}
74+
finally
75+
{
76+
adapter->Release();
77+
}
78+
}
79+
}
80+
catch (Exception ex)
81+
{
82+
CoreLogger.LogError("Failed to enumerate DXGI adapters for GPU names.", ex);
83+
}
84+
finally
85+
{
86+
if (factory is not null)
87+
{
88+
factory->Release();
89+
}
90+
}
91+
92+
return adapters;
93+
}
94+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "https://aka.ms/CsWin32.schema.json",
3+
"allowMarshaling": false,
4+
"comInterop": {
5+
"preserveSigMethods": [ "*" ]
6+
}
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
GlobalMemoryStatusEx
22
GetSystemPowerStatus
3+
CreateDXGIFactory1
4+
IDXGIFactory1
5+
IDXGIAdapter1
6+
DXGI_ADAPTER_DESC1

src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ public PerformanceWidgetsPage(SettingsManager settingsManager, bool isBandPage =
156156
_gpuPage.Updated += (s, e) =>
157157
{
158158
_gpuItem.Title = _gpuPage.GetItemTitle(isBandPage);
159+
if (_isBandPage)
160+
{
161+
// Bands only show the usage percentage as the title, so put
162+
// the active GPU's name in the subtitle - otherwise cycling
163+
// Prev/Next GPU between two idle adapters looks like nothing
164+
// changed.
165+
_gpuItem.Subtitle = _gpuPage.GetBandSubtitle();
166+
}
159167
};
160168
}
161169

@@ -1040,6 +1048,16 @@ public string GetItemTitle(bool isBandPage)
10401048
}
10411049
}
10421050

1051+
public string GetBandSubtitle()
1052+
{
1053+
if (ContentData.TryGetValue("gpuName", out var name) && !string.IsNullOrEmpty(name))
1054+
{
1055+
return name;
1056+
}
1057+
1058+
return Resources.GetResource("GPU_Usage_Subtitle");
1059+
}
1060+
10431061
internal override void PushActivate()
10441062
{
10451063
base.PushActivate();

0 commit comments

Comments
 (0)