diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs index d49bec0496..6e328016b2 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs @@ -31,21 +31,14 @@ namespace PowerDisplay.Common.Drivers.DDC { /// /// Represents a candidate monitor discovered during Phase 1 of monitor enumeration. - /// This record replaces the long tuple for better readability and maintainability. /// /// Physical monitor handle for DDC/CI communication - /// Stable device key for handle reuse across discoveries /// Native physical monitor structure with description - /// Display adapter name (e.g., "\\.\DISPLAY1") - /// Index of this monitor on its adapter - /// Optional matched DisplayDeviceInfo with EDID data + /// Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber) private readonly record struct CandidateMonitor( IntPtr Handle, - string DeviceKey, PHYSICAL_MONITOR PhysicalMonitor, - string AdapterName, - int Index, - DisplayDeviceInfo? MatchedDevice); + MonitorDisplayInfo MonitorInfo); /// /// 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 /// /// 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. /// private async Task> CollectCandidateMonitorsAsync( List monitorHandles, - Dictionary> devicesByAdapter, + List monitorDisplayInfoList, CancellationToken cancellationToken) { var candidates = new List(); + 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; + foreach (var physicalMonitor in physicalMonitors) + { + // Get MonitorDisplayInfo by index (from QueryDisplayConfig) + var monitorInfo = monitorIndex < monitorDisplayInfoList.Count + ? monitorDisplayInfoList[monitorIndex] + : new MonitorDisplayInfo { MonitorNumber = monitorIndex + 1 }; - candidates.AddRange( - CreateCandidatesFromPhysicalMonitors(physicalMonitors, adapterName, adapterDevices)); + // Generate stable device key: "{HardwareId}_{MonitorNumber}" + 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; } - /// - /// Create candidate monitors from physical monitor array. - /// Handles device matching and handle reuse. - /// Note: NULL handles are already filtered out by GetPhysicalMonitors. - /// - private IEnumerable CreateCandidatesFromPhysicalMonitors( - PHYSICAL_MONITOR[] physicalMonitors, - string adapterName, - List? 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); - } - } - /// /// 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. /// private List CreateValidMonitors( - (CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults, - Dictionary monitorDisplayInfo) + (CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults) { var monitors = new List(); var newHandleMap = new Dictionary(); @@ -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) { diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs index ec5df906ff..cabe4645ba 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs @@ -26,20 +26,6 @@ using RECT = PowerDisplay.Common.Drivers.Rect; namespace PowerDisplay.Common.Drivers.DDC { - /// - /// Display device information class - /// - 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; - } - /// /// 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; } - - /// - /// Get all display device information using EnumDisplayDevices API - /// - /// List of display device information - public static unsafe List GetAllDisplayDevices() - { - var devices = new List(); - - 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; - } } /// diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs index e4fe07d031..63e32d7f1f 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs @@ -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 } /// - /// Create Monitor object from physical monitor + /// Create Monitor object from physical monitor and display info. + /// Uses MonitorDisplayInfo directly from QueryDisplayConfig for stable identification. /// + /// Physical monitor structure with handle and description + /// Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber) internal Monitor? CreateMonitorFromPhysical( PHYSICAL_MONITOR physicalMonitor, - string adapterName, - int index, - Dictionary 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 } } - /// - /// Extract HardwareId from DeviceID string. - /// DeviceID format: \\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID} - /// Returns the second segment (e.g., "GSM5C6D") which is the manufacturer+product code. - /// - 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; - } - /// /// Get current brightness using VCP code 0x10 /// @@ -317,42 +228,5 @@ namespace PowerDisplay.Common.Drivers.DDC var firstWord = name.Split(' ')[0]; return firstWord.Length > 2 ? firstWord : "Unknown"; } - - /// - /// Get monitor number from MonitorDisplayInfo (QueryDisplayConfig path index). - /// This matches the number shown in Windows Display Settings "Identify" feature. - /// - 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; - } - - /// - /// Get monitor orientation using EnumDisplaySettings - /// - 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; - } } } diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs index 7bdb616449..a03198789d 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs @@ -243,12 +243,13 @@ namespace PowerDisplay.Common.Models public IntPtr Handle { get; set; } = IntPtr.Zero; /// - /// Windows device path fragment for physical monitor handle management. + /// Stable device key for persistent monitor identification and handle management. /// /// - /// 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. /// public string DeviceKey { get; set; } = string.Empty;