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);