mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
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:
@@ -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);
|
||||
candidateMonitors.Add((handleToUse, deviceKey, monitorToCreate, adapterName, i, matchedDevice));
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
monitors.Add(monitor);
|
||||
|
||||
// Store in new map for cleanup
|
||||
newHandleMap[monitor.DeviceKey] = handleToUse;
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,30 +214,132 @@ 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 _))
|
||||
// Try a quick brightness read to verify connection
|
||||
return TryGetMonitorBrightness(hPhysicalMonitor, out _, out _, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
/// Gets the monitor friendly name
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -157,11 +157,20 @@ namespace PowerDisplay.Core
|
||||
IMonitorController controller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Verify if monitor can be controlled
|
||||
// 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)
|
||||
{
|
||||
// Verify if monitor can be controlled (for monitors not validated in discovery phase)
|
||||
if (!await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current brightness
|
||||
await InitializeMonitorBrightnessAsync(monitor, controller, cancellationToken);
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user