@@ -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.} \u00B0 C " ) ;
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 {
0 commit comments