mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
Fix GPU Stats not cycling through multiple GPUs (#48503)
## Summary of the Pull Request Command Palette's Performance Monitor dock band exposes "Previous GPU" / "Next GPU" commands, but on multi-GPU machines they did nothing and only one GPU was ever shown. Root cause: `GPUStats` keyed GPUs by the `phys_N` token in the "GPU Engine" perf-counter instance names, assuming it enumerated physical adapters. On modern Windows that token is effectively always `phys_0` (verified on a desktop with an RTX 4090 + AMD iGPU — all 1640 instances were `phys_0`), so every adapter collapsed into one bucket and the reported usage was the *sum* across adapters. The real per-adapter identifier is the LUID. ---------- _Note from 6/24 after latest rebase on top of #48710_: Rebased onto latest main, which now includes #48710 ("CmdPal: Accurate GPU usage in Dock"). The two changes overlap in GPUStats.GetData(), so I merged them rather than picking a side — #48710's accuracy fix is fully preserved, just re-keyed. #48710 buckets utilization by (physId, engineId), takes the max engine per adapter, and clamps to [0, 100]. The catch is that phys_N is effectively always phys_0 on modern Windows (the exact reason this PR switched to LUID keying), so on a multi-GPU machine #48710 alone still collapses all adapters into one bucket. So I kept its max-per-engine + clamp + NaN/negative filtering verbatim and only changed the key from (phys, engine) to (LUID, engine). Net result: each adapter is tracked separately and reports a correct, bounded 0–100% value. Verified on a desktop with an RTX 4090 + AMD Radeon iGPU: Prev/Next GPU cycles between the two adapters (WARP hidden). GPU usage stays ≤ 100% under load and matches Task Manager. Load on the 4090 doesn't move the idle iGPU's reading (per-adapter independence holds). No other behavior from #48710 changed. ---------- ## PR Checklist - [x] Closes: #47583 - [x] **Communication:** Posted in #28769 - [x] **Tests:** No tests exist for `Microsoft.CmdPal.Ext.PerformanceMonitor` so did not add any. - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments - Key GPUs by **LUID** (parsed from the instance name) instead of `phys`. - Resolve friendly adapter names via DXGI (`IDXGIFactory1`/`IDXGIAdapter1` through CsWin32, AOT-safe) and **filter out software adapters** (Microsoft Basic Render Driver / WARP). - Discover adapters on each tick as well as at construction, so a GPU that registers counters later still appears. - Show the active GPU's name in the dock band subtitle so cycling is visible. ## Validation Steps Performed Tested on a desktop (RTX 4090 + AMD Radeon iGPU). Prev/Next GPU now cycles between "NVIDIA GeForce RTX 4090" and "AMD Radeon(TM) Graphics", each showing its own utilization; WARP is hidden.
This commit is contained in:
@@ -21,13 +21,13 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
// Instance-name key tokens
|
||||
private const string KeyPid = "pid";
|
||||
private const string KeyLuid = "luid";
|
||||
private const string KeyPhys = "phys";
|
||||
private const string KeyEngineType = "engtype";
|
||||
|
||||
// Engine type filter
|
||||
private const string EngineType3D = "3D";
|
||||
|
||||
// Instance-name key token for the engine index
|
||||
// Instance-name key tokens for the physical adapter slot and engine index
|
||||
private const string KeyPhys = "phys";
|
||||
private const string KeyEng = "eng";
|
||||
|
||||
// Display strings
|
||||
@@ -37,11 +37,21 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
// Batch read via category - single kernel transition per tick
|
||||
private readonly PerformanceCounterCategory? _gpuEngineCategory;
|
||||
|
||||
// Discovered physical GPU IDs
|
||||
private readonly HashSet<int> _knownPhysIds = [];
|
||||
// Friendly adapter names (and software flag) keyed by LUID, resolved via DXGI.
|
||||
private readonly Dictionary<long, GpuAdapterNames.AdapterInfo> _adaptersByLuid;
|
||||
|
||||
// LUIDs we've already turned into a _stats entry, or deliberately skipped
|
||||
// (e.g. software adapters). Used to discover GPUs at most once each.
|
||||
private readonly HashSet<long> _knownLuids = [];
|
||||
|
||||
private readonly List<Data> _stats = [];
|
||||
|
||||
// Guards structural access to _stats, _knownLuids, and _adaptersByLuid. They
|
||||
// are mutated on the perf-counter timer thread (GetData -> DiscoverGpus) but
|
||||
// read on the UI / command thread (GetGPUName, GetPrev/NextGPUIndex, etc.),
|
||||
// and List<T> is not safe for concurrent add/read.
|
||||
private readonly object _statsLock = new();
|
||||
|
||||
// Previous raw samples for computing cooked (delta-based) values
|
||||
private Dictionary<string, CounterSample> _previousSamples = [];
|
||||
private bool _gpuEnumerationFailureLogged;
|
||||
@@ -51,7 +61,7 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public int PhysId { get; set; }
|
||||
public long LuidKey { get; set; }
|
||||
|
||||
public float Usage { get; set; }
|
||||
|
||||
@@ -63,12 +73,12 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
public GPUStats()
|
||||
{
|
||||
_gpuEngineCategory = CreatePerformanceCounterCategory(GpuEngineCategoryName);
|
||||
_adaptersByLuid = GpuAdapterNames.GetByLuid();
|
||||
|
||||
GetGPUPerfCounters();
|
||||
LoadGPUsFromCounters();
|
||||
DiscoverGPUsFromCounters();
|
||||
}
|
||||
|
||||
public void GetGPUPerfCounters()
|
||||
private void DiscoverGPUsFromCounters()
|
||||
{
|
||||
if (_gpuEngineCategory is null)
|
||||
{
|
||||
@@ -77,23 +87,15 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
|
||||
try
|
||||
{
|
||||
// There are really 4 different things we should be tracking the usage
|
||||
// of. Similar to how the instance name ends with `3D`, the following
|
||||
// suffixes are important.
|
||||
//
|
||||
// * `3D`
|
||||
// * `VideoEncode`
|
||||
// * `VideoDecode`
|
||||
// * `VideoProcessing`
|
||||
//
|
||||
// We could totally put each of those sets of counters into their own
|
||||
// set. That's what we should do, so that we can report the sum of those
|
||||
// numbers as the total utilization, and then have them broken out in
|
||||
// the card template and in the details metadata.
|
||||
_knownPhysIds.Clear();
|
||||
|
||||
// The old Dev Home code keyed GPUs by the "phys_N" token in the
|
||||
// instance name, assuming it enumerated physical adapters. On modern
|
||||
// Windows that token is effectively always "phys_0" - even on machines
|
||||
// with multiple discrete GPUs - so every adapter collapsed into a
|
||||
// single bucket and Prev/Next GPU had nothing to cycle through. The
|
||||
// real per-adapter identifier is the LUID, so we key on that instead.
|
||||
var instanceNames = _gpuEngineCategory.GetInstanceNames();
|
||||
|
||||
var seenLuids = new HashSet<long>();
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
|
||||
@@ -101,17 +103,13 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
continue;
|
||||
}
|
||||
|
||||
var counterKey = instanceName;
|
||||
|
||||
// skip these values
|
||||
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
|
||||
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
|
||||
|
||||
if (int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
|
||||
if (TryGetLuidAndEngine(instanceName, out var luidKey, out _))
|
||||
{
|
||||
_knownPhysIds.Add(phys);
|
||||
seenLuids.Add(luidKey);
|
||||
}
|
||||
}
|
||||
|
||||
DiscoverGpus(seenLuids);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -119,23 +117,6 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadGPUsFromCounters()
|
||||
{
|
||||
// The old dev home code tracked GPU stats by querying WMI for the list
|
||||
// of GPUs, and then matching them up with the performance counter IDs.
|
||||
//
|
||||
// We can't use WMI here, because it drags in a dependency on
|
||||
// Microsoft.Management.Infrastructure, which is not compatible with
|
||||
// AOT.
|
||||
//
|
||||
// For now, we'll just use the indices as the GPU names.
|
||||
_stats.Clear();
|
||||
foreach (var id in _knownPhysIds)
|
||||
{
|
||||
_stats.Add(new Data() { PhysId = id, Name = GpuNamePrefix + id });
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
if (_gpuEngineCategory is null)
|
||||
@@ -155,17 +136,20 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
|
||||
var utilizationData = categoryData[UtilizationPercentageCounter];
|
||||
|
||||
// Accumulate utilization for each (physical GPU, 3D engine) pair. Each instance
|
||||
// Accumulate utilization for each (adapter, 3D engine) pair. Each instance
|
||||
// (pid_<pid>_luid_<luid>_phys_<phys>_eng_<engId>_engtype_3D) reports the percentage
|
||||
// of wall-clock time a single process spent on that engine. Summing across processes
|
||||
// for the same engine is correct (gives total engine utilization). Summing across
|
||||
// multiple 3D engines on the same adapter, however, is NOT — that produced values
|
||||
// multiple 3D engines on the same adapter, however, is NOT - that produced values
|
||||
// >100% in the dock under heavy GPU load (issue #48677). Mirroring Task Manager,
|
||||
// we take the maximum 3D engine utilization per adapter and clamp to [0, 100].
|
||||
// This parallels the CPU fix in #46381, which switched to a counter that is
|
||||
// naturally bounded to 0-100%.
|
||||
var perEngineUsage = new Dictionary<(int Phys, string EngineId), float>();
|
||||
// naturally bounded to 0-100%. Adapters are keyed by LUID rather than the
|
||||
// "phys_N" token, which is effectively always 0 even on multi-GPU machines and
|
||||
// so cannot tell adapters apart.
|
||||
var perEngineUsage = new Dictionary<(long Luid, string EngineId), float>();
|
||||
var currentSamples = new Dictionary<string, CounterSample>();
|
||||
var seenLuids = new HashSet<long>();
|
||||
|
||||
foreach (InstanceData instance in utilizationData.Values)
|
||||
{
|
||||
@@ -175,20 +159,15 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
continue;
|
||||
}
|
||||
|
||||
var counterKey = instanceName;
|
||||
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
|
||||
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
|
||||
|
||||
if (!int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
|
||||
if (!TryGetLuidAndEngine(instanceName, out var luidKey, out var engineId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var engineId = GetKeyValueFromCounterKey(KeyEng, ref counterKey);
|
||||
if (string.IsNullOrEmpty(engineId) || engineId == "error")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Just record which adapters we saw; discovery of new ones is
|
||||
// batched after the loop so we don't take _statsLock per instance
|
||||
// (there can be hundreds of instances per tick).
|
||||
seenLuids.Add(luidKey);
|
||||
|
||||
var sample = instance.Sample;
|
||||
currentSamples[instanceName] = sample;
|
||||
@@ -203,7 +182,7 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = (phys, engineId);
|
||||
var key = (luidKey, engineId);
|
||||
perEngineUsage[key] = perEngineUsage.GetValueOrDefault(key) + cookedValue;
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -216,27 +195,34 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
// Swap samples - stale entries are automatically cleaned up
|
||||
_previousSamples = currentSamples;
|
||||
|
||||
// Discover adapters we haven't seen before. Batched: one lock check
|
||||
// per tick, and any DXGI name enumeration happens off the lock. New
|
||||
// adapters land in _stats before the update loop so they get a value
|
||||
// this tick.
|
||||
DiscoverGpus(seenLuids);
|
||||
|
||||
// Reduce per-engine values to a single 0-100 utilization per adapter (max across engines).
|
||||
var gpuUsage = new Dictionary<int, float>();
|
||||
var gpuUsage = new Dictionary<long, float>();
|
||||
foreach (var kvp in perEngineUsage)
|
||||
{
|
||||
var phys = kvp.Key.Phys;
|
||||
var engineUsage = kvp.Value;
|
||||
if (engineUsage > gpuUsage.GetValueOrDefault(phys))
|
||||
if (kvp.Value > gpuUsage.GetValueOrDefault(kvp.Key.Luid))
|
||||
{
|
||||
gpuUsage[phys] = engineUsage;
|
||||
gpuUsage[kvp.Key.Luid] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats
|
||||
foreach (var gpu in _stats)
|
||||
lock (_statsLock)
|
||||
{
|
||||
var raw = gpuUsage.TryGetValue(gpu.PhysId, out var usage) ? usage : 0f;
|
||||
var clamped = Math.Clamp(raw, 0f, 100f);
|
||||
gpu.Usage = clamped / 100f;
|
||||
lock (gpu.GpuChartValues)
|
||||
foreach (var gpu in _stats)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(clamped, gpu.GpuChartValues);
|
||||
var raw = gpuUsage.TryGetValue(gpu.LuidKey, out var usage) ? usage : 0f;
|
||||
var clamped = Math.Clamp(raw, 0f, 100f);
|
||||
gpu.Usage = clamped / 100f;
|
||||
lock (gpu.GpuChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(clamped, gpu.GpuChartValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,64 +232,178 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
internal string CreateGPUImageUrl(int gpuChartIndex)
|
||||
// Adds any newly-seen adapters to _stats. Called once per tick with the set
|
||||
// of LUIDs observed this tick, so the hot per-instance path never touches the
|
||||
// stats lock. The slow part - DXGI name enumeration for an adapter that only
|
||||
// appeared after construction (e.g. an eGPU hot-plug) - is done outside the
|
||||
// lock, since _statsLock is also held by the UI / command accessors.
|
||||
private void DiscoverGpus(HashSet<long> seenLuids)
|
||||
{
|
||||
if (_stats.Count <= gpuChartIndex)
|
||||
List<long>? newLuids = null;
|
||||
var needDxgiRefresh = false;
|
||||
|
||||
lock (_statsLock)
|
||||
{
|
||||
return string.Empty;
|
||||
foreach (var luidKey in seenLuids)
|
||||
{
|
||||
if (_knownLuids.Contains(luidKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
(newLuids ??= []).Add(luidKey);
|
||||
if (!_adaptersByLuid.ContainsKey(luidKey))
|
||||
{
|
||||
needDxgiRefresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ChartHelper.CreateImageUrl(_stats[gpuChartIndex].GpuChartValues, ChartHelper.ChartType.GPU);
|
||||
// Common case: nothing new, so we took the lock exactly once this tick.
|
||||
if (newLuids is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A newly-seen adapter that isn't in the cached name map registered
|
||||
// after we were constructed, so re-enumerate DXGI to pick up its friendly
|
||||
// name. Done outside the lock because enumeration can be slow.
|
||||
var refreshedNames = needDxgiRefresh ? GpuAdapterNames.GetByLuid() : null;
|
||||
|
||||
lock (_statsLock)
|
||||
{
|
||||
if (refreshedNames is not null)
|
||||
{
|
||||
foreach (var adapter in refreshedNames)
|
||||
{
|
||||
_adaptersByLuid[adapter.Key] = adapter.Value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var luidKey in newLuids)
|
||||
{
|
||||
AddGpuLocked(luidKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a single adapter to _stats. The caller must hold _statsLock, and must
|
||||
// already have a name for this LUID cached in _adaptersByLuid if one exists.
|
||||
private void AddGpuLocked(long luidKey)
|
||||
{
|
||||
if (!_knownLuids.Add(luidKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_adaptersByLuid.TryGetValue(luidKey, out var info);
|
||||
|
||||
// Hide software adapters (Microsoft Basic Render Driver / WARP) only when
|
||||
// there's a real GPU to show instead. On VMs / RDP / headless boxes the
|
||||
// software adapter is the only one present, so keep it rather than
|
||||
// leaving the band with nothing to display.
|
||||
if (info.IsSoftware && HasHardwareAdapter())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var name = string.IsNullOrEmpty(info.Description)
|
||||
? GpuNamePrefix + _stats.Count
|
||||
: info.Description;
|
||||
|
||||
_stats.Add(new Data() { LuidKey = luidKey, Name = name });
|
||||
}
|
||||
|
||||
// True if DXGI reports at least one non-software adapter in the system. DXGI
|
||||
// enumerates hardware adapters regardless of power state, so this stays
|
||||
// correct even when the real GPU is idle and hasn't produced counters yet.
|
||||
// Caller must hold _statsLock (reads _adaptersByLuid).
|
||||
private bool HasHardwareAdapter()
|
||||
{
|
||||
foreach (var adapter in _adaptersByLuid.Values)
|
||||
{
|
||||
if (!adapter.IsSoftware)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal string CreateGPUImageUrl(int gpuChartIndex)
|
||||
{
|
||||
lock (_statsLock)
|
||||
{
|
||||
if (_stats.Count <= gpuChartIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return ChartHelper.CreateImageUrl(_stats[gpuChartIndex].GpuChartValues, ChartHelper.ChartType.GPU);
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetGPUName(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
lock (_statsLock)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _stats[gpuActiveIndex].Name ?? string.Empty;
|
||||
return _stats[gpuActiveIndex].Name ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal int GetPrevGPUIndex(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count == 0)
|
||||
lock (_statsLock)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (_stats.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gpuActiveIndex == 0)
|
||||
{
|
||||
return _stats.Count - 1;
|
||||
}
|
||||
if (gpuActiveIndex == 0)
|
||||
{
|
||||
return _stats.Count - 1;
|
||||
}
|
||||
|
||||
return gpuActiveIndex - 1;
|
||||
return gpuActiveIndex - 1;
|
||||
}
|
||||
}
|
||||
|
||||
internal int GetNextGPUIndex(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count == 0)
|
||||
lock (_statsLock)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (_stats.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gpuActiveIndex == _stats.Count - 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (gpuActiveIndex == _stats.Count - 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return gpuActiveIndex + 1;
|
||||
return gpuActiveIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
internal float GetGPUUsage(int gpuActiveIndex, string gpuActiveEngType)
|
||||
{
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
lock (_statsLock)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _stats[gpuActiveIndex].Usage;
|
||||
return _stats[gpuActiveIndex].Usage;
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetGPUTemperature(int gpuActiveIndex)
|
||||
@@ -314,21 +414,78 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
//
|
||||
// I have not done the code archeology to figure out why they were
|
||||
// removed.
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
lock (_statsLock)
|
||||
{
|
||||
return TemperatureUnavailable;
|
||||
}
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return TemperatureUnavailable;
|
||||
}
|
||||
|
||||
var temperature = _stats[gpuActiveIndex].Temperature;
|
||||
if (temperature == 0)
|
||||
{
|
||||
return TemperatureUnavailable;
|
||||
}
|
||||
var temperature = _stats[gpuActiveIndex].Temperature;
|
||||
if (temperature == 0)
|
||||
{
|
||||
return TemperatureUnavailable;
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, TemperatureFormat.Format, temperature);
|
||||
return string.Format(CultureInfo.InvariantCulture, TemperatureFormat.Format, temperature);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
|
||||
private static bool TryGetLuidAndEngine(string instanceName, out long luidKey, out string engineId)
|
||||
{
|
||||
// Instance names look like:
|
||||
// pid_1234_luid_0x00000000_0x0001766D_phys_0_eng_0_engtype_3D
|
||||
// Advance past pid, read the luid, skip phys, then read the engine index.
|
||||
luidKey = 0;
|
||||
engineId = string.Empty;
|
||||
|
||||
var counterKey = instanceName;
|
||||
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
|
||||
var luid = GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
|
||||
GetKeyValueFromCounterKey(KeyPhys, ref counterKey);
|
||||
engineId = GetKeyValueFromCounterKey(KeyEng, ref counterKey);
|
||||
|
||||
if (string.IsNullOrEmpty(engineId) || engineId == "error")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseLuidKey(luid, out luidKey);
|
||||
}
|
||||
|
||||
private static bool TryParseLuidKey(string luid, out long luidKey)
|
||||
{
|
||||
luidKey = 0;
|
||||
|
||||
// The luid token is "0x{HighPart}_0x{LowPart}", matching DXGI's
|
||||
// LUID.HighPart / LUID.LowPart so the key lines up with GpuAdapterNames.
|
||||
var separator = luid.IndexOf('_');
|
||||
if (separator < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseHex(luid.AsSpan(0, separator), out var high) ||
|
||||
!TryParseHex(luid.AsSpan(separator + 1), out var low))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
luidKey = ((long)high << 32) | low;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseHex(ReadOnlySpan<char> token, out uint value)
|
||||
{
|
||||
if (token.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
token = token[2..];
|
||||
}
|
||||
|
||||
return uint.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
private static string GetKeyValueFromCounterKey(string key, ref string counterKey)
|
||||
{
|
||||
if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Graphics.Dxgi;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves friendly GPU adapter names (and whether an adapter is a software
|
||||
/// renderer) keyed by adapter LUID, using DXGI.
|
||||
///
|
||||
/// The "GPU Engine" performance counters identify each physical adapter by its
|
||||
/// LUID, but not by name, so we enumerate DXGI adapters once and match them up.
|
||||
/// We can't use WMI for this (it isn't AOT-compatible), but DXGI via CsWin32 is.
|
||||
/// </summary>
|
||||
internal static class GpuAdapterNames
|
||||
{
|
||||
internal readonly record struct AdapterInfo(string Description, bool IsSoftware);
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the system's DXGI adapters and returns their descriptions keyed
|
||||
/// by LUID. The key matches <see cref="GPUStats"/>'s LUID parsing:
|
||||
/// <c>(HighPart << 32) | LowPart</c>. Returns an empty map on any failure;
|
||||
/// callers fall back to generic names.
|
||||
/// </summary>
|
||||
internal static unsafe Dictionary<long, AdapterInfo> GetByLuid()
|
||||
{
|
||||
var adapters = new Dictionary<long, AdapterInfo>();
|
||||
|
||||
IDXGIFactory1* factory = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (PInvoke.CreateDXGIFactory1(IDXGIFactory1.IID_Guid, out var factoryPtr).Failed || factoryPtr is null)
|
||||
{
|
||||
return adapters;
|
||||
}
|
||||
|
||||
factory = (IDXGIFactory1*)factoryPtr;
|
||||
|
||||
for (uint index = 0; ; index++)
|
||||
{
|
||||
IDXGIAdapter1* adapter = null;
|
||||
|
||||
// EnumAdapters1 returns DXGI_ERROR_NOT_FOUND once we walk past the
|
||||
// last adapter, which surfaces here as a failed HRESULT.
|
||||
if (factory->EnumAdapters1(index, &adapter).Failed || adapter is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DXGI_ADAPTER_DESC1 desc = default;
|
||||
if (adapter->GetDesc1(&desc).Failed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var luidKey = ((long)(uint)desc.AdapterLuid.HighPart << 32) | desc.AdapterLuid.LowPart;
|
||||
var isSoftware = (desc.Flags & DXGI_ADAPTER_FLAG.DXGI_ADAPTER_FLAG_SOFTWARE) != 0;
|
||||
|
||||
// __char_128.ToString() is generated as
|
||||
// AsReadOnlySpan().SliceAtNull().ToString(), so it already
|
||||
// stops at the null terminator and yields the friendly name.
|
||||
var description = desc.Description.ToString();
|
||||
|
||||
adapters[luidKey] = new AdapterInfo(description, isSoftware);
|
||||
}
|
||||
finally
|
||||
{
|
||||
adapter->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to enumerate DXGI adapters for GPU names.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (factory is not null)
|
||||
{
|
||||
factory->Release();
|
||||
}
|
||||
}
|
||||
|
||||
return adapters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": false,
|
||||
"comInterop": {
|
||||
"preserveSigMethods": [ "*" ]
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
GlobalMemoryStatusEx
|
||||
GetSystemPowerStatus
|
||||
CreateDXGIFactory1
|
||||
IDXGIFactory1
|
||||
IDXGIAdapter1
|
||||
DXGI_ADAPTER_DESC1
|
||||
|
||||
@@ -175,6 +175,14 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
|
||||
_gpuPage.Updated += (s, e) =>
|
||||
{
|
||||
_gpuItem.Title = _gpuPage.GetItemTitle(isBandPage);
|
||||
if (_isBandPage)
|
||||
{
|
||||
// Bands only show the usage percentage as the title, so put
|
||||
// the active GPU's name in the subtitle - otherwise cycling
|
||||
// Prev/Next GPU between two idle adapters looks like nothing
|
||||
// changed.
|
||||
_gpuItem.Subtitle = _gpuPage.GetBandSubtitle();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1048,6 +1056,16 @@ internal sealed partial class SystemGPUUsageWidgetPage : WidgetPage, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public string GetBandSubtitle()
|
||||
{
|
||||
if (ContentData.TryGetValue("gpuName", out var name) && !string.IsNullOrEmpty(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return Resources.GetResource("GPU_Usage_Subtitle");
|
||||
}
|
||||
|
||||
internal override void PushActivate()
|
||||
{
|
||||
base.PushActivate();
|
||||
|
||||
Reference in New Issue
Block a user