Files
ReLibreHardwareMonitor/LibreHardwareMonitorLib/Hardware/D3DDisplayDevice.cs
T
JMR-devandClaude Opus 4.8 1bde5d531d Fix unbounded memory growth during monitoring
While the app was running, working set grew ~20 MB/min (reaching tens of GB
over a day). dotnet-gcdump traced it to COM-callable wrappers accumulating from
the per-tick binding interop: SensorTreeItemViewModel.RefreshValues raised
PropertyChanged for Value/Min/Max/ToolTip on every sensor every update tick, and
ViewModelBase allocated a new PropertyChangedEventArgs per raise. WinUI's binding
engine is native, so each raised event marshals its args across the boundary and
retains a wrapper.

- ViewModelBase: reuse one cached PropertyChangedEventArgs per property name.
- SensorTreeItemViewModel.RefreshValues: raise PropertyChanged only when the
  formatted text actually changed.

Also close a leaked D3DKMT adapter handle: D3DDisplayDevice.GetDeviceInfoByIdentifier
opened the adapter but skipped CloseAdapter on every early-return failure path,
and it runs on each GPU's Update() tick. Close it in a finally block.

Verified with dotnet-gcdump over an 18-minute soak: managed heap and live object
count stay flat (~50 MB) instead of climbing 47 -> 470 MB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:16:40 -05:00

319 lines
13 KiB
C#

// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// All Rights Reserved.
// Ported from: https://github.com/processhacker/processhacker/blob/master/plugins/ExtendedTools/gpumon.c
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Windows.Wdk.Graphics.Direct3D;
using Windows.Win32.Devices.DeviceAndDriverInstallation;
using Windows.Win32.Foundation;
using PInvoke = Windows.Win32.PInvoke;
namespace LibreHardwareMonitor.Hardware;
internal static class D3DDisplayDevice
{
public static unsafe string[] GetDeviceIdentifiers()
{
if (PInvoke.CM_Get_Device_Interface_List_Size(out uint size, PInvoke.GUID_DISPLAY_DEVICE_ARRIVAL, null, CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) !=
CONFIGRET.CR_SUCCESS)
return null;
char[] data = new char[size];
fixed (char* pData = data)
{
if (PInvoke.CM_Get_Device_Interface_List(PInvoke.GUID_DISPLAY_DEVICE_ARRIVAL, null, pData, (uint)data.Length, CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) ==
CONFIGRET.CR_SUCCESS)
{
return new string(data).Split('\0').Where(m => !string.IsNullOrEmpty(m)).ToArray();
}
}
return null;
}
public static string GetActualDeviceIdentifier(string deviceIdentifier)
{
if (string.IsNullOrEmpty(deviceIdentifier))
return deviceIdentifier;
// For example:
// \\?\ROOT#BasicRender#0000#{1ca05180-a699-450a-9a0c-de4fbe3ddd89} --> ROOT\BasicRender\0000
// \\?\PCI#VEN_1002&DEV_731F&SUBSYS_57051682&REV_C4#6&e539058&0&00000019#{1ca05180-a699-450a-9a0c-de4fbe3ddd89} --> PCI\VEN_1002&DEV_731F&SUBSYS_57051682&REV_C4\6&e539058&0&00000019
if (deviceIdentifier.StartsWith(@"\\?\"))
deviceIdentifier = deviceIdentifier.Substring(4);
if (deviceIdentifier.Length > 0 && deviceIdentifier[deviceIdentifier.Length - 1] == '}')
{
int lastIndex = deviceIdentifier.LastIndexOf('{');
if (lastIndex > 0)
deviceIdentifier = deviceIdentifier.Substring(0, lastIndex - 1);
}
return deviceIdentifier.Replace('#', '\\');
}
public static bool GetDeviceInfoByIdentifier(string deviceIdentifier, out D3DDeviceInfo deviceInfo)
{
deviceInfo = new D3DDeviceInfo();
OpenAdapterFromDeviceName(out NTSTATUS status, deviceIdentifier, out D3DKMT_OPENADAPTERFROMDEVICENAME adapter);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
// Once the adapter is open it must be closed on every exit path. The guard clauses below bail out on the first
// failed D3DKMT query, and this method runs on every GPU's Update() tick, so a missing close leaks a kernel
// adapter handle (and its driver-side allocations) once per tick — unbounded over time. Close it in finally.
try
{
GetAdapterType(out status, adapter, out D3DKMT_ADAPTERTYPE adapterType);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
if (adapterType.Anonymous.Anonymous.SoftwareDevice)
return false;
deviceInfo.Integrated = adapterType.Anonymous.Anonymous.HybridIntegrated;
GetQueryStatisticsAdapterInformation(out status, adapter, out D3DKMT_QUERYSTATISTICS_ADAPTER_INFORMATION adapterInformation);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
uint segmentCount = adapterInformation.NbSegments;
uint nodeCount = adapterInformation.NodeCount;
deviceInfo.Nodes = new D3DDeviceNodeInfo[nodeCount];
DateTime queryTime = DateTime.Now;
for (uint nodeId = 0; nodeId < nodeCount; nodeId++)
{
GetNodeMetaData(out status, adapter, nodeId, out D3DKMT_NODEMETADATA nodeMetaData);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
GetQueryStatisticsNode(out status, adapter, nodeId, out D3DKMT_QUERYSTATISTICS_NODE_INFORMATION nodeInformation);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
deviceInfo.Nodes[nodeId] = new D3DDeviceNodeInfo
{
Id = nodeId,
Name = GetNodeEngineTypeString(nodeMetaData),
RunningTime = nodeInformation.GlobalInformation.RunningTime,
QueryTime = queryTime
};
}
GetSegmentSize(out status, adapter, out D3DKMT_SEGMENTSIZEINFO segmentSizeInfo);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
deviceInfo.GpuSharedLimit = segmentSizeInfo.SharedSystemMemorySize;
deviceInfo.GpuVideoMemoryLimit = segmentSizeInfo.DedicatedVideoMemorySize;
deviceInfo.GpuDedicatedLimit = segmentSizeInfo.DedicatedSystemMemorySize;
for (uint segmentId = 0; segmentId < segmentCount; segmentId++)
{
GetQueryStatisticsSegment(out status, adapter, segmentId, out D3DKMT_QUERYSTATISTICS_SEGMENT_INFORMATION segmentInformation);
if (status != NTSTATUS.STATUS_SUCCESS)
return false;
ulong bytesResident = segmentInformation.BytesResident;
ulong bytesCommitted = segmentInformation.BytesCommitted;
uint aperture = segmentInformation.Aperture;
if (aperture == 1)
{
deviceInfo.GpuSharedUsed += bytesResident;
deviceInfo.GpuSharedMax += bytesCommitted;
}
else
{
deviceInfo.GpuDedicatedUsed += bytesResident;
deviceInfo.GpuDedicatedMax += bytesCommitted;
}
}
return true;
}
finally
{
CloseAdapter(out _, adapter);
}
}
private static string GetNodeEngineTypeString(D3DKMT_NODEMETADATA nodeMetaData)
{
return nodeMetaData.NodeData.EngineType switch
{
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_OTHER => GetOtherName(),
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_3D => "D3D 3D",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_VIDEO_DECODE => "D3D Video Decode",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_VIDEO_ENCODE => "D3D Video Encode",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_VIDEO_PROCESSING => "D3D Video Processing",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_SCENE_ASSEMBLY => "D3D Scene Assembly",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_COPY => "D3D Copy",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_OVERLAY => "D3D Overlay",
DXGK_ENGINE_TYPE.DXGK_ENGINE_TYPE_CRYPTO => "D3D Crypto",
_ => "D3D Unknown"
};
string GetOtherName()
{
string friendlyName = (nodeMetaData.NodeData.FriendlyName.Length > 0 ? nodeMetaData.NodeData.FriendlyName : "Other").ToString();
if (friendlyName.StartsWith("JPEG_Decode_"))
friendlyName = friendlyName.Replace("JPEG_Decode_", "JPEG Decode ");
if (friendlyName.StartsWith("OFA_"))
friendlyName = friendlyName.Replace("OFA_", "Optical Flow Accelerator ");
return "D3D " + friendlyName;
}
}
private static unsafe void GetSegmentSize
(
out NTSTATUS status,
D3DKMT_OPENADAPTERFROMDEVICENAME adapter,
out D3DKMT_SEGMENTSIZEINFO sizeInformation)
{
IntPtr segmentSizePtr = Marshal.AllocHGlobal(sizeof(D3DKMT_SEGMENTSIZEINFO));
var queryAdapterInfo = new D3DKMT_QUERYADAPTERINFO
{
hAdapter = adapter.hAdapter,
Type = KMTQUERYADAPTERINFOTYPE.KMTQAITYPE_GETSEGMENTSIZE,
pPrivateDriverData = (void*)segmentSizePtr,
PrivateDriverDataSize = (uint)sizeof(D3DKMT_SEGMENTSIZEINFO)
};
status = Windows.Wdk.PInvoke.D3DKMTQueryAdapterInfo(ref queryAdapterInfo);
sizeInformation = *(D3DKMT_SEGMENTSIZEINFO*)segmentSizePtr;
Marshal.FreeHGlobal(segmentSizePtr);
}
private static unsafe void GetNodeMetaData(out NTSTATUS status, D3DKMT_OPENADAPTERFROMDEVICENAME adapter, uint nodeId, out D3DKMT_NODEMETADATA nodeMetaDataResult)
{
IntPtr nodeMetaDataPtr = Marshal.AllocHGlobal(sizeof(D3DKMT_NODEMETADATA));
D3DKMT_NODEMETADATA* pData = (D3DKMT_NODEMETADATA*)nodeMetaDataPtr;
pData->NodeOrdinalAndAdapterIndex = nodeId;
var queryAdapterInfo = new D3DKMT_QUERYADAPTERINFO
{
hAdapter = adapter.hAdapter,
Type = KMTQUERYADAPTERINFOTYPE.KMTQAITYPE_NODEMETADATA,
pPrivateDriverData = (void*)nodeMetaDataPtr,
PrivateDriverDataSize = (uint)sizeof(D3DKMT_NODEMETADATA)
};
status = Windows.Wdk.PInvoke.D3DKMTQueryAdapterInfo(ref queryAdapterInfo);
nodeMetaDataResult = *(D3DKMT_NODEMETADATA*)nodeMetaDataPtr;
Marshal.FreeHGlobal(nodeMetaDataPtr);
}
private static void GetQueryStatisticsNode(out NTSTATUS status, D3DKMT_OPENADAPTERFROMDEVICENAME adapter, uint nodeId, out D3DKMT_QUERYSTATISTICS_NODE_INFORMATION nodeInformation)
{
var queryStatistics = new D3DKMT_QUERYSTATISTICS
{
AdapterLuid = adapter.AdapterLuid,
Type = D3DKMT_QUERYSTATISTICS_TYPE.D3DKMT_QUERYSTATISTICS_NODE,
Anonymous = new D3DKMT_QUERYSTATISTICS._Anonymous_e__Union { QueryNode = new D3DKMT_QUERYSTATISTICS_QUERY_NODE { NodeId = nodeId } }
};
status = Windows.Wdk.PInvoke.D3DKMTQueryStatistics(queryStatistics);
nodeInformation = queryStatistics.QueryResult.NodeInformation;
}
private static void GetQueryStatisticsSegment
(
out NTSTATUS status,
D3DKMT_OPENADAPTERFROMDEVICENAME adapter,
uint segmentId,
out D3DKMT_QUERYSTATISTICS_SEGMENT_INFORMATION segmentInformation)
{
var queryStatistics = new D3DKMT_QUERYSTATISTICS
{
AdapterLuid = adapter.AdapterLuid,
Type = D3DKMT_QUERYSTATISTICS_TYPE.D3DKMT_QUERYSTATISTICS_SEGMENT,
Anonymous = new D3DKMT_QUERYSTATISTICS._Anonymous_e__Union { QuerySegment = new D3DKMT_QUERYSTATISTICS_QUERY_SEGMENT { SegmentId = segmentId } }
};
status = Windows.Wdk.PInvoke.D3DKMTQueryStatistics(queryStatistics);
segmentInformation = queryStatistics.QueryResult.SegmentInformation;
}
private static void GetQueryStatisticsAdapterInformation
(
out NTSTATUS status,
D3DKMT_OPENADAPTERFROMDEVICENAME adapter,
out D3DKMT_QUERYSTATISTICS_ADAPTER_INFORMATION adapterInformation)
{
var queryStatistics = new D3DKMT_QUERYSTATISTICS { AdapterLuid = adapter.AdapterLuid, Type = D3DKMT_QUERYSTATISTICS_TYPE.D3DKMT_QUERYSTATISTICS_ADAPTER };
status = Windows.Wdk.PInvoke.D3DKMTQueryStatistics(queryStatistics);
adapterInformation = queryStatistics.QueryResult.AdapterInformation;
}
private static unsafe void GetAdapterType(out NTSTATUS status, D3DKMT_OPENADAPTERFROMDEVICENAME adapter, out D3DKMT_ADAPTERTYPE adapterTypeResult)
{
IntPtr adapterTypePtr = Marshal.AllocHGlobal(sizeof(D3DKMT_ADAPTERTYPE));
var queryAdapterInfo = new D3DKMT_QUERYADAPTERINFO
{
hAdapter = adapter.hAdapter, Type = KMTQUERYADAPTERINFOTYPE.KMTQAITYPE_ADAPTERTYPE, pPrivateDriverData = (void*)adapterTypePtr, PrivateDriverDataSize = (uint)sizeof(D3DKMT_ADAPTERTYPE)
};
status = Windows.Wdk.PInvoke.D3DKMTQueryAdapterInfo(ref queryAdapterInfo);
adapterTypeResult = *(D3DKMT_ADAPTERTYPE*)adapterTypePtr;
Marshal.FreeHGlobal(adapterTypePtr);
}
private static unsafe void OpenAdapterFromDeviceName(out NTSTATUS status, string displayDeviceName, out D3DKMT_OPENADAPTERFROMDEVICENAME adapter)
{
fixed (char* pDisplayDeviceName = displayDeviceName)
{
adapter = new D3DKMT_OPENADAPTERFROMDEVICENAME { pDeviceName = new PCWSTR(pDisplayDeviceName) };
status = Windows.Wdk.PInvoke.D3DKMTOpenAdapterFromDeviceName(ref adapter);
}
}
private static void CloseAdapter(out NTSTATUS status, D3DKMT_OPENADAPTERFROMDEVICENAME adapter)
{
var closeAdapter = new D3DKMT_CLOSEADAPTER { hAdapter = adapter.hAdapter };
status = Windows.Wdk.PInvoke.D3DKMTCloseAdapter(closeAdapter);
}
public struct D3DDeviceNodeInfo
{
public ulong Id;
public string Name;
public long RunningTime;
public DateTime QueryTime;
}
public struct D3DDeviceInfo
{
public ulong GpuSharedLimit;
public ulong GpuDedicatedLimit;
public ulong GpuVideoMemoryLimit;
public ulong GpuSharedUsed;
public ulong GpuDedicatedUsed;
public ulong GpuSharedMax;
public ulong GpuDedicatedMax;
public D3DDeviceNodeInfo[] Nodes;
public bool Integrated;
}
}