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.
This commit is contained in:
Yu Leng
2025-11-27 17:34:44 +08:00
parent 04c1a2cac9
commit 79c155e422
4 changed files with 283 additions and 40 deletions

View File

@@ -47,7 +47,9 @@ namespace PowerDisplay.Common.Drivers.DDC
public string Name => "DDC/CI Monitor Controller";
/// <summary>
/// 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.
/// </summary>
public async Task<bool> 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
}
/// <summary>
/// Get monitor capabilities string with retry logic
/// Get monitor capabilities string with retry logic.
/// Uses cached CapabilitiesRaw if available to avoid slow I2C operations.
/// </summary>
public async Task<string> 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
}
/// <summary>
/// Validate monitor connection status
/// Validate monitor connection status.
/// Uses quick VCP read instead of full capabilities retrieval.
/// </summary>
public async Task<bool> 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);
}

View File

@@ -40,6 +40,57 @@ namespace PowerDisplay.Common.Drivers.DDC
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.
/// </summary>
public struct DdcCiValidationResult
{
/// <summary>
/// Gets a value indicating whether the monitor has a valid DDC/CI connection with brightness support.
/// </summary>
public bool IsValid { get; }
/// <summary>
/// Gets the raw capabilities string retrieved during validation.
/// Null if retrieval failed.
/// </summary>
public string? CapabilitiesString { get; }
/// <summary>
/// Gets the parsed VCP capabilities info retrieved during validation.
/// Null if parsing failed.
/// </summary>
public Models.VcpCapabilities? VcpCapabilitiesInfo { get; }
/// <summary>
/// Gets a value indicating whether capabilities retrieval was attempted.
/// True means the result is from an actual attempt (success or failure).
/// </summary>
public bool WasAttempted { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DdcCiValidationResult"/> struct.
/// </summary>
public DdcCiValidationResult(bool isValid, string? capabilitiesString = null, Models.VcpCapabilities? vcpCapabilitiesInfo = null, bool wasAttempted = true)
{
IsValid = isValid;
CapabilitiesString = capabilitiesString;
VcpCapabilitiesInfo = vcpCapabilitiesInfo;
WasAttempted = wasAttempted;
}
/// <summary>
/// Gets an invalid validation result with no cached data.
/// </summary>
public static DdcCiValidationResult Invalid => new(false, null, null, true);
/// <summary>
/// Gets a result indicating validation was not attempted yet.
/// </summary>
public static DdcCiValidationResult NotAttempted => new(false, null, null, false);
}
/// <summary>
/// DDC/CI native API wrapper
/// </summary>
@@ -163,29 +214,131 @@ namespace PowerDisplay.Common.Drivers.DDC
}
/// <summary>
/// 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.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>True if connection is valid</returns>
public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor)
/// <returns>Validation result with capabilities data (or failure status)</returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>Validation result containing status and cached capabilities data</returns>
[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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>True if the monitor responds to VCP queries</returns>
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;
}
}
/// <summary>
/// Try to get capabilities string from a physical monitor handle.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>Capabilities string, or null if failed</returns>
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;
}
}
/// <summary>

View File

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

View File

@@ -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
/// <summary>
/// Initialize monitor DDC/CI capabilities.
/// If capabilities are already cached from discovery phase, only update derived properties.
/// </summary>
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);