2025-10-20 16:22:47 +08:00
|
|
|
// Copyright (c) Microsoft Corporation
|
|
|
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
|
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using ManagedCommon;
|
2025-11-24 18:08:11 +08:00
|
|
|
using PowerDisplay.Common.Interfaces;
|
|
|
|
|
using PowerDisplay.Common.Models;
|
|
|
|
|
using PowerDisplay.Common.Utils;
|
2025-11-24 21:58:34 +08:00
|
|
|
using static PowerDisplay.Common.Drivers.NativeConstants;
|
|
|
|
|
using static PowerDisplay.Common.Drivers.NativeDelegates;
|
|
|
|
|
using static PowerDisplay.Common.Drivers.PInvoke;
|
2025-11-24 18:08:11 +08:00
|
|
|
using Monitor = PowerDisplay.Common.Models.Monitor;
|
2025-10-20 16:22:47 +08:00
|
|
|
|
|
|
|
|
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
|
|
|
|
|
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
|
2025-11-24 21:58:34 +08:00
|
|
|
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
|
|
|
|
|
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
|
|
|
|
|
using RECT = PowerDisplay.Common.Drivers.Rect;
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-24 21:58:34 +08:00
|
|
|
namespace PowerDisplay.Common.Drivers.DDC
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// DDC/CI monitor controller for controlling external monitors
|
|
|
|
|
/// </summary>
|
|
|
|
|
public partial class DdcCiController : IMonitorController, IDisposable
|
|
|
|
|
{
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Represents a candidate monitor discovered during Phase 1 of monitor enumeration.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="Handle">Physical monitor handle for DDC/CI communication</param>
|
|
|
|
|
/// <param name="PhysicalMonitor">Native physical monitor structure with description</param>
|
2025-12-10 06:47:39 +08:00
|
|
|
/// <param name="MonitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
|
2025-12-10 06:21:50 +08:00
|
|
|
private readonly record struct CandidateMonitor(
|
|
|
|
|
IntPtr Handle,
|
|
|
|
|
PHYSICAL_MONITOR PhysicalMonitor,
|
2025-12-10 06:47:39 +08:00
|
|
|
MonitorDisplayInfo MonitorInfo);
|
2025-12-10 06:21:50 +08:00
|
|
|
|
2025-11-24 18:08:11 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Delay between retry attempts for DDC/CI operations (in milliseconds)
|
|
|
|
|
/// </summary>
|
|
|
|
|
private const int RetryDelayMs = 100;
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
private readonly PhysicalMonitorHandleManager _handleManager = new();
|
|
|
|
|
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
|
|
|
|
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
public DdcCiController()
|
|
|
|
|
{
|
2025-11-14 13:17:55 +08:00
|
|
|
_discoveryHelper = new MonitorDiscoveryHelper();
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Name => "DDC/CI Monitor Controller";
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-14 13:17:55 +08:00
|
|
|
/// Get monitor brightness using VCP code 0x10
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
public async Task<VcpFeatureValue> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
|
|
|
ArgumentNullException.ThrowIfNull(monitor);
|
2025-12-10 17:17:13 +08:00
|
|
|
return await GetVcpFeatureAsync(monitor, VcpCodeBrightness, "Brightness", cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-14 13:17:55 +08:00
|
|
|
/// Set monitor brightness using VCP code 0x10
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
2025-12-04 05:44:59 +08:00
|
|
|
public Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
|
2025-12-10 19:04:19 +08:00
|
|
|
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeBrightness, brightness, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set monitor contrast
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
|
2025-12-10 19:04:19 +08:00
|
|
|
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set monitor volume
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
|
2025-12-10 19:04:19 +08:00
|
|
|
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-14 13:17:55 +08:00
|
|
|
/// Get monitor color temperature using VCP code 0x14 (Select Color Preset)
|
|
|
|
|
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
public async Task<VcpFeatureValue> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
|
|
|
ArgumentNullException.ThrowIfNull(monitor);
|
2025-12-10 06:21:50 +08:00
|
|
|
return await GetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, "Color temperature", cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-14 13:17:55 +08:00
|
|
|
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
|
|
|
|
=> SetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, colorTemperature, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-27 14:51:31 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Get current input source using VCP code 0x60
|
|
|
|
|
/// Returns the raw VCP value (e.g., 0x11 for HDMI-1)
|
|
|
|
|
/// </summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
public async Task<VcpFeatureValue> GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
2025-11-27 14:51:31 +08:00
|
|
|
{
|
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
|
|
|
ArgumentNullException.ThrowIfNull(monitor);
|
2025-12-10 06:21:50 +08:00
|
|
|
return await GetVcpFeatureAsync(monitor, VcpCodeInputSource, "Input source", cancellationToken);
|
2025-11-27 14:51:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set input source using VCP code 0x60
|
|
|
|
|
/// </summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
public Task<MonitorOperationResult> SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
|
|
|
|
|
=> SetVcpFeatureAsync(monitor, VcpCodeInputSource, inputSource, cancellationToken);
|
2025-12-10 06:21:50 +08:00
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
/// <summary>
|
2025-11-27 17:34:44 +08:00
|
|
|
/// Get monitor capabilities string with retry logic.
|
|
|
|
|
/// Uses cached CapabilitiesRaw if available to avoid slow I2C operations.
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
|
|
|
|
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
|
|
|
ArgumentNullException.ThrowIfNull(monitor);
|
|
|
|
|
|
2025-11-27 17:34:44 +08:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
return await Task.Run(
|
|
|
|
|
() =>
|
|
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
if (monitor.Handle == IntPtr.Zero)
|
2025-11-14 02:51:43 +08:00
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
try
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
// Step 1: Get capabilities string length with retry
|
|
|
|
|
var length = RetryHelper.ExecuteWithRetry(
|
|
|
|
|
() =>
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
if (GetCapabilitiesStringLength(monitor.Handle, out uint len) && len > 0)
|
2025-11-14 02:51:43 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
return len;
|
2025-11-14 02:51:43 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
|
|
|
|
|
return 0u;
|
|
|
|
|
},
|
|
|
|
|
len => len > 0,
|
|
|
|
|
maxRetries: 3,
|
|
|
|
|
delayMs: RetryDelayMs,
|
|
|
|
|
operationName: "GetCapabilitiesStringLength");
|
|
|
|
|
|
|
|
|
|
if (length == 0)
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
return string.Empty;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
2025-11-14 02:51:43 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
// Step 2: Get actual capabilities string with retry
|
|
|
|
|
var capsString = RetryHelper.ExecuteWithRetry(
|
|
|
|
|
() => TryGetCapabilitiesString(monitor.Handle, length),
|
|
|
|
|
str => !string.IsNullOrEmpty(str),
|
|
|
|
|
maxRetries: 5,
|
|
|
|
|
delayMs: RetryDelayMs,
|
|
|
|
|
operationName: "GetCapabilitiesString");
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(capsString))
|
2025-11-14 02:51:43 +08:00
|
|
|
{
|
2025-12-01 06:09:26 +08:00
|
|
|
Logger.LogDebug($"Got capabilities string (length: {capsString.Length})");
|
2025-11-26 05:02:49 +08:00
|
|
|
return capsString;
|
2025-11-14 02:51:43 +08:00
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"Exception getting capabilities string: {ex.Message}");
|
|
|
|
|
}
|
2025-11-14 02:51:43 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
return string.Empty;
|
|
|
|
|
},
|
|
|
|
|
cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Try to get capabilities string from monitor handle.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private string? TryGetCapabilitiesString(IntPtr handle, uint length)
|
|
|
|
|
{
|
|
|
|
|
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (CapabilitiesRequestAndCapabilitiesReply(handle, buffer, length))
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer);
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-10 06:21:50 +08:00
|
|
|
/// Discover supported monitors using a three-phase approach:
|
|
|
|
|
/// Phase 1: Enumerate and collect candidate monitors with their handles
|
|
|
|
|
/// Phase 2: Fetch DDC/CI capabilities in parallel (slow I2C operations)
|
|
|
|
|
/// Phase 3: Create Monitor objects for valid DDC/CI monitors
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
|
|
|
|
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
2025-12-10 08:43:44 +08:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Get monitor display info from QueryDisplayConfig, keyed by device path (unique per target)
|
|
|
|
|
var allMonitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
|
2025-12-10 06:21:50 +08:00
|
|
|
|
2025-12-10 08:43:44 +08:00
|
|
|
// Phase 1: Collect candidate monitors
|
|
|
|
|
var monitorHandles = EnumerateMonitorHandles();
|
|
|
|
|
if (monitorHandles.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return Enumerable.Empty<Monitor>();
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:43:44 +08:00
|
|
|
var candidateMonitors = await CollectCandidateMonitorsAsync(
|
|
|
|
|
monitorHandles, allMonitorDisplayInfo, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:43:44 +08:00
|
|
|
if (candidateMonitors.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return Enumerable.Empty<Monitor>();
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:43:44 +08:00
|
|
|
// Phase 2: Fetch capabilities in parallel
|
|
|
|
|
var fetchResults = await FetchCapabilitiesInParallelAsync(
|
|
|
|
|
candidateMonitors, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:43:44 +08:00
|
|
|
// Phase 3: Create monitor objects
|
|
|
|
|
return CreateValidMonitors(fetchResults);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}");
|
|
|
|
|
return Enumerable.Empty<Monitor>();
|
|
|
|
|
}
|
2025-12-10 06:21:50 +08:00
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Enumerate all logical monitor handles using Win32 API.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private List<IntPtr> EnumerateMonitorHandles()
|
|
|
|
|
{
|
|
|
|
|
var handles = new List<IntPtr>();
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
|
|
|
|
|
{
|
|
|
|
|
handles.Add(hMonitor);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
if (!EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("DDC: EnumDisplayMonitors failed");
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
return handles;
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:40:44 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Get GDI device name for a monitor handle (e.g., "\\.\DISPLAY1").
|
|
|
|
|
/// </summary>
|
|
|
|
|
private unsafe string? GetGdiDeviceName(IntPtr hMonitor)
|
|
|
|
|
{
|
|
|
|
|
var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MONITORINFOEX) };
|
|
|
|
|
if (GetMonitorInfo(hMonitor, ref monitorInfo))
|
|
|
|
|
{
|
|
|
|
|
return monitorInfo.GetDeviceName();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Phase 1: Collect all candidate monitors with their physical handles.
|
2025-12-10 08:40:44 +08:00
|
|
|
/// Matches physical monitors with MonitorDisplayInfo using GDI device name and friendly name.
|
|
|
|
|
/// Supports mirror mode where multiple physical monitors share the same GDI name.
|
2025-12-10 06:21:50 +08:00
|
|
|
/// </summary>
|
|
|
|
|
private async Task<List<CandidateMonitor>> CollectCandidateMonitorsAsync(
|
|
|
|
|
List<IntPtr> monitorHandles,
|
2025-12-10 08:40:44 +08:00
|
|
|
Dictionary<string, MonitorDisplayInfo> allMonitorDisplayInfo,
|
2025-12-10 06:21:50 +08:00
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var candidates = new List<CandidateMonitor>();
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
foreach (var hMonitor in monitorHandles)
|
|
|
|
|
{
|
2025-12-10 08:40:44 +08:00
|
|
|
// Get GDI device name for this monitor (e.g., "\\.\DISPLAY1")
|
|
|
|
|
var gdiDeviceName = GetGdiDeviceName(hMonitor);
|
|
|
|
|
if (string.IsNullOrEmpty(gdiDeviceName))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"DDC: Failed to get GDI device name for hMonitor 0x{hMonitor:X}");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
|
|
|
|
|
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
|
|
|
|
{
|
2025-12-10 08:40:44 +08:00
|
|
|
Logger.LogWarning($"DDC: Failed to get physical monitors for {gdiDeviceName} after retries");
|
2025-12-10 06:21:50 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:40:44 +08:00
|
|
|
// Find all MonitorDisplayInfo entries that match this GDI device name
|
|
|
|
|
// In mirror mode, multiple targets share the same GDI name
|
|
|
|
|
var matchingInfos = allMonitorDisplayInfo.Values
|
|
|
|
|
.Where(info => string.Equals(info.GdiDeviceName, gdiDeviceName, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (matchingInfos.Count == 0)
|
2025-12-10 06:47:39 +08:00
|
|
|
{
|
2025-12-10 08:40:44 +08:00
|
|
|
Logger.LogWarning($"DDC: No QueryDisplayConfig info for {gdiDeviceName}, skipping");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 08:40:44 +08:00
|
|
|
for (int i = 0; i < physicalMonitors.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var physicalMonitor = physicalMonitors[i];
|
|
|
|
|
|
|
|
|
|
if (i >= matchingInfos.Count)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"DDC: Physical monitor index {i} exceeds available QueryDisplayConfig entries ({matchingInfos.Count}) for {gdiDeviceName}");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var monitorInfo = matchingInfos[i];
|
|
|
|
|
|
2025-12-10 19:04:19 +08:00
|
|
|
candidates.Add(new CandidateMonitor(physicalMonitor.HPhysicalMonitor, physicalMonitor, monitorInfo));
|
2025-12-10 08:40:44 +08:00
|
|
|
Logger.LogDebug($"DDC: Candidate {gdiDeviceName} -> DevicePath={monitorInfo.DevicePath}, HardwareId={monitorInfo.HardwareId}");
|
2025-12-10 06:47:39 +08:00
|
|
|
}
|
2025-12-10 06:21:50 +08:00
|
|
|
}
|
2025-12-10 06:47:39 +08:00
|
|
|
|
|
|
|
|
return candidates;
|
2025-12-10 06:21:50 +08:00
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Phase 2: Fetch DDC/CI 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.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<(CandidateMonitor Candidate, DdcCiValidationResult Result)[]> FetchCapabilitiesInParallelAsync(
|
|
|
|
|
List<CandidateMonitor> candidates,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogInfo($"DDC: Phase 2 - Fetching capabilities for {candidates.Count} monitors in parallel");
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
var tasks = candidates.Select(candidate =>
|
|
|
|
|
Task.Run(
|
|
|
|
|
() => (Candidate: candidate, Result: DdcCiNative.FetchCapabilities(candidate.Handle)),
|
|
|
|
|
cancellationToken));
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
var results = await Task.WhenAll(tasks);
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
Logger.LogInfo($"DDC: Phase 2 completed - Got results for {results.Length} monitors");
|
|
|
|
|
return results;
|
|
|
|
|
}
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Phase 3: Create Monitor objects for valid DDC/CI monitors.
|
|
|
|
|
/// A monitor is valid if it has capabilities with brightness support.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private List<Monitor> CreateValidMonitors(
|
2025-12-10 06:47:39 +08:00
|
|
|
(CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults)
|
2025-12-10 06:21:50 +08:00
|
|
|
{
|
|
|
|
|
var monitors = new List<Monitor>();
|
|
|
|
|
var newHandleMap = new Dictionary<string, IntPtr>();
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
foreach (var (candidate, capResult) in fetchResults)
|
|
|
|
|
{
|
|
|
|
|
if (!capResult.IsValid)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug($"DDC: Handle 0x{candidate.Handle:X} - No DDC/CI brightness support, skipping");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
var monitor = _discoveryHelper.CreateMonitorFromPhysical(
|
|
|
|
|
candidate.PhysicalMonitor,
|
2025-12-10 06:47:39 +08:00
|
|
|
candidate.MonitorInfo);
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 11:00:36 +08:00
|
|
|
// Set capabilities data
|
2025-12-10 08:40:44 +08:00
|
|
|
if (!string.IsNullOrEmpty(capResult.CapabilitiesString))
|
|
|
|
|
{
|
|
|
|
|
monitor.CapabilitiesRaw = capResult.CapabilitiesString;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (capResult.VcpCapabilitiesInfo != null)
|
|
|
|
|
{
|
|
|
|
|
monitor.VcpCapabilitiesInfo = capResult.VcpCapabilitiesInfo;
|
2025-12-10 11:00:36 +08:00
|
|
|
UpdateMonitorCapabilitiesFromVcp(monitor, capResult.VcpCapabilitiesInfo);
|
|
|
|
|
|
|
|
|
|
// Initialize input source if supported
|
|
|
|
|
if (monitor.SupportsInputSource)
|
|
|
|
|
{
|
|
|
|
|
InitializeInputSource(monitor, candidate.Handle);
|
|
|
|
|
}
|
2025-12-10 11:18:28 +08:00
|
|
|
|
|
|
|
|
// Initialize color temperature if supported
|
|
|
|
|
if (monitor.SupportsColorTemperature)
|
|
|
|
|
{
|
|
|
|
|
InitializeColorTemperature(monitor, candidate.Handle);
|
|
|
|
|
}
|
2025-12-10 08:40:44 +08:00
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
monitors.Add(monitor);
|
2025-12-10 13:34:36 +08:00
|
|
|
newHandleMap[monitor.Id] = candidate.Handle;
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
Logger.LogInfo($"DDC: Added monitor {monitor.Id} with {monitor.VcpCapabilitiesInfo?.SupportedVcpCodes.Count ?? 0} VCP codes");
|
|
|
|
|
}
|
2025-11-27 17:34:44 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
_handleManager.UpdateHandleMap(newHandleMap);
|
|
|
|
|
return monitors;
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 11:00:36 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize input source value for a monitor using VCP 0x60.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static void InitializeInputSource(Monitor monitor, IntPtr handle)
|
|
|
|
|
{
|
2025-12-10 19:04:19 +08:00
|
|
|
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeInputSource, IntPtr.Zero, out uint current, out uint _))
|
2025-12-10 11:00:36 +08:00
|
|
|
{
|
|
|
|
|
monitor.CurrentInputSource = (int)current;
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Input source: {VcpValueNames.GetFormattedName(VcpCodeInputSource, (int)current)}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 11:18:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize color temperature value for a monitor using VCP 0x14.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static void InitializeColorTemperature(Monitor monitor, IntPtr handle)
|
|
|
|
|
{
|
2025-12-10 19:04:19 +08:00
|
|
|
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeSelectColorPreset, IntPtr.Zero, out uint current, out uint _))
|
2025-12-10 11:18:28 +08:00
|
|
|
{
|
|
|
|
|
monitor.CurrentColorTemperature = (int)current;
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Color temperature: {VcpValueNames.GetFormattedName(VcpCodeSelectColorPreset, (int)current)}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 11:00:36 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Update monitor capability flags based on parsed VCP capabilities.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static void UpdateMonitorCapabilitiesFromVcp(Monitor monitor, VcpCapabilities vcpCaps)
|
|
|
|
|
{
|
|
|
|
|
// Check for Contrast support (VCP 0x12)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(VcpCodeContrast))
|
|
|
|
|
{
|
|
|
|
|
monitor.Capabilities |= MonitorCapabilities.Contrast;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Volume support (VCP 0x62)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(VcpCodeVolume))
|
|
|
|
|
{
|
|
|
|
|
monitor.Capabilities |= MonitorCapabilities.Volume;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Color Temperature support (VCP 0x14)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(VcpCodeSelectColorPreset))
|
|
|
|
|
{
|
|
|
|
|
monitor.SupportsColorTemperature = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Capabilities: Contrast={monitor.SupportsContrast}, Volume={monitor.SupportsVolume}, ColorTemp={monitor.SupportsColorTemperature}, InputSource={monitor.SupportsInputSource}");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 01:42:10 +08:00
|
|
|
/// <summary>
|
2025-12-10 06:21:50 +08:00
|
|
|
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles.
|
|
|
|
|
/// NULL handles are automatically filtered out by GetPhysicalMonitors; retry if any were filtered.
|
2025-11-18 01:42:10 +08:00
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="hMonitor">Handle to the monitor</param>
|
|
|
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <returns>Array of valid physical monitors, or null if failed after retries</returns>
|
2025-11-18 01:42:10 +08:00
|
|
|
private async Task<PHYSICAL_MONITOR[]?> GetPhysicalMonitorsWithRetryAsync(
|
|
|
|
|
IntPtr hMonitor,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
const int maxRetries = 3;
|
|
|
|
|
const int retryDelayMs = 200;
|
|
|
|
|
|
|
|
|
|
for (int attempt = 0; attempt < maxRetries; attempt++)
|
|
|
|
|
{
|
|
|
|
|
if (attempt > 0)
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(retryDelayMs, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
var monitors = _discoveryHelper.GetPhysicalMonitors(hMonitor, out bool hasNullHandles);
|
2025-11-18 01:42:10 +08:00
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
// Success: got valid monitors with no NULL handles filtered out
|
|
|
|
|
if (monitors != null && !hasNullHandles)
|
2025-11-18 01:42:10 +08:00
|
|
|
{
|
|
|
|
|
return monitors;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
// Got monitors but some had NULL handles - retry to see if API stabilizes
|
|
|
|
|
if (monitors != null && hasNullHandles && attempt < maxRetries - 1)
|
2025-11-18 01:42:10 +08:00
|
|
|
{
|
2025-12-10 06:21:50 +08:00
|
|
|
Logger.LogWarning($"DDC: Some monitors had NULL handles on attempt {attempt + 1}, will retry");
|
2025-11-18 01:42:10 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
// No monitors returned - retry
|
|
|
|
|
if (monitors == null && attempt < maxRetries - 1)
|
2025-11-18 01:42:10 +08:00
|
|
|
{
|
2025-12-10 06:21:50 +08:00
|
|
|
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null on attempt {attempt + 1}, will retry");
|
|
|
|
|
continue;
|
2025-11-18 01:42:10 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
// Last attempt - return whatever we have (may have NULL handles filtered)
|
|
|
|
|
if (monitors != null && hasNullHandles)
|
2025-11-18 01:42:10 +08:00
|
|
|
{
|
2025-12-10 06:21:50 +08:00
|
|
|
Logger.LogWarning($"DDC: NULL handles still present after {maxRetries} attempts, using filtered result");
|
2025-11-18 01:42:10 +08:00
|
|
|
}
|
2025-12-10 06:21:50 +08:00
|
|
|
|
|
|
|
|
return monitors;
|
2025-11-18 01:42:10 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
return null;
|
2025-11-18 01:42:10 +08:00
|
|
|
}
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
/// <summary>
|
2025-12-10 06:21:50 +08:00
|
|
|
/// Generic method to get VCP feature value with optional logging.
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <param name="monitor">Monitor to query</param>
|
|
|
|
|
/// <param name="vcpCode">VCP code to read</param>
|
|
|
|
|
/// <param name="featureName">Optional feature name for logging (e.g., "color temperature", "input source")</param>
|
|
|
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
2025-12-10 19:04:19 +08:00
|
|
|
private async Task<VcpFeatureValue> GetVcpFeatureAsync(
|
2025-10-20 16:22:47 +08:00
|
|
|
Monitor monitor,
|
|
|
|
|
byte vcpCode,
|
2025-12-10 06:21:50 +08:00
|
|
|
string? featureName = null,
|
2025-10-20 16:22:47 +08:00
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
return await Task.Run(
|
|
|
|
|
() =>
|
|
|
|
|
{
|
|
|
|
|
if (monitor.Handle == IntPtr.Zero)
|
|
|
|
|
{
|
2025-12-10 06:21:50 +08:00
|
|
|
if (featureName != null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Invalid handle for {featureName} read");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 19:04:19 +08:00
|
|
|
return VcpFeatureValue.Invalid;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 19:04:19 +08:00
|
|
|
if (GetVCPFeatureAndVCPFeatureReply(monitor.Handle, vcpCode, IntPtr.Zero, out uint current, out uint max))
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-12-10 06:21:50 +08:00
|
|
|
if (featureName != null)
|
|
|
|
|
{
|
|
|
|
|
var valueName = VcpValueNames.GetFormattedName(vcpCode, (int)current);
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] {featureName} via 0x{vcpCode:X2}: {valueName}");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 19:04:19 +08:00
|
|
|
return new VcpFeatureValue((int)current, 0, (int)max);
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
if (featureName != null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"[{monitor.Id}] Failed to read {featureName} (0x{vcpCode:X2} not supported)");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 19:04:19 +08:00
|
|
|
return VcpFeatureValue.Invalid;
|
2025-10-20 16:22:47 +08:00
|
|
|
},
|
|
|
|
|
cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 06:21:50 +08:00
|
|
|
/// <summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
/// Generic method to set VCP feature value directly.
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
2025-12-10 19:04:19 +08:00
|
|
|
private Task<MonitorOperationResult> SetVcpFeatureAsync(
|
2025-10-20 16:22:47 +08:00
|
|
|
Monitor monitor,
|
|
|
|
|
byte vcpCode,
|
|
|
|
|
int value,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
2025-12-10 19:04:19 +08:00
|
|
|
ArgumentNullException.ThrowIfNull(monitor);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 19:04:19 +08:00
|
|
|
return Task.Run(
|
|
|
|
|
() =>
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
|
|
|
|
if (monitor.Handle == IntPtr.Zero)
|
|
|
|
|
{
|
|
|
|
|
return MonitorOperationResult.Failure("Invalid monitor handle");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-12-10 19:04:19 +08:00
|
|
|
if (SetVCPFeature(monitor.Handle, vcpCode, (uint)value))
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
|
|
|
|
return MonitorOperationResult.Success();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var lastError = GetLastError();
|
|
|
|
|
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", (int)lastError);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return MonitorOperationResult.Failure($"Exception setting VCP 0x{vcpCode:X2}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Dispose(true);
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (!_disposed && disposing)
|
|
|
|
|
{
|
|
|
|
|
_handleManager?.Dispose();
|
|
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|