From 79c155e422f78f4cc2a94e3aadccf8ef7c80dc92 Mon Sep 17 00:00:00 2001 From: Yu Leng Date: Thu, 27 Nov 2025 17:34:44 +0800 Subject: [PATCH] Optimize monitor discovery and validation process Refactored monitor discovery to a two-phase process, enabling parallel capability fetching and reducing total discovery time. Introduced caching of monitor capabilities to avoid redundant I2C operations, improving performance during initialization and runtime validation. Added `DdcCiValidationResult` to encapsulate validation status and cached capabilities. Replaced `ValidateDdcCiConnection` with `FetchCapabilities` for capability retrieval, marking the former as obsolete. Introduced `QuickConnectionCheck` for fast runtime validation. Updated `CanControlMonitorAsync`, `GetCapabilitiesStringAsync`, and `InitializeMonitorCapabilitiesAsync` to leverage cached data. Improved logging for better insights into discovery and validation processes. --- .../Drivers/DDC/DdcCiController.cs | 118 +++++++++--- .../Drivers/DDC/DdcCiNative.cs | 177 ++++++++++++++++-- .../DDC/PhysicalMonitorHandleManager.cs | 3 +- .../PowerDisplay/Core/MonitorManager.cs | 25 ++- 4 files changed, 283 insertions(+), 40 deletions(-) diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs index c5bddae18e..7daae750b5 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs @@ -47,7 +47,9 @@ namespace PowerDisplay.Common.Drivers.DDC public string Name => "DDC/CI Monitor Controller"; /// - /// Check if the specified monitor can be controlled + /// Check if the specified monitor can be controlled. + /// Uses quick connection check if capabilities are already cached, + /// otherwise falls back to full validation. /// public async Task CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default) { @@ -55,7 +57,23 @@ namespace PowerDisplay.Common.Drivers.DDC () => { var physicalHandle = GetPhysicalHandle(monitor); - return physicalHandle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(physicalHandle); + if (physicalHandle == IntPtr.Zero) + { + return false; + } + + // If monitor already has cached capabilities with brightness support, + // use quick connection check instead of full capabilities retrieval + if (monitor.VcpCapabilitiesInfo != null && + monitor.VcpCapabilitiesInfo.SupportsVcpCode(NativeConstants.VcpCodeBrightness)) + { + return DdcCiNative.QuickConnectionCheck(physicalHandle); + } + + // Fall back to full validation for monitors without cached capabilities +#pragma warning disable CS0618 // Suppress obsolete warning - needed for backward compatibility + return DdcCiNative.ValidateDdcCiConnection(physicalHandle).IsValid; +#pragma warning restore CS0618 }, cancellationToken); } @@ -345,10 +363,18 @@ namespace PowerDisplay.Common.Drivers.DDC } /// - /// Get monitor capabilities string with retry logic + /// Get monitor capabilities string with retry logic. + /// Uses cached CapabilitiesRaw if available to avoid slow I2C operations. /// public async Task GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default) { + // Check if capabilities are already cached + if (!string.IsNullOrEmpty(monitor.CapabilitiesRaw)) + { + Logger.LogDebug($"GetCapabilitiesStringAsync: Using cached capabilities for {monitor.Id} (length: {monitor.CapabilitiesRaw.Length})"); + return monitor.CapabilitiesRaw; + } + return await Task.Run( () => { @@ -492,7 +518,9 @@ namespace PowerDisplay.Common.Drivers.DDC return monitors; } - // Get physical handles for each monitor + // Phase 1: Collect all candidate monitors with their handles + var candidateMonitors = new List<(IntPtr Handle, string DeviceKey, PHYSICAL_MONITOR PhysicalMonitor, string AdapterName, int Index, DisplayDeviceInfo? MatchedDevice)>(); + foreach (var hMonitor in monitorHandles) { var adapterName = _discoveryHelper.GetMonitorDeviceId(hMonitor); @@ -511,7 +539,6 @@ namespace PowerDisplay.Common.Drivers.DDC } // Match physical monitors with DisplayDeviceInfo - // For each physical monitor on this adapter, find the corresponding DisplayDeviceInfo for (int i = 0; i < physicalMonitors.Length; i++) { var physicalMonitor = physicalMonitors[i]; @@ -542,29 +569,71 @@ namespace PowerDisplay.Common.Drivers.DDC string deviceKey = matchedDevice?.DeviceKey ?? $"{adapterName}_{i}"; // Use HandleManager to reuse or create handle - var (handleToUse, reusingOldHandle) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor); + var (handleToUse, _) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor); - // Always validate DDC/CI connection, regardless of handle reuse - // This ensures monitors that don't support DDC/CI (e.g., internal laptop displays) - // are not included in the DDC controller's results - if (!DdcCiNative.ValidateDdcCiConnection(handleToUse)) - { - Logger.LogDebug($"DDC: Handle 0x{handleToUse:X} (reused={reusingOldHandle}) failed DDC/CI validation, skipping"); - continue; - } - - // Update physical monitor handle to use the correct one + // Update physical monitor handle var monitorToCreate = physicalMonitor; monitorToCreate.HPhysicalMonitor = handleToUse; - var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice); - if (monitor != null) - { - monitors.Add(monitor); + candidateMonitors.Add((handleToUse, deviceKey, monitorToCreate, adapterName, i, matchedDevice)); + } + } - // Store in new map for cleanup - newHandleMap[monitor.DeviceKey] = handleToUse; + // Phase 2: Fetch capabilities in PARALLEL for all candidate monitors + // This is the slow I2C operation (~4s per monitor), but parallelization + // significantly reduces total time when multiple monitors are connected. + // Results are cached regardless of success/failure. + Logger.LogInfo($"DDC: Phase 2 - Fetching capabilities for {candidateMonitors.Count} monitors in parallel"); + + var fetchTasks = candidateMonitors.Select(candidate => + Task.Run( + () => + { + var capabilitiesResult = DdcCiNative.FetchCapabilities(candidate.Handle); + return (Candidate: candidate, CapabilitiesResult: capabilitiesResult); + }, + cancellationToken)); + + var fetchResults = await Task.WhenAll(fetchTasks); + + Logger.LogInfo($"DDC: Phase 2 completed - Got results for {fetchResults.Length} monitors"); + + // Phase 3: Create monitor objects for valid DDC/CI monitors + // A monitor is valid for DDC if it has capabilities with brightness support + foreach (var result in fetchResults) + { + // Skip monitors that don't support DDC/CI brightness control + if (!result.CapabilitiesResult.IsValid) + { + Logger.LogDebug($"DDC: Handle 0x{result.Candidate.Handle:X} - No DDC/CI brightness support, skipping"); + continue; + } + + var monitor = _discoveryHelper.CreateMonitorFromPhysical( + result.Candidate.PhysicalMonitor, + result.Candidate.AdapterName, + result.Candidate.Index, + monitorDisplayInfo, + result.Candidate.MatchedDevice); + + if (monitor != null) + { + // Attach cached capabilities data - this is the key optimization! + // By caching here, we avoid re-fetching during InitializeMonitorCapabilitiesAsync + if (!string.IsNullOrEmpty(result.CapabilitiesResult.CapabilitiesString)) + { + monitor.CapabilitiesRaw = result.CapabilitiesResult.CapabilitiesString; } + + if (result.CapabilitiesResult.VcpCapabilitiesInfo != null) + { + monitor.VcpCapabilitiesInfo = result.CapabilitiesResult.VcpCapabilitiesInfo; + } + + monitors.Add(monitor); + newHandleMap[monitor.DeviceKey] = result.Candidate.Handle; + + Logger.LogInfo($"DDC: Added monitor {monitor.Id} with {monitor.VcpCapabilitiesInfo?.SupportedVcpCodes.Count ?? 0} VCP codes"); } } @@ -582,12 +651,13 @@ namespace PowerDisplay.Common.Drivers.DDC } /// - /// Validate monitor connection status + /// Validate monitor connection status. + /// Uses quick VCP read instead of full capabilities retrieval. /// public async Task ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default) { return await Task.Run( - () => monitor.Handle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(monitor.Handle), + () => monitor.Handle != IntPtr.Zero && DdcCiNative.QuickConnectionCheck(monitor.Handle), cancellationToken); } diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs index 8a5c58184e..1d200ba0cd 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs @@ -40,6 +40,57 @@ namespace PowerDisplay.Common.Drivers.DDC 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. + /// + public struct DdcCiValidationResult + { + /// + /// Gets a value indicating whether the monitor has a valid DDC/CI connection with brightness support. + /// + public bool IsValid { get; } + + /// + /// Gets the raw capabilities string retrieved during validation. + /// Null if retrieval failed. + /// + public string? CapabilitiesString { get; } + + /// + /// Gets the parsed VCP capabilities info retrieved during validation. + /// Null if parsing failed. + /// + public Models.VcpCapabilities? VcpCapabilitiesInfo { get; } + + /// + /// Gets a value indicating whether capabilities retrieval was attempted. + /// True means the result is from an actual attempt (success or failure). + /// + public bool WasAttempted { get; } + + /// + /// Initializes a new instance of the struct. + /// + public DdcCiValidationResult(bool isValid, string? capabilitiesString = null, Models.VcpCapabilities? vcpCapabilitiesInfo = null, bool wasAttempted = true) + { + IsValid = isValid; + CapabilitiesString = capabilitiesString; + VcpCapabilitiesInfo = vcpCapabilitiesInfo; + WasAttempted = wasAttempted; + } + + /// + /// Gets an invalid validation result with no cached data. + /// + public static DdcCiValidationResult Invalid => new(false, null, null, true); + + /// + /// Gets a result indicating validation was not attempted yet. + /// + public static DdcCiValidationResult NotAttempted => new(false, null, null, false); + } + /// /// DDC/CI native API wrapper /// @@ -163,29 +214,131 @@ namespace PowerDisplay.Common.Drivers.DDC } /// - /// Validates the DDC/CI connection + /// Fetches VCP capabilities string from a monitor and returns a validation result. + /// This is the slow I2C operation (~4 seconds per monitor) that should only be done once. + /// The result is cached regardless of success or failure. /// /// Physical monitor handle - /// True if connection is valid - public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor) + /// Validation result with capabilities data (or failure status) + public static DdcCiValidationResult FetchCapabilities(IntPtr hPhysicalMonitor) + { + if (hPhysicalMonitor == IntPtr.Zero) + { + return DdcCiValidationResult.Invalid; + } + + try + { + // Try to get capabilities string (slow I2C operation) + var capsString = TryGetCapabilitiesString(hPhysicalMonitor); + if (string.IsNullOrEmpty(capsString)) + { + Logger.LogDebug($"FetchCapabilities: Failed to get capabilities string for handle 0x{hPhysicalMonitor:X}"); + return DdcCiValidationResult.Invalid; + } + + // Parse the capabilities string + var capabilities = Utils.VcpCapabilitiesParser.Parse(capsString); + if (capabilities == null || capabilities.SupportedVcpCodes.Count == 0) + { + Logger.LogDebug($"FetchCapabilities: Failed to parse capabilities string for handle 0x{hPhysicalMonitor:X}"); + return DdcCiValidationResult.Invalid; + } + + // Check if brightness (VCP 0x10) is supported - determines DDC/CI validity + bool supportsBrightness = capabilities.SupportsVcpCode(NativeConstants.VcpCodeBrightness); + + Logger.LogDebug($"FetchCapabilities: Handle 0x{hPhysicalMonitor:X} - BrightnessSupport={supportsBrightness}, VcpCodes={capabilities.SupportedVcpCodes.Count}"); + return new DdcCiValidationResult(supportsBrightness, capsString, capabilities); + } + catch (Exception ex) when (ex is not OutOfMemoryException) + { + Logger.LogDebug($"FetchCapabilities: Exception for handle 0x{hPhysicalMonitor:X}: {ex.Message}"); + return DdcCiValidationResult.Invalid; + } + } + + /// + /// Validates the DDC/CI connection by checking if the monitor returns a valid capabilities string + /// that includes brightness control (VCP 0x10). + /// NOTE: This method performs a slow I2C operation. Prefer using FetchCapabilities() during + /// discovery phase and caching the result. + /// + /// Physical monitor handle + /// Validation result containing status and cached capabilities data + [System.Obsolete("Use FetchCapabilities() during discovery and cache results. This method is kept for backward compatibility.")] + public static DdcCiValidationResult ValidateDdcCiConnection(IntPtr hPhysicalMonitor) + { + // Delegate to FetchCapabilities which does the same thing + return FetchCapabilities(hPhysicalMonitor); + } + + /// + /// Quick connection check using a simple VCP read (brightness). + /// This is much faster than full capabilities retrieval (~50ms vs ~4s). + /// Use this for runtime connection validation when capabilities are already cached. + /// + /// Physical monitor handle + /// True if the monitor responds to VCP queries + public static bool QuickConnectionCheck(IntPtr hPhysicalMonitor) { if (hPhysicalMonitor == IntPtr.Zero) { return false; } - // Try reading basic VCP codes to validate connection - var testCodes = new byte[] { NativeConstants.VcpCodeBrightness, NativeConstants.VcpCodeContrast, NativeConstants.VcpCodeVcpVersion, NativeConstants.VcpCodeVolume }; - - foreach (var code in testCodes) + try { - if (TryGetVCPFeature(hPhysicalMonitor, code, out _, out _)) - { - return true; - } + // Try a quick brightness read to verify connection + return TryGetMonitorBrightness(hPhysicalMonitor, out _, out _, out _); + } + catch + { + return false; + } + } + + /// + /// Try to get capabilities string from a physical monitor handle. + /// + /// Physical monitor handle + /// Capabilities string, or null if failed + private static string? TryGetCapabilitiesString(IntPtr hPhysicalMonitor) + { + if (hPhysicalMonitor == IntPtr.Zero) + { + return null; } - return false; + try + { + // Get capabilities string length + if (!GetCapabilitiesStringLength(hPhysicalMonitor, out uint length) || length == 0) + { + return null; + } + + // Allocate buffer and get capabilities string + var buffer = Marshal.AllocHGlobal((int)length); + try + { + if (!CapabilitiesRequestAndCapabilitiesReply(hPhysicalMonitor, buffer, length)) + { + return null; + } + + return Marshal.PtrToStringAnsi(buffer); + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + catch (Exception ex) when (ex is not OutOfMemoryException) + { + Logger.LogDebug($"TryGetCapabilitiesString failed: {ex.Message}"); + return null; + } } /// diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs index 81ac531efd..6aafeb9456 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs @@ -55,9 +55,10 @@ namespace PowerDisplay.Common.Drivers.DDC return _deviceKeyToHandleMap.ExecuteWithLock(dict => { // Try to reuse existing handle if it's still valid + // Use quick connection check instead of full capabilities retrieval if (dict.TryGetValue(deviceKey, out var existingHandle) && existingHandle != IntPtr.Zero && - DdcCiNative.ValidateDdcCiConnection(existingHandle)) + DdcCiNative.QuickConnectionCheck(existingHandle)) { // Destroy the newly created handle since we're using the old one if (newHandle != existingHandle && newHandle != IntPtr.Zero) diff --git a/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs b/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs index 0329249104..23d6f910a3 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs @@ -157,10 +157,19 @@ namespace PowerDisplay.Core IMonitorController controller, CancellationToken cancellationToken) { - // Verify if monitor can be controlled - if (!await controller.CanControlMonitorAsync(monitor, cancellationToken)) + // Skip control verification if monitor was already validated during discovery phase + // The presence of cached VcpCapabilitiesInfo indicates the monitor passed DDC/CI validation + // This avoids redundant capabilities retrieval (~4 seconds per monitor) + bool alreadyValidated = monitor.VcpCapabilitiesInfo != null && + monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0; + + if (!alreadyValidated) { - return null; + // Verify if monitor can be controlled (for monitors not validated in discovery phase) + if (!await controller.CanControlMonitorAsync(monitor, cancellationToken)) + { + return null; + } } // Get current brightness @@ -207,6 +216,7 @@ namespace PowerDisplay.Core /// /// Initialize monitor DDC/CI capabilities. + /// If capabilities are already cached from discovery phase, only update derived properties. /// private async Task InitializeMonitorCapabilitiesAsync( Monitor monitor, @@ -215,6 +225,15 @@ namespace PowerDisplay.Core { try { + // Check if capabilities were already cached during discovery phase + // This avoids expensive I2C calls (~4 seconds per monitor) for redundant data + if (monitor.VcpCapabilitiesInfo != null && monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0) + { + Logger.LogInfo($"Using cached capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes"); + UpdateMonitorCapabilitiesFromVcp(monitor); + return; + } + Logger.LogInfo($"Getting capabilities for monitor {monitor.Id}"); var capsString = await controller.GetCapabilitiesStringAsync(monitor, cancellationToken);