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

View File

@@ -26,20 +26,6 @@ using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Common.Drivers.DDC 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> /// <summary>
/// DDC/CI validation result containing both validation status and cached capabilities data. /// DDC/CI validation result containing both validation status and cached capabilities data.
/// This allows reusing capabilities data retrieved during validation, avoiding duplicate I2C calls. /// This allows reusing capabilities data retrieved during validation, avoiding duplicate I2C calls.
@@ -512,94 +498,6 @@ namespace PowerDisplay.Common.Drivers.DDC
return monitorInfo; 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> /// <summary>

View File

@@ -4,9 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ManagedCommon; using ManagedCommon;
using PowerDisplay.Common.Models; using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils; using PowerDisplay.Common.Utils;
@@ -15,7 +12,6 @@ using static PowerDisplay.Common.Drivers.PInvoke;
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx; using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor; using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Common.Drivers.DDC namespace PowerDisplay.Common.Drivers.DDC
{ {
@@ -123,90 +119,36 @@ namespace PowerDisplay.Common.Drivers.DDC
} }
/// <summary> /// <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> /// </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( internal Monitor? CreateMonitorFromPhysical(
PHYSICAL_MONITOR physicalMonitor, PHYSICAL_MONITOR physicalMonitor,
string adapterName, MonitorDisplayInfo monitorInfo)
int index,
Dictionary<string, MonitorDisplayInfo> monitorDisplayInfo,
DisplayDeviceInfo? displayDevice)
{ {
try try
{ {
// Get hardware ID and friendly name from the display info // Get hardware ID and friendly name directly from MonitorDisplayInfo
string hardwareId = string.Empty; string hardwareId = monitorInfo.HardwareId ?? string.Empty;
string name = physicalMonitor.GetDescription() ?? string.Empty; string name = physicalMonitor.GetDescription() ?? string.Empty;
// Step 1: Extract HardwareId from displayDevice.DeviceID // Use FriendlyName from QueryDisplayConfig if available and not generic
// DeviceID format: \\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID} if (!string.IsNullOrEmpty(monitorInfo.FriendlyName) &&
// We need to extract "GSM5C6D" (the second segment after DISPLAY#) !monitorInfo.FriendlyName.Contains("Generic"))
string? extractedHardwareId = null;
if (displayDevice != null && !string.IsNullOrEmpty(displayDevice.DeviceID))
{ {
extractedHardwareId = ExtractHardwareIdFromDeviceId(displayDevice.DeviceID); name = monitorInfo.FriendlyName;
} }
// Step 2: Find matching MonitorDisplayInfo by HardwareId // Generate stable device key: "{HardwareId}_{MonitorNumber}"
MonitorDisplayInfo? matchedInfo = null; string deviceKey = !string.IsNullOrEmpty(hardwareId)
if (!string.IsNullOrEmpty(extractedHardwareId)) ? $"{hardwareId}_{monitorInfo.MonitorNumber}"
{ : $"Unknown_{monitorInfo.MonitorNumber}";
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;
}
}
}
// Step 3: Fallback to first match if no direct match found (for backward compatibility) string monitorId = $"DDC_{deviceKey}";
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}";
}
// If still no good name, use default value // 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")) if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
{ {
name = "External Display"; name = "External Display";
@@ -231,8 +173,8 @@ namespace PowerDisplay.Common.Drivers.DDC
CommunicationMethod = "DDC/CI", CommunicationMethod = "DDC/CI",
Manufacturer = ExtractManufacturer(name), Manufacturer = ExtractManufacturer(name),
CapabilitiesStatus = "unknown", CapabilitiesStatus = "unknown",
MonitorNumber = GetMonitorNumber(matchedInfo), MonitorNumber = monitorInfo.MonitorNumber,
Orientation = GetMonitorOrientation(adapterName), Orientation = DmdoDefault, // Orientation will be set separately if needed
}; };
// Note: Feature detection (brightness, contrast, color temp, volume) is now done // 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> /// <summary>
/// Get current brightness using VCP code 0x10 /// Get current brightness using VCP code 0x10
/// </summary> /// </summary>
@@ -317,42 +228,5 @@ namespace PowerDisplay.Common.Drivers.DDC
var firstWord = name.Split(' ')[0]; var firstWord = name.Split(' ')[0];
return firstWord.Length > 2 ? firstWord : "Unknown"; 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; public IntPtr Handle { get; set; } = IntPtr.Zero;
/// <summary> /// <summary>
/// Windows device path fragment for physical monitor handle management. /// Stable device key for persistent monitor identification and handle management.
/// </summary> /// </summary>
/// <remarks> /// <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. /// 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> /// </remarks>
public string DeviceKey { get; set; } = string.Empty; public string DeviceKey { get; set; } = string.Empty;