From 391f61d4ed3b3da9351ced70a3562132f00e675b Mon Sep 17 00:00:00 2001 From: moooyo Date: Mon, 1 Dec 2025 06:09:26 +0800 Subject: [PATCH] Fix some issues --- .../Drivers/DDC/DdcCiController.cs | 8 +-- .../Services/ProfileService.cs | 15 ++++- .../PowerDisplay.Lib/Utils/EventHelper.cs | 47 ++++++++++++++++ .../Configuration/AppConstants.cs | 55 +++++++++++++++++++ .../PowerDisplay/PowerDisplayXAML/App.xaml.cs | 31 +++++++++++ .../ViewModels/MainViewModel.Monitors.cs | 5 +- .../ViewModels/MainViewModel.Settings.cs | 18 +----- .../PowerDisplay/ViewModels/MainViewModel.cs | 2 +- .../ViewModels/MonitorViewModel.cs | 7 +++ .../PowerDisplayModuleInterface/Constants.h | 5 ++ .../PowerDisplayModuleInterface/dllmain.cpp | 41 ++++++++++++-- .../ViewModels/PowerDisplayViewModel.cs | 32 ++--------- 12 files changed, 212 insertions(+), 54 deletions(-) create mode 100644 src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs index 17af0ee0c9..285d0811d4 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs @@ -181,7 +181,7 @@ namespace PowerDisplay.Common.Drivers.DDC if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, out uint current, out uint max)) { var presetName = VcpValueNames.GetFormattedName(0x14, (int)current); - Logger.LogInfo($"[{monitor.Id}] Color temperature via 0x14: {presetName}"); + Logger.LogDebug($"[{monitor.Id}] Color temperature via 0x14: {presetName}"); return new BrightnessInfo((int)current, 0, (int)max); } @@ -266,7 +266,7 @@ namespace PowerDisplay.Common.Drivers.DDC if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeInputSource, out uint current, out uint max)) { var sourceName = VcpValueNames.GetFormattedName(0x60, (int)current); - Logger.LogInfo($"[{monitor.Id}] Input source via 0x60: {sourceName}"); + Logger.LogDebug($"[{monitor.Id}] Input source via 0x60: {sourceName}"); return new BrightnessInfo((int)current, 0, (int)max); } @@ -322,7 +322,7 @@ namespace PowerDisplay.Common.Drivers.DDC var verifyName = VcpValueNames.GetFormattedName(0x60, (int)verifyValue); if (verifyValue == (uint)inputSource) { - Logger.LogInfo($"[{monitor.Id}] Input source verified: {verifyName} (0x{verifyValue:X2})"); + Logger.LogDebug($"[{monitor.Id}] Input source verified: {verifyName} (0x{verifyValue:X2})"); } else { @@ -409,7 +409,7 @@ namespace PowerDisplay.Common.Drivers.DDC if (!string.IsNullOrEmpty(capsString)) { - Logger.LogInfo($"Got capabilities string (length: {capsString.Length})"); + Logger.LogDebug($"Got capabilities string (length: {capsString.Length})"); return capsString; } } diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs index 8d5f3d57a8..53d8f7163a 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs @@ -16,8 +16,21 @@ namespace PowerDisplay.Common.Services /// Centralized service for managing PowerDisplay profiles storage and retrieval. /// Provides unified access to profile data for PowerDisplay, Settings UI, and LightSwitch modules. /// Thread-safe and AOT-compatible. - /// Implements for dependency injection support. /// + /// + /// Design Note: This class provides both static and instance-based access patterns: + /// + /// Static methods: Direct access for backward compatibility with existing code. Thread-safe with internal locking. + /// Instance methods (via ): For dependency injection consumers. Delegates to static methods internally. + /// + /// Usage Guidelines: + /// + /// New code should prefer DI: serviceProvider.GetService<IProfileService>() + /// Or use the singleton: ProfileService.Instance.LoadProfiles() + /// Static methods remain available for backward compatibility + /// + /// All access patterns are thread-safe and share the same underlying lock. + /// public class ProfileService : IProfileService { private const string LogPrefix = "[ProfileService]"; diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs new file mode 100644 index 0000000000..3d8a8330da --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs @@ -0,0 +1,47 @@ +// 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.Threading; +using ManagedCommon; + +namespace PowerDisplay.Common.Utils +{ + /// + /// Helper class for Windows named event operations. + /// Provides unified event signaling with consistent error handling and logging. + /// + public static class EventHelper + { + /// + /// Signals a named event. Creates the event if it doesn't exist. + /// + /// The name of the event to signal. + /// True if the event was signaled successfully, false otherwise. + public static bool SignalEvent(string eventName) + { + if (string.IsNullOrEmpty(eventName)) + { + Logger.LogWarning("[EventHelper] SignalEvent called with null or empty event name"); + return false; + } + + try + { + using var eventHandle = new EventWaitHandle( + false, + EventResetMode.AutoReset, + eventName); + eventHandle.Set(); + Logger.LogDebug($"[EventHelper] Signaled event: {eventName}"); + return true; + } + catch (Exception ex) + { + Logger.LogError($"[EventHelper] Failed to signal event '{eventName}': {ex.Message}"); + return false; + } + } + } +} diff --git a/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs b/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs index 42b92ac799..4759b3bbe4 100644 --- a/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs +++ b/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs @@ -34,5 +34,60 @@ namespace PowerDisplay.Configuration /// public const string ExternalMonitorGlyph = "\uE7F4"; } + + /// + /// DDC/CI protocol constants + /// + public static class Ddc + { + /// + /// Retry delay between DDC/CI operations in milliseconds + /// + public const int RetryDelayMs = 100; + + /// + /// Maximum number of retries for DDC/CI operations + /// + public const int MaxRetries = 3; + + /// + /// Timeout for getting monitor capabilities in milliseconds + /// + public const int CapabilitiesTimeoutMs = 5000; + + // VCP Codes + /// VCP code for Brightness (0x10) + public const byte VcpBrightness = 0x10; + + /// VCP code for Contrast (0x12) + public const byte VcpContrast = 0x12; + + /// VCP code for Select Color Preset / Color Temperature (0x14) + public const byte VcpColorTemperature = 0x14; + + /// VCP code for Input Source (0x60) + public const byte VcpInputSource = 0x60; + } + + /// + /// Process synchronization constants + /// + public static class Process + { + /// + /// Timeout for waiting for process ready signal in milliseconds + /// + public const int StartupTimeoutMs = 5000; + + /// + /// Polling interval when waiting for process ready in milliseconds + /// + public const int ReadyPollIntervalMs = 100; + + /// + /// Fallback delay when event-based synchronization fails in milliseconds + /// + public const int FallbackDelayMs = 500; + } } } diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs index acaf953ddc..6ce39c7ef8 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs @@ -26,6 +26,12 @@ namespace PowerDisplay public partial class App : Application #pragma warning restore CA1001 { + /// + /// Event name for signaling that PowerDisplay process is ready. + /// Must match the constant in C++ PowerDisplayModuleInterface. + /// + private const string ProcessReadyEventName = "Local\\PowerToys_PowerDisplay_Ready"; + private readonly ISettingsUtils _settingsUtils = new SettingsUtils(); private Window? _mainWindow; private int _powerToysRunnerPid; @@ -106,6 +112,10 @@ namespace PowerDisplay RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature"); RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile"); + // Signal that process is ready to receive events + // This allows the C++ module to wait for initialization instead of using hardcoded Sleep + SignalProcessReady(); + // Monitor Runner process (backup exit mechanism) if (_powerToysRunnerPid > 0) { @@ -354,5 +364,26 @@ namespace PowerDisplay _trayIconService?.Destroy(); Environment.Exit(0); } + + /// + /// Signal that PowerDisplay process is ready to receive events. + /// Uses a ManualReset event so the C++ module can wait on it. + /// + private static void SignalProcessReady() + { + try + { + using var readyEvent = new EventWaitHandle( + false, + EventResetMode.ManualReset, + ProcessReadyEventName); + readyEvent.Set(); + Logger.LogInfo("Signaled process ready event"); + } + catch (Exception ex) + { + Logger.LogError($"Failed to signal process ready event: {ex.Message}"); + } + } } } diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs index ebbe0375d4..02877f58de 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; @@ -19,7 +20,7 @@ namespace PowerDisplay.ViewModels; /// public partial class MainViewModel { - private async Task InitializeAsync() + private async Task InitializeAsync(CancellationToken cancellationToken = default) { try { @@ -27,7 +28,7 @@ public partial class MainViewModel IsScanning = true; // Discover monitors - var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token); + var monitors = await _monitorManager.DiscoverMonitorsAsync(cancellationToken); // Update UI on the dispatcher thread _dispatcherQueue.TryEnqueue(() => diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs index 69d3c138f0..c0df41e50e 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs @@ -10,6 +10,7 @@ using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using PowerDisplay.Common.Models; using PowerDisplay.Common.Services; +using PowerDisplay.Common.Utils; using PowerDisplay.Serialization; using PowerToys.Interop; @@ -621,21 +622,8 @@ public partial class MainViewModel /// private void SignalMonitorsRefreshEvent() { - try - { - using (var eventHandle = new System.Threading.EventWaitHandle( - false, - System.Threading.EventResetMode.AutoReset, - Constants.RefreshPowerDisplayMonitorsEvent())) - { - eventHandle.Set(); - Logger.LogInfo("Signaled refresh monitors event to Settings UI"); - } - } - catch (Exception ex) - { - Logger.LogError($"Failed to signal refresh monitors event: {ex.Message}"); - } + EventHelper.SignalEvent(Constants.RefreshPowerDisplayMonitorsEvent()); + Logger.LogInfo("Signaled refresh monitors event to Settings UI"); } /// diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs index 5d604bdab8..6f43b786c2 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs @@ -80,7 +80,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable LoadProfiles(); // Start initial discovery - _ = InitializeAsync(); + _ = InitializeAsync(_cancellationTokenSource.Token); } public ObservableCollection Monitors diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs index 145015f034..0770a12913 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs @@ -404,6 +404,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable /// Orientation: 0=normal, 1=90°, 2=180°, 3=270° public async Task SetRotationAsync(int orientation) { + // Validate orientation range (0=normal, 1=90°, 2=180°, 3=270°) + if (orientation < 0 || orientation > 3) + { + Logger.LogWarning($"[{HardwareId}] Invalid rotation value: {orientation}. Must be 0-3."); + return; + } + // If already at this orientation, do nothing if (CurrentRotation == orientation) { diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/Constants.h b/src/modules/powerdisplay/PowerDisplayModuleInterface/Constants.h index 007ff97a32..4d19dae4ca 100644 --- a/src/modules/powerdisplay/PowerDisplayModuleInterface/Constants.h +++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/Constants.h @@ -4,4 +4,9 @@ namespace PowerDisplayConstants { // Name of the powertoy module. inline const std::wstring ModuleKey = L"PowerDisplay"; + + // Process synchronization constants + inline const wchar_t* ProcessReadyEventName = L"Local\\PowerToys_PowerDisplay_Ready"; + constexpr DWORD ProcessReadyTimeoutMs = 5000; + constexpr DWORD FallbackDelayMs = 500; } \ No newline at end of file diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp index a691cd84cf..e23ba1d05a 100644 --- a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp +++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp @@ -185,6 +185,38 @@ private: } } + // Helper method to wait for PowerDisplay.exe process to be ready + // Uses a named event for precise synchronization instead of hardcoded Sleep + void wait_for_process_ready() + { + HANDLE readyEvent = OpenEventW(SYNCHRONIZE, FALSE, PowerDisplayConstants::ProcessReadyEventName); + if (readyEvent) + { + Logger::trace(L"Waiting for PowerDisplay process ready signal..."); + DWORD waitResult = WaitForSingleObject(readyEvent, PowerDisplayConstants::ProcessReadyTimeoutMs); + CloseHandle(readyEvent); + + if (waitResult == WAIT_OBJECT_0) + { + Logger::trace(L"PowerDisplay process ready signal received"); + } + else if (waitResult == WAIT_TIMEOUT) + { + Logger::warn(L"PowerDisplay process ready timeout after {}ms", PowerDisplayConstants::ProcessReadyTimeoutMs); + } + else + { + Logger::warn(L"WaitForSingleObject failed with error: {}", GetLastError()); + } + } + else + { + // Fallback: if cannot open event, use a shorter delay + Logger::warn(L"Could not open PowerDisplay ready event, using fallback delay"); + Sleep(PowerDisplayConstants::FallbackDelayMs); + } + } + public: PowerDisplayModule() { @@ -332,9 +364,8 @@ public: { Logger::trace(L"PowerDisplay process not running, launching before applying color temperature"); launch_process(); - // Wait for process to initialize and register event listeners - // This prevents race condition where event is set before process is ready - Sleep(1000); + // Wait for process to signal ready + wait_for_process_ready(); } if (m_hApplyColorTemperatureEvent) @@ -364,8 +395,8 @@ public: { Logger::trace(L"PowerDisplay process not running, launching before applying profile"); launch_process(); - // Wait for process to initialize and register event listeners - Sleep(1000); + // Wait for process to signal ready + wait_for_process_ready(); } if (m_hApplyProfileEvent) diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs index a27a46a77f..a236b092d0 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs @@ -142,11 +142,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { // Explicitly signal PowerDisplay to refresh tray icon // This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues - using var eventHandle = new System.Threading.EventWaitHandle( - false, - System.Threading.EventResetMode.AutoReset, - Constants.SettingsUpdatedPowerDisplayEvent()); - eventHandle.Set(); + EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent()); Logger.LogInfo($"ShowSystemTrayIcon changed to {value}, signaled SettingsUpdatedPowerDisplayEvent"); } } @@ -396,19 +392,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels /// private void SignalSettingsUpdated() { - try - { - using var eventHandle = new System.Threading.EventWaitHandle( - false, - System.Threading.EventResetMode.AutoReset, - Constants.SettingsUpdatedPowerDisplayEvent()); - eventHandle.Set(); - Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change"); - } - catch (Exception ex) - { - Logger.LogError($"Failed to signal SettingsUpdatedPowerDisplayEvent: {ex.Message}"); - } + EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent()); + Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change"); } public void Launch() @@ -513,7 +498,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels for (int i = Monitors.Count - 1; i >= 0; i--) { var existingMonitor = Monitors[i]; - if (updatedMonitorsDict.TryGetValue(existingMonitor.InternalName, out var updatedMonitor)) + if (updatedMonitorsDict.TryGetValue(existingMonitor.InternalName, out var updatedMonitor) + && updatedMonitor != null) { // Monitor still exists - update its properties in place Logger.LogInfo($"[ReloadMonitors] Updating existing monitor: {existingMonitor.InternalName}"); @@ -661,13 +647,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels profile.Name)); // Signal PowerDisplay to apply profile - using (var eventHandle = new System.Threading.EventWaitHandle( - false, - System.Threading.EventResetMode.AutoReset, - Constants.ApplyProfilePowerDisplayEvent())) - { - eventHandle.Set(); - } + EventHelper.SignalEvent(Constants.ApplyProfilePowerDisplayEvent()); Logger.LogInfo($"Profile '{profile.Name}' applied successfully"); }