Refactor DDC/CI monitor identification to use QueryDisplayConfig

Switch monitor discovery from EnumDisplayDevices to QueryDisplayConfig for stable identification using hardware ID and monitor number. Simplify CandidateMonitor structure and remove DisplayDeviceInfo and related matching logic. Update device key format to "{HardwareId}_{MonitorNumber}" for improved handle management. Rewrite CreateMonitorFromPhysical to use MonitorDisplayInfo directly. Update documentation and remove obsolete helpers for better reliability and maintainability.
This commit is contained in:
Yu Leng
2025-12-10 06:47:39 +08:00
parent 0bc59e7101
commit 725ac65450
4 changed files with 52 additions and 323 deletions

View File

@@ -31,21 +31,14 @@ namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Represents a candidate monitor discovered during Phase 1 of monitor enumeration.
/// This record replaces the long tuple for better readability and maintainability.
/// </summary>
/// <param name="Handle">Physical monitor handle for DDC/CI communication</param>
/// <param name="DeviceKey">Stable device key for handle reuse across discoveries</param>
/// <param name="PhysicalMonitor">Native physical monitor structure with description</param>
/// <param name="AdapterName">Display adapter name (e.g., "\\.\DISPLAY1")</param>
/// <param name="Index">Index of this monitor on its adapter</param>
/// <param name="MatchedDevice">Optional matched DisplayDeviceInfo with EDID data</param>
/// <param name="MonitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
private readonly record struct CandidateMonitor(
IntPtr Handle,
string DeviceKey,
PHYSICAL_MONITOR PhysicalMonitor,
string AdapterName,
int Index,
DisplayDeviceInfo? MatchedDevice);
MonitorDisplayInfo MonitorInfo);
/// <summary>
/// Delay between retry attempts for DDC/CI operations (in milliseconds)
@@ -382,14 +375,8 @@ namespace PowerDisplay.Common.Drivers.DDC
{
try
{
// Pre-fetch display information
var displayDevices = DdcCiNative.GetAllDisplayDevices();
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
// Pre-group devices by adapter for O(1) lookup instead of O(n) per monitor
var devicesByAdapter = displayDevices
.GroupBy(d => d.AdapterName)
.ToDictionary(g => g.Key, g => g.ToList());
// Get monitor display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)
var monitorDisplayInfoList = DdcCiNative.GetAllMonitorDisplayInfo().Values.ToList();
// Phase 1: Collect candidate monitors
var monitorHandles = EnumerateMonitorHandles();
@@ -399,7 +386,7 @@ namespace PowerDisplay.Common.Drivers.DDC
}
var candidateMonitors = await CollectCandidateMonitorsAsync(
monitorHandles, devicesByAdapter, cancellationToken);
monitorHandles, monitorDisplayInfoList, cancellationToken);
if (candidateMonitors.Count == 0)
{
@@ -411,7 +398,7 @@ namespace PowerDisplay.Common.Drivers.DDC
candidateMonitors, cancellationToken);
// Phase 3: Create monitor objects
return CreateValidMonitors(fetchResults, monitorDisplayInfo);
return CreateValidMonitors(fetchResults);
}
catch (Exception ex)
{
@@ -445,23 +432,18 @@ namespace PowerDisplay.Common.Drivers.DDC
/// <summary>
/// Phase 1: Collect all candidate monitors with their physical handles.
/// Uses pre-grouped device lookup for better performance.
/// Pairs each physical monitor with its corresponding MonitorDisplayInfo by index.
/// </summary>
private async Task<List<CandidateMonitor>> CollectCandidateMonitorsAsync(
List<IntPtr> monitorHandles,
Dictionary<string, List<DisplayDeviceInfo>> devicesByAdapter,
List<MonitorDisplayInfo> monitorDisplayInfoList,
CancellationToken cancellationToken)
{
var candidates = new List<CandidateMonitor>();
int monitorIndex = 0;
foreach (var hMonitor in monitorHandles)
{
var adapterName = _discoveryHelper.GetMonitorDeviceId(hMonitor);
if (string.IsNullOrEmpty(adapterName))
{
continue;
}
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
if (physicalMonitors == null || physicalMonitors.Length == 0)
{
@@ -469,53 +451,31 @@ namespace PowerDisplay.Common.Drivers.DDC
continue;
}
// Get devices for this adapter (O(1) lookup)
var adapterDevices = devicesByAdapter.TryGetValue(adapterName, out var devices)
? devices
: null;
candidates.AddRange(
CreateCandidatesFromPhysicalMonitors(physicalMonitors, adapterName, adapterDevices));
}
return candidates;
}
/// <summary>
/// Create candidate monitors from physical monitor array.
/// Handles device matching and handle reuse.
/// Note: NULL handles are already filtered out by GetPhysicalMonitors.
/// </summary>
private IEnumerable<CandidateMonitor> CreateCandidatesFromPhysicalMonitors(
PHYSICAL_MONITOR[] physicalMonitors,
string adapterName,
List<DisplayDeviceInfo>? adapterDevices)
foreach (var physicalMonitor in physicalMonitors)
{
for (int i = 0; i < physicalMonitors.Length; i++)
{
var physicalMonitor = physicalMonitors[i];
// Get MonitorDisplayInfo by index (from QueryDisplayConfig)
var monitorInfo = monitorIndex < monitorDisplayInfoList.Count
? monitorDisplayInfoList[monitorIndex]
: new MonitorDisplayInfo { MonitorNumber = monitorIndex + 1 };
// O(1) lookup: devices are already filtered by adapter
var matchedDevice = adapterDevices != null && i < adapterDevices.Count
? adapterDevices[i]
: null;
// Generate stable device key: "{HardwareId}_{MonitorNumber}"
var deviceKey = !string.IsNullOrEmpty(monitorInfo.HardwareId)
? $"{monitorInfo.HardwareId}_{monitorInfo.MonitorNumber}"
: $"Unknown_{monitorInfo.MonitorNumber}";
var deviceKey = matchedDevice?.DeviceKey ?? $"{adapterName}_{i}";
var (handleToUse, _) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor);
var monitorToCreate = physicalMonitor;
monitorToCreate.HPhysicalMonitor = handleToUse;
yield return new CandidateMonitor(
handleToUse,
deviceKey,
monitorToCreate,
adapterName,
i,
matchedDevice);
candidates.Add(new CandidateMonitor(handleToUse, monitorToCreate, monitorInfo));
monitorIndex++;
}
}
return candidates;
}
/// <summary>
/// Phase 2: Fetch DDC/CI capabilities in parallel for all candidate monitors.
/// This is the slow I2C operation (~4s per monitor), but parallelization
@@ -543,8 +503,7 @@ namespace PowerDisplay.Common.Drivers.DDC
/// A monitor is valid if it has capabilities with brightness support.
/// </summary>
private List<Monitor> CreateValidMonitors(
(CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults,
Dictionary<string, MonitorDisplayInfo> monitorDisplayInfo)
(CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults)
{
var monitors = new List<Monitor>();
var newHandleMap = new Dictionary<string, IntPtr>();
@@ -559,10 +518,7 @@ namespace PowerDisplay.Common.Drivers.DDC
var monitor = _discoveryHelper.CreateMonitorFromPhysical(
candidate.PhysicalMonitor,
candidate.AdapterName,
candidate.Index,
monitorDisplayInfo,
candidate.MatchedDevice);
candidate.MonitorInfo);
if (monitor == null)
{

View File

@@ -26,20 +26,6 @@ using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Display device information class
/// </summary>
public class DisplayDeviceInfo
{
public string DeviceName { get; set; } = string.Empty;
public string AdapterName { get; set; } = string.Empty;
public string DeviceID { get; set; } = string.Empty;
public string DeviceKey { get; set; } = string.Empty;
}
/// <summary>
/// DDC/CI validation result containing both validation status and cached capabilities data.
/// This allows reusing capabilities data retrieved during validation, avoiding duplicate I2C calls.
@@ -512,94 +498,6 @@ namespace PowerDisplay.Common.Drivers.DDC
return monitorInfo;
}
/// <summary>
/// Get all display device information using EnumDisplayDevices API
/// </summary>
/// <returns>List of display device information</returns>
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
{
var devices = new List<DisplayDeviceInfo>();
try
{
// Enumerate all adapters
uint adapterIndex = 0;
var adapter = default(DISPLAY_DEVICE);
adapter.Cb = (uint)sizeof(DisplayDevice);
while (EnumDisplayDevices(null, adapterIndex, ref adapter, EddGetDeviceInterfaceName))
{
// Skip mirroring drivers
if ((adapter.StateFlags & DisplayDeviceMirroringDriver) != 0)
{
adapterIndex++;
adapter = default(DISPLAY_DEVICE);
adapter.Cb = (uint)sizeof(DisplayDevice);
continue;
}
// Only process adapters attached to desktop
if ((adapter.StateFlags & DisplayDeviceAttachedToDesktop) != 0)
{
// Enumerate all monitors on this adapter
uint displayIndex = 0;
var display = default(DISPLAY_DEVICE);
display.Cb = (uint)sizeof(DisplayDevice);
string adapterDeviceName = adapter.GetDeviceName();
while (EnumDisplayDevices(adapterDeviceName, displayIndex, ref display, EddGetDeviceInterfaceName))
{
string displayDeviceID = display.GetDeviceID();
// Only process active monitors
if ((display.StateFlags & DisplayDeviceAttachedToDesktop) != 0 &&
!string.IsNullOrEmpty(displayDeviceID))
{
var deviceInfo = new DisplayDeviceInfo
{
DeviceName = display.GetDeviceName(),
AdapterName = adapterDeviceName,
DeviceID = displayDeviceID,
};
// Extract DeviceKey: remove GUID part (#{...} and everything after)
// Example: \\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID} -> \\?\DISPLAY#GSM5C6D#5&1234&0&UID
int guidIndex = deviceInfo.DeviceID.IndexOf("#{", StringComparison.Ordinal);
if (guidIndex >= 0)
{
deviceInfo.DeviceKey = deviceInfo.DeviceID.Substring(0, guidIndex);
}
else
{
deviceInfo.DeviceKey = deviceInfo.DeviceID;
}
devices.Add(deviceInfo);
Logger.LogDebug($"Found display device - Name: {deviceInfo.DeviceName}, Adapter: {deviceInfo.AdapterName}, DeviceKey: {deviceInfo.DeviceKey}");
}
displayIndex++;
display = default(DISPLAY_DEVICE);
display.Cb = (uint)sizeof(DisplayDevice);
}
}
adapterIndex++;
adapter = default(DISPLAY_DEVICE);
adapter.Cb = (uint)sizeof(DisplayDevice);
}
Logger.LogInfo($"GetAllDisplayDevices found {devices.Count} display devices");
}
catch (Exception ex)
{
Logger.LogError($"GetAllDisplayDevices exception: {ex.Message}");
}
return devices;
}
}
/// <summary>

View File

@@ -4,9 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ManagedCommon;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
@@ -15,7 +12,6 @@ using static PowerDisplay.Common.Drivers.PInvoke;
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Common.Drivers.DDC
{
@@ -123,90 +119,36 @@ namespace PowerDisplay.Common.Drivers.DDC
}
/// <summary>
/// Create Monitor object from physical monitor
/// Create Monitor object from physical monitor and display info.
/// Uses MonitorDisplayInfo directly from QueryDisplayConfig for stable identification.
/// </summary>
/// <param name="physicalMonitor">Physical monitor structure with handle and description</param>
/// <param name="monitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
internal Monitor? CreateMonitorFromPhysical(
PHYSICAL_MONITOR physicalMonitor,
string adapterName,
int index,
Dictionary<string, MonitorDisplayInfo> monitorDisplayInfo,
DisplayDeviceInfo? displayDevice)
MonitorDisplayInfo monitorInfo)
{
try
{
// Get hardware ID and friendly name from the display info
string hardwareId = string.Empty;
// Get hardware ID and friendly name directly from MonitorDisplayInfo
string hardwareId = monitorInfo.HardwareId ?? string.Empty;
string name = physicalMonitor.GetDescription() ?? string.Empty;
// Step 1: Extract HardwareId from displayDevice.DeviceID
// DeviceID format: \\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID}
// We need to extract "GSM5C6D" (the second segment after DISPLAY#)
string? extractedHardwareId = null;
if (displayDevice != null && !string.IsNullOrEmpty(displayDevice.DeviceID))
// Use FriendlyName from QueryDisplayConfig if available and not generic
if (!string.IsNullOrEmpty(monitorInfo.FriendlyName) &&
!monitorInfo.FriendlyName.Contains("Generic"))
{
extractedHardwareId = ExtractHardwareIdFromDeviceId(displayDevice.DeviceID);
name = monitorInfo.FriendlyName;
}
// Step 2: Find matching MonitorDisplayInfo by HardwareId
MonitorDisplayInfo? matchedInfo = null;
if (!string.IsNullOrEmpty(extractedHardwareId))
{
foreach (var kvp in monitorDisplayInfo.Values)
{
// Match by HardwareId (e.g., "GSM5C6D" matches "GSM5C6D")
if (!string.IsNullOrEmpty(kvp.HardwareId) &&
kvp.HardwareId.Equals(extractedHardwareId, StringComparison.OrdinalIgnoreCase))
{
matchedInfo = kvp;
break;
}
}
}
// Generate stable device key: "{HardwareId}_{MonitorNumber}"
string deviceKey = !string.IsNullOrEmpty(hardwareId)
? $"{hardwareId}_{monitorInfo.MonitorNumber}"
: $"Unknown_{monitorInfo.MonitorNumber}";
// Step 3: Fallback to first match if no direct match found (for backward compatibility)
if (matchedInfo == null)
{
foreach (var kvp in monitorDisplayInfo.Values)
{
if (!string.IsNullOrEmpty(kvp.HardwareId))
{
matchedInfo = kvp;
break;
}
}
}
// Step 4: Use matched info
if (matchedInfo.HasValue)
{
hardwareId = matchedInfo.Value.HardwareId;
if (!string.IsNullOrEmpty(matchedInfo.Value.FriendlyName) &&
!matchedInfo.Value.FriendlyName.Contains("Generic"))
{
name = matchedInfo.Value.FriendlyName;
}
}
// Use stable device IDs from DisplayDeviceInfo
string deviceKey;
string monitorId;
if (displayDevice != null && !string.IsNullOrEmpty(displayDevice.DeviceKey))
{
// Use stable device key from EnumDisplayDevices
deviceKey = displayDevice.DeviceKey;
monitorId = $"DDC_{deviceKey.Replace(@"\\?\", string.Empty, StringComparison.Ordinal).Replace("#", "_", StringComparison.Ordinal).Replace("&", "_", StringComparison.Ordinal)}";
}
else
{
// Fallback: create device ID without handle in the key
var baseDevice = adapterName.Replace(@"\\.\", string.Empty, StringComparison.Ordinal);
deviceKey = $"{baseDevice}_{index}";
monitorId = $"DDC_{deviceKey}";
}
string monitorId = $"DDC_{deviceKey}";
// If still no good name, use default value
// Note: Don't include index in the name - let DisplayName property handle numbering
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
{
name = "External Display";
@@ -231,8 +173,8 @@ namespace PowerDisplay.Common.Drivers.DDC
CommunicationMethod = "DDC/CI",
Manufacturer = ExtractManufacturer(name),
CapabilitiesStatus = "unknown",
MonitorNumber = GetMonitorNumber(matchedInfo),
Orientation = GetMonitorOrientation(adapterName),
MonitorNumber = monitorInfo.MonitorNumber,
Orientation = DmdoDefault, // Orientation will be set separately if needed
};
// Note: Feature detection (brightness, contrast, color temp, volume) is now done
@@ -247,37 +189,6 @@ namespace PowerDisplay.Common.Drivers.DDC
}
}
/// <summary>
/// Extract HardwareId from DeviceID string.
/// DeviceID format: \\?\DISPLAY#GSM5C6D#5&amp;1234&amp;0&amp;UID#{GUID}
/// Returns the second segment (e.g., "GSM5C6D") which is the manufacturer+product code.
/// </summary>
private static string? ExtractHardwareIdFromDeviceId(string deviceId)
{
if (string.IsNullOrEmpty(deviceId))
{
return null;
}
// Find "DISPLAY#" and extract the next segment before the next "#"
const string displayPrefix = "DISPLAY#";
int startIndex = deviceId.IndexOf(displayPrefix, StringComparison.OrdinalIgnoreCase);
if (startIndex < 0)
{
return null;
}
startIndex += displayPrefix.Length;
int endIndex = deviceId.IndexOf('#', startIndex);
if (endIndex < 0)
{
return null;
}
var hardwareId = deviceId.Substring(startIndex, endIndex - startIndex);
return string.IsNullOrEmpty(hardwareId) ? null : hardwareId;
}
/// <summary>
/// Get current brightness using VCP code 0x10
/// </summary>
@@ -317,42 +228,5 @@ namespace PowerDisplay.Common.Drivers.DDC
var firstWord = name.Split(' ')[0];
return firstWord.Length > 2 ? firstWord : "Unknown";
}
/// <summary>
/// Get monitor number from MonitorDisplayInfo (QueryDisplayConfig path index).
/// This matches the number shown in Windows Display Settings "Identify" feature.
/// </summary>
private int GetMonitorNumber(MonitorDisplayInfo? matchedInfo)
{
if (matchedInfo.HasValue && matchedInfo.Value.MonitorNumber > 0)
{
return matchedInfo.Value.MonitorNumber;
}
// No match found - return 0 (will not display number suffix)
return 0;
}
/// <summary>
/// Get monitor orientation using EnumDisplaySettings
/// </summary>
private unsafe int GetMonitorOrientation(string adapterName)
{
try
{
DevMode devMode = default;
devMode.DmSize = (short)sizeof(DevMode);
if (EnumDisplaySettings(adapterName, EnumCurrentSettings, &devMode))
{
return devMode.DmDisplayOrientation;
}
}
catch
{
// Ignore errors
}
return DmdoDefault;
}
}
}

View File

@@ -243,12 +243,13 @@ namespace PowerDisplay.Common.Models
public IntPtr Handle { get; set; } = IntPtr.Zero;
/// <summary>
/// Windows device path fragment for physical monitor handle management.
/// Stable device key for persistent monitor identification and handle management.
/// </summary>
/// <remarks>
/// Format: Registry-style path from DisplayDeviceInfo (e.g., "\\?\DISPLAY#GSM5C6D#...").
/// Format: "{HardwareId}_{MonitorNumber}" (e.g., "GSM5C6D_1").
/// HardwareId is from EDID (manufacturer+product code), MonitorNumber from QueryDisplayConfig.
/// Used by PhysicalMonitorHandleManager to reuse handles across monitor discovery cycles.
/// Changes when monitor is reconnected to a different port.
/// Same-model monitors on different ports will have different keys due to MonitorNumber.
/// </remarks>
public string DeviceKey { get; set; } = string.Empty;