mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Introduce a new PowerToys' module PowerDisplay to let user can control their monitor settings without touching monitor's button. Support feature list: Common: 1. Profiles support 2. Integration with LightSwitch (auto switch profile when theme change) 3. TrayIcon 4. Save and restore settings when startup 5. Shortcut 6. Rotation 7. GPO support 8. Auto re-discovery monitor when plugging and unplugging monitors. 9. Identify Monitors 10. Quick profile switch Especially for DDC/CI monitor: 1. Brightness 2. Contrast 3. Volume 4. Color temperature (preset profile) 5. Input source 6. Power State (poweroff) Design doc: https://github.com/microsoft/PowerToys/blob/yuleng/display/pr/3/doc/devdocs/modules/powerdisplay/design.md AOT compatibility: I designed this module for AOT from the start, so I'm pretty sure at least 95% of it is AOT compatible. But unfortunately, PowerToys still have a AOT blocker to block this module publish with AOT. Currently PowerToys will check the .net file version (file version not lib version) to avoid crash. So, all modules should reference Common.UI or add UseWPF to avoid overwrite the .net file with different version (which may cause crash). Todo: - [ ] BugBash - [ ] Icon - [ ] IdentifyWindow UI improvement Demo Main UI: <img width="546" height="671" alt="image" src="https://github.com/user-attachments/assets/b0ad9ac5-8000-4365-a192-ab8c2d66d4f1" /> Input Source: <img width="536" height="674" alt="image" src="https://github.com/user-attachments/assets/80f9ccd7-4f8c-4201-b177-cc86c5bcc9e3" /> Settings UI: <img width="1581" height="1191" alt="image" src="https://github.com/user-attachments/assets/6a82e4bb-8f96-4f28-abf9-d7c45e1c8ef7" /> <img width="1525" height="1146" alt="image" src="https://github.com/user-attachments/assets/aae81e65-08fd-453a-bf52-02a74f2fdea0" /> Closes: #42942 #42678 #41117 #38109 #35564 #34932 #28500 #1052 #18149 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #1052 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Yu Leng <yuleng@microsoft.com> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: moooyo <lengyuchn@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1617 lines
62 KiB
Markdown
1617 lines
62 KiB
Markdown
# PowerDisplay Module Design Document
|
|
|
|
## Table of Contents
|
|
|
|
1. [Background](#background)
|
|
2. [Problem Statement](#problem-statement)
|
|
3. [Goals](#goals)
|
|
4. [Technical Terminology](#technical-terminology)
|
|
- [DDC/CI (Display Data Channel Command Interface)](#ddcci-display-data-channel-command-interface)
|
|
- [WMI (Windows Management Instrumentation)](#wmi-windows-management-instrumentation)
|
|
5. [Architecture Overview](#architecture-overview)
|
|
- [High-Level Component Architecture](#high-level-component-architecture)
|
|
- [Project Structure](#project-structure)
|
|
6. [Component Design](#component-design)
|
|
- [PowerDisplay Module Internal Structure](#powerdisplay-module-internal-structure)
|
|
- [DisplayChangeWatcher - Monitor Hot-Plug Detection](#displaychangewatcher---monitor-hot-plug-detection)
|
|
- [DDC/CI and WMI Interaction Architecture](#ddcci-and-wmi-interaction-architecture)
|
|
- [IMonitorController Interface Methods](#imonitorcontroller-interface-methods)
|
|
- [Why WmiLight Instead of System.Management](#why-wmilight-instead-of-systemmanagement)
|
|
- [Why We Need an MCCS Capabilities String Parser](#why-we-need-an-mccs-capabilities-string-parser)
|
|
- [Monitor Identification: Handles, IDs, and Names](#monitor-identification-handles-ids-and-names)
|
|
- [Settings UI and PowerDisplay Interaction Architecture](#settings-ui-and-powerdisplay-interaction-architecture)
|
|
- [Windows Events for IPC](#windows-events-for-ipc)
|
|
- [LightSwitch Profile Integration Architecture](#lightswitch-profile-integration-architecture)
|
|
- [LightSwitch Settings JSON Structure](#lightswitch-settings-json-structure)
|
|
7. [Data Flow and Communication](#data-flow-and-communication)
|
|
- [Monitor Discovery Flow](#monitor-discovery-flow)
|
|
8. [Sequence Diagrams](#sequence-diagrams)
|
|
- [Sequence: Modifying Color Temperature in Flyout UI](#sequence-modifying-color-temperature-in-flyout-ui)
|
|
- [Sequence: Creating and Saving a Profile](#sequence-creating-and-saving-a-profile)
|
|
- [Sequence: Applying Profile via LightSwitch Theme Change](#sequence-applying-profile-via-lightswitch-theme-change)
|
|
- [Sequence: UI Slider Adjustment (Brightness)](#sequence-ui-slider-adjustment-brightness)
|
|
- [Sequence: Module Enable/Disable Lifecycle](#sequence-module-enabledisable-lifecycle)
|
|
9. [Future Considerations](#future-considerations)
|
|
- [Already Implemented](#already-implemented)
|
|
- [Potential Future Enhancements](#potential-future-enhancements)
|
|
10. [References](#references)
|
|
|
|
---
|
|
|
|
## Background
|
|
|
|
PowerDisplay is a PowerToys module designed to provide unified control over display
|
|
settings across multiple monitors. Users often work with multiple displays (external monitors or laptop screens) and need a
|
|
convenient way to adjust display parameters such as brightness, contrast, color
|
|
temperature, volume, and input source without navigating through individual monitor
|
|
OSD menus.
|
|
|
|
The module leverages two primary technologies for monitor control:
|
|
|
|
1. **DDC/CI (Display Data Channel Command Interface)** - For external monitors
|
|
2. **WMI (Windows Management Instrumentation)** - For internal(laptop) displays
|
|
|
|
---
|
|
|
|
## Problem Statement
|
|
|
|
Users with multiple monitors face several challenges:
|
|
|
|
1. **Fragmented Control**: Each monitor requires separate OSD navigation
|
|
2. **Inconsistent Brightness**: Difficult to maintain uniform brightness across displays
|
|
3. **No Profile Support**: Cannot quickly switch display configurations for different
|
|
scenarios (gaming, productivity, movie watching)
|
|
4. **Theme Integration Gap**: No automatic display adjustment when switching between
|
|
light and dark themes
|
|
|
|
---
|
|
|
|
## Goals
|
|
|
|
- Provide unified control for brightness, contrast, volume, color temperature, and
|
|
input source across all connected monitors
|
|
- Support both DDC/CI (external monitors) and WMI (laptop displays)
|
|
- Support user-defined profiles for quick configuration switching
|
|
- Integrate with LightSwitch module for automatic profile application on theme changes
|
|
- Support global hotkey activation
|
|
|
|
---
|
|
|
|
## Technical Terminology
|
|
|
|
### DDC/CI (Display Data Channel Command Interface)
|
|
|
|
**DDC/CI** is a VESA standard (defined in the DDC specification) that allows
|
|
bidirectional communication between a computer and a display over the I2C bus
|
|
embedded in display cables.
|
|
|
|
Most external monitors support DDC/CI, allowing applications to read and modify settings
|
|
like brightness and contrast programmatically. But unfortunately, some manufacturers have poor implementations of their product's driver. They may not support DDC/CI or report itself supports DDC/CI (through capabilities string) when it does not. Even if a monitor supports DDC/CI, they may only support a limited subset of VCP codes, or have buggy implementations.
|
|
|
|
And sometimes, users may connect monitor through a KVM switch or docking station that does not pass through DDC/CI commands correctly, and their docking may report it supports (hard code a capabilities string) but in reality, it does not. And will do thing when we try to send DDC/CI commands.
|
|
|
|
PowerDisplay relies on the monitor-reported capabilities string to determine supported features. But if your monitor's manufacturer has a poor DDC/CI implementation, or you are connecting through a docking station that does not properly support DDC/CI, some features may not work as expected. And we can do nothing about it.
|
|
|
|
**Key Concepts:**
|
|
|
|
| Term | Description |
|
|
|------|-------------|
|
|
| **VCP (Virtual Control Panel)** | Standardized codes for monitor settings |
|
|
| **MCCS (Monitor Command Control Set)** | VESA standard defining VCP codes |
|
|
| **Capabilities String** | Monitor-reported string describing supported features |
|
|
|
|
**Common VCP Codes Used:**
|
|
|
|
| VCP Code | Name | Description |
|
|
|----------|------|-------------|
|
|
| `0x10` | Brightness | Display luminance (0-100) |
|
|
| `0x12` | Contrast | Display contrast ratio (0-100) |
|
|
| `0x14` | Select Color Preset | Color temperature presets (sRGB, 5000K, 6500K, etc.) |
|
|
| `0x60` | Input Source | Active video input (HDMI, DP, USB-C, etc.) |
|
|
| `0x62` | Volume | Speaker/headphone volume (0-100) |
|
|
|
|
---
|
|
|
|
### WMI (Windows Management Instrumentation)
|
|
|
|
**WMI** is Microsoft's implementation of Web-Based Enterprise Management (WBEM),
|
|
providing a standardized interface for accessing management information in Windows.
|
|
For display control, WMI is primarily used for laptop internal displays that may not
|
|
support DDC/CI.
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
### High-Level Component Architecture
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph PowerToys["PowerToys Application"]
|
|
Runner["Runner (PowerToys.exe)"]
|
|
SettingsUI["Settings UI (WinUI 3)"]
|
|
LightSwitch["LightSwitch Module"]
|
|
end
|
|
|
|
subgraph PowerDisplayModule["PowerDisplay Module"]
|
|
ModuleInterface["Module Interface<br/>(PowerDisplayModuleInterface.dll)"]
|
|
PowerDisplayApp["PowerDisplay App<br/>(PowerToys.PowerDisplay.exe)"]
|
|
PowerDisplayLib["PowerDisplay.Lib<br/>(Shared Library)"]
|
|
end
|
|
|
|
subgraph External["External"]
|
|
Hardware["Display Hardware<br/>(External + Internal)"]
|
|
Storage["Persistent Storage<br/>(settings.json, profiles.json)"]
|
|
end
|
|
|
|
Runner -->|"Loads DLL"| ModuleInterface
|
|
Runner -->|"Hotkey Events"| ModuleInterface
|
|
SettingsUI <-->|"Named Pipes"| Runner
|
|
SettingsUI -->|"Custom Actions<br/>(Launch, ApplyProfile)"| ModuleInterface
|
|
|
|
ModuleInterface <-->|"Windows Events<br/>(Show/Toggle/Terminate)"| PowerDisplayApp
|
|
PowerDisplayApp -->|"RefreshMonitors Event"| SettingsUI
|
|
LightSwitch -->|"Theme Events<br/>(Light/Dark)"| PowerDisplayApp
|
|
|
|
PowerDisplayApp --> PowerDisplayLib
|
|
PowerDisplayLib -->|"DDC/CI (Dxva2.dll)"| Hardware
|
|
PowerDisplayLib -->|"WMI (WmiLight)"| Hardware
|
|
PowerDisplayLib -->|"ChangeDisplaySettingsEx"| Hardware
|
|
PowerDisplayApp <--> Storage
|
|
|
|
style Runner fill:#e1f5fe
|
|
style SettingsUI fill:#e1f5fe
|
|
style LightSwitch fill:#e1f5fe
|
|
style ModuleInterface fill:#fff3e0
|
|
style PowerDisplayApp fill:#fff3e0
|
|
style PowerDisplayLib fill:#e8f5e9
|
|
style Hardware fill:#f3e5f5
|
|
style Storage fill:#fffde7
|
|
```
|
|
|
|
This high-level view shows the module boundaries. See [Component Design](#component-design)
|
|
for internal structure details.
|
|
|
|
---
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
src/modules/powerdisplay/
|
|
├── PowerDisplay.Lib/ # Core library (shared)
|
|
│ ├── Drivers/
|
|
│ │ ├── DDC/
|
|
│ │ │ ├── DdcCiController.cs # DDC/CI implementation
|
|
│ │ │ ├── DdcCiNative.cs # P/Invoke declarations & QueryDisplayConfig
|
|
│ │ │ ├── MonitorDiscoveryHelper.cs
|
|
│ │ │ └── PhysicalMonitorHandleManager.cs
|
|
│ │ ├── WMI/
|
|
│ │ │ └── WmiController.cs # WMI implementation (WmiLight library)
|
|
│ │ ├── NativeConstants.cs # Win32 constants (VCP codes, etc.)
|
|
│ │ ├── NativeDelegates.cs # P/Invoke delegate types
|
|
│ │ ├── NativeStructures.cs # Win32 structures
|
|
│ │ └── PInvoke.cs # P/Invoke declarations
|
|
│ ├── Interfaces/
|
|
│ │ ├── IMonitorController.cs # Controller abstraction
|
|
│ │ ├── IMonitorData.cs # Monitor data interface
|
|
│ │ └── IProfileService.cs # Profile service interface
|
|
│ ├── Models/
|
|
│ │ ├── Monitor.cs # Runtime monitor data
|
|
│ │ ├── MonitorCapabilities.cs # Monitor capability flags
|
|
│ │ ├── MonitorOperationResult.cs # Operation result
|
|
│ │ ├── MonitorStateEntry.cs # Persisted monitor state
|
|
│ │ ├── MonitorStateFile.cs # State file schema
|
|
│ │ ├── PowerDisplayProfile.cs # Profile definition
|
|
│ │ ├── PowerDisplayProfiles.cs # Profile collection
|
|
│ │ ├── ProfileMonitorSetting.cs # Per-monitor profile settings
|
|
│ │ ├── ColorPresetItem.cs # Color preset UI item
|
|
│ │ ├── VcpCapabilities.cs # Parsed VCP capabilities
|
|
│ │ └── VcpFeatureValue.cs # VCP feature value (current/min/max)
|
|
│ ├── Serialization/
|
|
│ │ └── ProfileSerializationContext.cs # JSON source generation
|
|
│ ├── Services/
|
|
│ │ ├── DisplayRotationService.cs # Display rotation via ChangeDisplaySettingsEx
|
|
│ │ ├── MonitorStateManager.cs # State persistence (debounced save) and restore on startup
|
|
│ │ └── ProfileService.cs # Profile persistence
|
|
│ ├── Utils/
|
|
│ │ ├── ColorTemperatureHelper.cs # Color temp utilities
|
|
│ │ ├── EventHelper.cs # Windows Event utilities
|
|
│ │ ├── MccsCapabilitiesParser.cs # DDC/CI capabilities parser
|
|
│ │ ├── MonitorFeatureHelper.cs # Monitor feature utilities
|
|
│ │ ├── MonitorMatchingHelper.cs # Profile-to-monitor matching
|
|
│ │ ├── MonitorValueConverter.cs # Value conversion utilities
|
|
│ │ ├── PnpIdHelper.cs # PnP manufacturer ID lookup
|
|
│ │ ├── ProfileHelper.cs # Profile helper utilities
|
|
│ │ ├── SimpleDebouncer.cs # Generic debouncer
|
|
│ │ └── VcpNames.cs # VCP code and value name lookup
|
|
│ └── PathConstants.cs # File path constants
|
|
│
|
|
├── PowerDisplay/ # WinUI 3 application
|
|
│ ├── Assets/ # App icons and images
|
|
│ ├── Configuration/
|
|
│ │ └── AppConstants.cs # Application constants
|
|
│ ├── Helpers/
|
|
│ │ ├── DisplayChangeWatcher.cs # Monitor hot-plug detection (WinRT DeviceWatcher)
|
|
│ │ ├── MonitorManager.cs # Discovery orchestrator
|
|
│ │ ├── NativeEventWaiter.cs # Windows Event waiting
|
|
│ │ ├── ResourceLoaderInstance.cs # Resource loader singleton
|
|
│ │ ├── SettingsDeepLink.cs # Deep link to Settings UI
|
|
│ │ ├── TrayIconService.cs # System tray integration
|
|
│ │ ├── TypePreservation.cs # AOT type preservation
|
|
│ │ └── WindowHelper.cs # Window utilities
|
|
│ ├── PowerDisplayXAML/
|
|
│ │ ├── App.xaml / App.xaml.cs # Application entry point
|
|
│ │ ├── MainWindow.xaml / .cs # Main UI window
|
|
│ │ ├── IdentifyWindow.xaml / .cs # Monitor identify overlay
|
|
│ │ └── MonitorIcon.xaml / .cs # Monitor icon control
|
|
│ ├── Serialization/
|
|
│ │ └── JsonSourceGenerationContext.cs # JSON source generation
|
|
│ ├── Services/
|
|
│ │ └── LightSwitchService.cs # LightSwitch theme change handling
|
|
│ ├── Strings/ # Localization resources (en-us)
|
|
│ ├── Telemetry/
|
|
│ │ └── Events/
|
|
│ │ └── PowerDisplayStartEvent.cs # Telemetry event
|
|
│ ├── ViewModels/
|
|
│ │ ├── ColorTemperatureItem.cs # Color temperature dropdown item
|
|
│ │ ├── InputSourceItem.cs # Input source dropdown item
|
|
│ │ ├── MainViewModel.cs # Main VM (partial class)
|
|
│ │ ├── MainViewModel.Monitors.cs # Monitor discovery methods
|
|
│ │ ├── MainViewModel.Settings.cs # Settings persistence methods
|
|
│ │ └── MonitorViewModel.cs # Per-monitor VM
|
|
│ ├── GlobalUsings.cs # Global using directives
|
|
│ └── Program.cs # Application entry point
|
|
│
|
|
├── PowerDisplay.Lib.UnitTests/ # Unit tests
|
|
│ ├── MccsCapabilitiesParserTests.cs
|
|
│ └── MonitorMatchingHelperTests.cs
|
|
│
|
|
└── PowerDisplayModuleInterface/ # C++ DLL (module interface)
|
|
├── dllmain.cpp # PowertoyModuleIface impl
|
|
├── Constants.h # Module constants (event names, timeouts)
|
|
├── resource.h # Resource definitions
|
|
├── pch.h / pch.cpp # Precompiled headers
|
|
└── Trace.h / Trace.cpp # ETW telemetry tracing
|
|
```
|
|
|
|
---
|
|
|
|
## Component Design
|
|
|
|
### PowerDisplay Module Internal Structure
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph ExternalInputs["External Inputs"]
|
|
ModuleInterface["Module Interface<br/>(C++ DLL)"]
|
|
LightSwitch["LightSwitch Module"]
|
|
end
|
|
|
|
subgraph WindowsEvents["Windows Events (IPC)"]
|
|
direction LR
|
|
ShowToggleEvents["Show/Toggle/Terminate<br/>Events"]
|
|
ThemeChangedEvent["ThemeChanged<br/>Events"]
|
|
end
|
|
|
|
subgraph PowerDisplayModule["PowerDisplay Module"]
|
|
subgraph PowerDisplayApp["PowerDisplay App (WinUI 3)"]
|
|
MainViewModel
|
|
MonitorViewModel
|
|
MonitorManager
|
|
DisplayChangeWatcher["DisplayChangeWatcher<br/>(Hot-Plug Detection)"]
|
|
LightSwitchService["LightSwitchService<br/>(Theme Handler)"]
|
|
end
|
|
|
|
subgraph PowerDisplayLib["PowerDisplay.Lib"]
|
|
subgraph Services
|
|
ProfileService
|
|
MonitorStateManager
|
|
DisplayRotationService
|
|
end
|
|
subgraph Drivers
|
|
DdcCiController
|
|
WmiController
|
|
end
|
|
subgraph Utils
|
|
PnpIdHelper["PnpIdHelper<br/>(Manufacturer Names)"]
|
|
end
|
|
end
|
|
end
|
|
|
|
subgraph Storage["Persistent Storage"]
|
|
SettingsJson[("settings.json")]
|
|
ProfilesJson[("profiles.json")]
|
|
MonitorStateJson[("monitor_state.json")]
|
|
end
|
|
|
|
subgraph Hardware["Display Hardware"]
|
|
ExternalMonitor["External Monitor"]
|
|
LaptopDisplay["Laptop Display"]
|
|
end
|
|
|
|
%% External to Windows Events
|
|
ModuleInterface -->|"SetEvent()"| ShowToggleEvents
|
|
LightSwitch -->|"SetEvent()"| ThemeChangedEvent
|
|
|
|
%% Windows Events to App
|
|
ShowToggleEvents --> MainViewModel
|
|
ThemeChangedEvent --> LightSwitchService
|
|
|
|
%% App internal
|
|
LightSwitchService -.->|"Get profile name"| MainViewModel
|
|
MainViewModel --> MonitorViewModel
|
|
MonitorViewModel --> MonitorManager
|
|
DisplayChangeWatcher -.->|"DisplayChanged event"| MainViewModel
|
|
|
|
%% App to Lib services
|
|
MainViewModel --> ProfileService
|
|
MonitorViewModel --> MonitorStateManager
|
|
MonitorManager --> Drivers
|
|
MonitorManager --> DisplayRotationService
|
|
|
|
%% Utils used during discovery
|
|
WmiController --> PnpIdHelper
|
|
|
|
%% Services to Storage
|
|
ProfileService --> ProfilesJson
|
|
MonitorStateManager --> MonitorStateJson
|
|
|
|
%% Drivers to Hardware
|
|
DdcCiController -->|"DDC/CI"| ExternalMonitor
|
|
WmiController -->|"WMI"| LaptopDisplay
|
|
DisplayRotationService -->|"ChangeDisplaySettingsEx"| ExternalMonitor
|
|
DisplayRotationService -->|"ChangeDisplaySettingsEx"| LaptopDisplay
|
|
|
|
%% Force vertical layout: PowerDisplay.Lib above Storage/Hardware
|
|
PowerDisplayLib ~~~ Storage
|
|
PowerDisplayLib ~~~ Hardware
|
|
|
|
%% Styling
|
|
style ExternalInputs fill:#e3f2fd,stroke:#1976d2
|
|
style WindowsEvents fill:#fce4ec,stroke:#c2185b
|
|
style PowerDisplayModule fill:#fff8e1,stroke:#f57c00,stroke-width:2px
|
|
style PowerDisplayApp fill:#ffe0b2,stroke:#ef6c00
|
|
style PowerDisplayLib fill:#c8e6c9,stroke:#388e3c
|
|
style Services fill:#a5d6a7,stroke:#2e7d32
|
|
style Drivers fill:#ffccbc,stroke:#e64a19
|
|
style Utils fill:#dcedc8,stroke:#689f38
|
|
style Storage fill:#e1bee7,stroke:#8e24aa
|
|
style Hardware fill:#b2dfdb,stroke:#00897b
|
|
```
|
|
|
|
---
|
|
|
|
### DisplayChangeWatcher - Monitor Hot-Plug Detection
|
|
|
|
The `DisplayChangeWatcher` component provides automatic detection of monitor connect/disconnect events using the WinRT DeviceWatcher API.
|
|
|
|
**Key Features:**
|
|
- Uses `DisplayMonitor.GetDeviceSelector()` to watch for display device changes
|
|
- Implements 1-second debouncing to coalesce rapid connect/disconnect events
|
|
- Triggers `DisplayChanged` event to notify `MainViewModel` for monitor list refresh
|
|
- Runs continuously after initial monitor discovery completes
|
|
|
|
**Implementation Details:**
|
|
```csharp
|
|
// Device selector for display monitors
|
|
string selector = DisplayMonitor.GetDeviceSelector();
|
|
_deviceWatcher = DeviceInformation.CreateWatcher(selector);
|
|
|
|
// Events monitored
|
|
_deviceWatcher.Added += OnDeviceAdded; // New monitor connected
|
|
_deviceWatcher.Removed += OnDeviceRemoved; // Monitor disconnected
|
|
_deviceWatcher.Updated += OnDeviceUpdated; // Monitor properties changed
|
|
```
|
|
|
|
**Debouncing Strategy:**
|
|
- Each device change event schedules a `DisplayChanged` event after 1 second
|
|
- Subsequent events within the debounce window cancel the previous timer
|
|
- This prevents excessive refreshes when multiple monitors change simultaneously
|
|
|
|
---
|
|
|
|
### DDC/CI and WMI Interaction Architecture
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph Application["Application Layer"]
|
|
MM["MonitorManager"]
|
|
end
|
|
|
|
subgraph Abstraction["Abstraction Layer"]
|
|
IMC["IMonitorController Interface"]
|
|
end
|
|
|
|
subgraph Controllers["Controller Implementations"]
|
|
DDC["DdcCiController"]
|
|
WMI["WmiController"]
|
|
end
|
|
|
|
subgraph DDCStack["DDC/CI Stack"]
|
|
DDCNative["DdcCiNative<br/>(P/Invoke)"]
|
|
PhysicalMonitorMgr["PhysicalMonitorHandleManager"]
|
|
MonitorDiscovery["MonitorDiscoveryHelper"]
|
|
CapParser["MccsCapabilitiesParser"]
|
|
|
|
subgraph Win32["Win32 APIs"]
|
|
User32["User32.dll<br/>EnumDisplayMonitors<br/>GetMonitorInfo"]
|
|
Dxva2["Dxva2.dll<br/>GetVCPFeature<br/>SetVCPFeature<br/>Capabilities"]
|
|
end
|
|
end
|
|
|
|
subgraph WMIStack["WMI Stack"]
|
|
WmiLight["WmiLight Library<br/>(Native AOT compatible,<br/>NuGet package)"]
|
|
PnpHelper["PnpIdHelper<br/>(Manufacturer name lookup)"]
|
|
|
|
subgraph WMIClasses["WMI Classes (root\\WMI)"]
|
|
WmiMonBright["WmiMonitorBrightness"]
|
|
WmiMonBrightMethods["WmiMonitorBrightnessMethods"]
|
|
end
|
|
end
|
|
|
|
subgraph Hardware["Hardware Layer"]
|
|
ExtMon["External Monitor<br/>(DDC/CI capable)"]
|
|
LaptopMon["Laptop Display<br/>(WMI only)"]
|
|
end
|
|
|
|
MM --> IMC
|
|
IMC -.-> DDC
|
|
IMC -.-> WMI
|
|
|
|
DDC --> DDCNative
|
|
DDC --> PhysicalMonitorMgr
|
|
DDC --> MonitorDiscovery
|
|
DDC --> CapParser
|
|
|
|
DDCNative --> User32
|
|
DDCNative --> Dxva2
|
|
MonitorDiscovery --> User32
|
|
PhysicalMonitorMgr --> Dxva2
|
|
|
|
Dxva2 -->|"I2C/DDC"| ExtMon
|
|
|
|
WMI --> WmiLight
|
|
WMI --> PnpHelper
|
|
WmiLight --> WmiMonBright
|
|
WmiLight --> WmiMonBrightMethods
|
|
|
|
WmiMonBrightMethods -->|"WMI Provider"| LaptopMon
|
|
|
|
style IMC fill:#bbdefb
|
|
style DDC fill:#c8e6c9
|
|
style WMI fill:#ffccbc
|
|
```
|
|
|
|
### IMonitorController Interface Methods
|
|
|
|
```mermaid
|
|
classDiagram
|
|
class IMonitorController {
|
|
<<interface>>
|
|
+Name: string
|
|
+DiscoverMonitorsAsync(cancellationToken) IEnumerable~Monitor~
|
|
+GetBrightnessAsync(monitor, cancellationToken) VcpFeatureValue
|
|
+SetBrightnessAsync(monitor, brightness, cancellationToken) MonitorOperationResult
|
|
+SetContrastAsync(monitor, contrast, cancellationToken) MonitorOperationResult
|
|
+SetVolumeAsync(monitor, volume, cancellationToken) MonitorOperationResult
|
|
+GetColorTemperatureAsync(monitor, cancellationToken) VcpFeatureValue
|
|
+SetColorTemperatureAsync(monitor, vcpValue, cancellationToken) MonitorOperationResult
|
|
+GetInputSourceAsync(monitor, cancellationToken) VcpFeatureValue
|
|
+SetInputSourceAsync(monitor, inputSource, cancellationToken) MonitorOperationResult
|
|
+Dispose()
|
|
}
|
|
|
|
class DdcCiController {
|
|
-_handleManager: PhysicalMonitorHandleManager
|
|
-_discoveryHelper: MonitorDiscoveryHelper
|
|
+Name: "DDC/CI Monitor Controller"
|
|
+DiscoverMonitorsAsync()
|
|
+GetBrightnessAsync(monitor)
|
|
+SetBrightnessAsync(monitor, brightness)
|
|
+SetContrastAsync(monitor, contrast)
|
|
+SetVolumeAsync(monitor, volume)
|
|
+GetColorTemperatureAsync(monitor)
|
|
+SetColorTemperatureAsync(monitor, colorTemperature)
|
|
+GetInputSourceAsync(monitor)
|
|
+SetInputSourceAsync(monitor, inputSource)
|
|
+GetCapabilitiesStringAsync(monitor) string
|
|
-GetVcpFeatureAsync(monitor, vcpCode)
|
|
-CollectCandidateMonitorsAsync()
|
|
-FetchCapabilitiesInParallelAsync()
|
|
-GetPhysicalMonitorsWithRetryAsync()
|
|
}
|
|
|
|
class WmiController {
|
|
+Name: "WMI Monitor Controller"
|
|
+DiscoverMonitorsAsync()
|
|
+GetBrightnessAsync(monitor)
|
|
+SetBrightnessAsync(monitor, brightness)
|
|
+SetContrastAsync(monitor, contrast)
|
|
+SetVolumeAsync(monitor, volume)
|
|
+GetColorTemperatureAsync(monitor)
|
|
+SetColorTemperatureAsync(monitor, colorTemperature)
|
|
+GetInputSourceAsync(monitor)
|
|
+SetInputSourceAsync(monitor, inputSource)
|
|
-ExtractHardwareIdFromInstanceName()
|
|
-GetMonitorDisplayInfoByHardwareId()
|
|
}
|
|
|
|
IMonitorController <|.. DdcCiController
|
|
IMonitorController <|.. WmiController
|
|
```
|
|
|
|
---
|
|
|
|
### Why WmiLight Instead of System.Management
|
|
|
|
PowerDisplay uses the [WmiLight](https://github.com/MartinKuschnik/WmiLight) NuGet package
|
|
for WMI operations instead of the built-in `System.Management` namespace. This decision was
|
|
driven by several technical requirements:
|
|
|
|
#### Native AOT Compatibility
|
|
|
|
PowerDisplay is built with Native AOT (Ahead-of-Time compilation) enabled for improved startup
|
|
performance and reduced memory footprint. The standard `System.Management` namespace is **not
|
|
compatible with Native AOT** because it relies heavily on runtime reflection and COM interop
|
|
patterns that cannot be statically analyzed.
|
|
|
|
WmiLight provides Native AOT support since version 5.0.0, making it the appropriate choice for
|
|
AOT-compiled applications.
|
|
|
|
```xml
|
|
<!-- PowerDisplay.Lib.csproj -->
|
|
<PropertyGroup>
|
|
<IsAotCompatible>true</IsAotCompatible>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
<PackageReference Include="WmiLight" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
#### Memory Leak Prevention
|
|
|
|
The `System.Management` implementation has a known issue where it leaks memory on each WMI
|
|
operation. While this might be acceptable for short-lived applications, PowerDisplay runs as
|
|
a long-running background process that may perform frequent WMI queries (e.g., polling
|
|
brightness levels, responding to theme changes). WmiLight addresses this memory leak issue.
|
|
|
|
#### Lightweight API
|
|
|
|
WmiLight provides a simpler, more lightweight API compared to `System.Management`:
|
|
|
|
```csharp
|
|
// WmiLight - Simple and direct
|
|
using (var connection = new WmiConnection(@"root\WMI"))
|
|
{
|
|
var results = connection.CreateQuery("SELECT * FROM WmiMonitorBrightness");
|
|
foreach (var obj in results)
|
|
{
|
|
var brightness = obj.GetPropertyValue<byte>("CurrentBrightness");
|
|
}
|
|
}
|
|
|
|
// System.Management - More verbose
|
|
using (var searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM WmiMonitorBrightness"))
|
|
{
|
|
foreach (ManagementObject obj in searcher.Get())
|
|
{
|
|
var brightness = (byte)obj["CurrentBrightness"];
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Comparison Summary
|
|
|
|
| Aspect | System.Management | WmiLight |
|
|
|--------|-------------------|----------|
|
|
| **Native AOT Support** | ❌ Not supported | ✅ Supported (v5.0.0+) |
|
|
| **Memory Leaks** | ⚠️ Leaks on remote operations | ✅ No known leaks |
|
|
| **API Complexity** | More verbose | Simpler, lighter |
|
|
| **Long-running Services** | Not recommended | ✅ Recommended |
|
|
| **Static Linking** | ❌ Not available | ✅ Optional (`PublishWmiLightStaticallyLinked`) |
|
|
|
|
#### References
|
|
|
|
- [WmiLight GitHub Repository](https://github.com/MartinKuschnik/WmiLight)
|
|
- [WmiLight NuGet Package](https://www.nuget.org/packages/WmiLight)
|
|
|
|
---
|
|
|
|
### Why We Need an MCCS Capabilities String Parser
|
|
|
|
DDC/CI monitors report their supported features via a **capabilities string** - a structured
|
|
text format defined by the VESA MCCS (Monitor Control Command Set) standard. This string
|
|
tells PowerDisplay which VCP codes the monitor supports and what values are valid for each.
|
|
|
|
#### Example Capabilities String
|
|
|
|
```
|
|
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 60(11 12 0F))mccs_ver(2.2))
|
|
```
|
|
|
|
This string encodes:
|
|
- **Protocol**: monitor
|
|
- **Type**: LCD display
|
|
- **Model**: PD3220U
|
|
- **Supported commands**: 0x01, 0x02, 0x03, 0x07
|
|
- **VCP codes**: 0x10 (brightness), 0x12 (contrast), 0x14 (color preset with values 4,5,6), 0x60 (input source with values 0x11, 0x12, 0x0F)
|
|
- **MCCS version**: 2.2
|
|
|
|
#### Why Parse It?
|
|
|
|
| Use Case | How Parser Helps |
|
|
|----------|------------------|
|
|
| **Feature Detection** | Determine if monitor supports contrast, volume, color temperature, input switching |
|
|
| **Input Source Dropdown** | Extract valid input source values (e.g., HDMI-1=0x11, DP=0x0F) for UI dropdown |
|
|
| **Color Preset List** | Extract supported color presets (e.g., sRGB, 5000K, 6500K) |
|
|
| **Diagnostics** | Display raw VCP codes in Settings UI for troubleshooting |
|
|
| **PIP/PBP Support** | Parse window capabilities for Picture-in-Picture features |
|
|
|
|
#### Why Not Use Regex?
|
|
|
|
The MCCS capabilities string format has **nested parentheses** that regex cannot reliably handle:
|
|
|
|
```
|
|
vcp(10 12 14(04 05 06) 60(11 12 0F))
|
|
^^^^^^^^^^^^ nested values
|
|
```
|
|
|
|
A recursive descent parser properly handles:
|
|
- Nested parentheses at arbitrary depth
|
|
- Variable whitespace (some monitors use `01 02 03`, others use `010203`)
|
|
- Optional outer parentheses (some monitors omit them)
|
|
- Unknown segments (graceful skip without failing)
|
|
|
|
#### Implementation
|
|
|
|
PowerDisplay implements a **zero-allocation recursive descent parser** using `ref struct` and
|
|
`ReadOnlySpan<char>` for optimal performance during monitor discovery.
|
|
|
|
```csharp
|
|
// Usage in DdcCiController
|
|
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
|
|
if (result.IsValid)
|
|
{
|
|
monitor.VcpCapabilitiesInfo = result.Capabilities;
|
|
// Now we know which features this monitor supports
|
|
}
|
|
```
|
|
|
|
> **Detailed Design:** See [mccsParserDesign.md](./mccsParserDesign.md) for the complete
|
|
> parser architecture, grammar definition, and implementation details.
|
|
|
|
---
|
|
|
|
### Monitor Identification: Handles, IDs, and Names
|
|
|
|
Understanding how Windows identifies monitors is critical for PowerDisplay's operation.
|
|
Different Windows APIs use different identifiers, and PowerDisplay must correlate these
|
|
to provide a unified view across DDC/CI and WMI subsystems.
|
|
|
|
#### Windows Display Subsystem Overview
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph WindowsAPIs["Windows Display APIs"]
|
|
EnumDisplayMonitors["EnumDisplayMonitors<br/>(User32.dll)"]
|
|
QueryDisplayConfig["QueryDisplayConfig<br/>(User32.dll)"]
|
|
GetPhysicalMonitors["GetPhysicalMonitorsFromHMONITOR<br/>(Dxva2.dll)"]
|
|
WmiMonitor["WMI root\\WMI<br/>(WmiLight)"]
|
|
end
|
|
|
|
subgraph Identifiers["Monitor Identifiers"]
|
|
HMONITOR["HMONITOR<br/>(Logical Monitor Handle)"]
|
|
GdiDeviceName["GDI Device Name<br/>(e.g., \\\\.\\DISPLAY1)"]
|
|
PhysicalHandle["Physical Monitor Handle<br/>(IntPtr for DDC/CI)"]
|
|
DevicePath["Device Path<br/>(Unique per target)"]
|
|
HardwareId["Hardware ID<br/>(e.g., DEL41B4)"]
|
|
InstanceName["WMI Instance Name<br/>(e.g., DISPLAY\\BOE0900\\...)"]
|
|
MonitorNumber["Monitor Number<br/>(1-based, matches Windows Settings)"]
|
|
end
|
|
|
|
EnumDisplayMonitors --> HMONITOR
|
|
HMONITOR --> GdiDeviceName
|
|
GetPhysicalMonitors --> PhysicalHandle
|
|
|
|
QueryDisplayConfig --> GdiDeviceName
|
|
QueryDisplayConfig --> DevicePath
|
|
QueryDisplayConfig --> HardwareId
|
|
QueryDisplayConfig --> MonitorNumber
|
|
|
|
WmiMonitor --> InstanceName
|
|
InstanceName --> HardwareId
|
|
|
|
style HMONITOR fill:#e3f2fd
|
|
style GdiDeviceName fill:#fff3e0
|
|
style PhysicalHandle fill:#c8e6c9
|
|
style DevicePath fill:#f3e5f5
|
|
style HardwareId fill:#ffccbc
|
|
style InstanceName fill:#ffe0b2
|
|
style MonitorNumber fill:#b2dfdb
|
|
```
|
|
|
|
#### Identifier Definitions
|
|
|
|
| Identifier | Source | Format | Example | Scope |
|
|
|------------|--------|--------|---------|-------|
|
|
| **HMONITOR** | `EnumDisplayMonitors` | `IntPtr` | `0x00010001` | Logical monitor (may represent multiple physical monitors in clone mode) |
|
|
| **GDI Device Name** | `GetMonitorInfo` / `QueryDisplayConfig` | String | `\\.\DISPLAY1` | Adapter output; multiple targets can share same GDI name in mirror mode |
|
|
| **Physical Monitor Handle** | `GetPhysicalMonitorsFromHMONITOR` | `IntPtr` | `0x00000B14` | DDC/CI communication handle; valid for `GetVCPFeature` / `SetVCPFeature` |
|
|
| **Device Path** | `QueryDisplayConfig` | String | `\\?\DISPLAY#DEL41B4#5&12a3b4c&0&UID123#{...}` | Unique per target; used as primary key in `MonitorDisplayInfo` |
|
|
| **Hardware ID** | EDID (via `QueryDisplayConfig`) | String | `DEL41B4` | Manufacturer (3-char PnP ID) + Product Code (4-char hex); identifies monitor model |
|
|
| **WMI Instance Name** | `WmiMonitorBrightness` | String | `DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0` | WMI object identifier; contains hardware ID in second segment |
|
|
| **Monitor Number** | `QueryDisplayConfig` path index | Integer | `1`, `2`, `3` | 1-based; matches Windows Settings → Display → "Identify" feature |
|
|
|
|
#### DDC/CI Monitor Discovery Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant App as PowerDisplay
|
|
participant Enum as EnumDisplayMonitors
|
|
participant Info as GetMonitorInfo
|
|
participant QDC as QueryDisplayConfig
|
|
participant Phys as GetPhysicalMonitors
|
|
participant DDC as DDC/CI (I2C)
|
|
|
|
App->>Enum: EnumDisplayMonitors(callback)
|
|
Enum-->>App: HMONITOR handles
|
|
|
|
loop For each HMONITOR
|
|
App->>Info: GetMonitorInfo(hMonitor)
|
|
Info-->>App: GDI Device Name (e.g., "\\.\DISPLAY1")
|
|
|
|
App->>Phys: GetPhysicalMonitorsFromHMONITOR(hMonitor)
|
|
Phys-->>App: Physical Monitor Handle(s) + Description
|
|
end
|
|
|
|
App->>QDC: QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)
|
|
QDC-->>App: MonitorDisplayInfo[] (DevicePath, GdiDeviceName, HardwareId, MonitorNumber)
|
|
|
|
Note over App: Match Physical Handles to MonitorDisplayInfo<br/>using GDI Device Name
|
|
|
|
loop For each Physical Handle
|
|
App->>DDC: GetCapabilitiesStringLength(handle)
|
|
DDC-->>App: Capabilities length
|
|
App->>DDC: CapabilitiesRequestAndCapabilitiesReply(handle)
|
|
DDC-->>App: Capabilities string (MCCS format)
|
|
end
|
|
|
|
Note over App: Create Monitor objects with:<br/>- Handle (Physical Monitor Handle)<br/>- MonitorNumber (from QueryDisplayConfig)<br/>- GdiDeviceName (for rotation APIs)
|
|
```
|
|
|
|
#### WMI Monitor Discovery Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant App as PowerDisplay
|
|
participant WMI as WmiLight
|
|
participant QDC as QueryDisplayConfig
|
|
participant PnP as PnpIdHelper
|
|
|
|
App->>WMI: Query WmiMonitorBrightness
|
|
WMI-->>App: InstanceName, CurrentBrightness
|
|
|
|
Note over App: Extract HardwareId from InstanceName<br/>"DISPLAY\BOE0900\..." → "BOE0900"
|
|
|
|
App->>QDC: GetAllMonitorDisplayInfo()
|
|
QDC-->>App: MonitorDisplayInfo[] (keyed by DevicePath)
|
|
|
|
Note over App: Match WMI monitor to QueryDisplayConfig<br/>by comparing HardwareId
|
|
|
|
App->>PnP: GetBuiltInDisplayName("BOE0900")
|
|
PnP-->>App: "BOE Built-in Display"
|
|
|
|
Note over App: Create Monitor objects with:<br/>- InstanceName (for WMI queries)<br/>- MonitorNumber (from QueryDisplayConfig)<br/>- GdiDeviceName (for rotation APIs)
|
|
```
|
|
|
|
#### Key Relationships
|
|
|
|
##### GDI Device Name ↔ Physical Monitors
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
HMON["HMONITOR (Logical)"]
|
|
|
|
HMON --> GDI["GetMonitorInfo()<br/>→ GDI Device Name<br/>\.DISPLAY1"]
|
|
HMON --> GetPhys["GetPhysicalMonitorsFromHMONITOR()"]
|
|
|
|
GetPhys --> PM0["Physical Monitor 0<br/>Handle: 0x0B14<br/>Desc: Dell U2722D"]
|
|
GetPhys --> PM1["Physical Monitor 1<br/>Handle: 0x0B18<br/>Desc: Dell U2722D<br/>Mirror mode"]
|
|
|
|
style HMON fill:#e3f2fd
|
|
style PM0 fill:#fff3e0
|
|
style PM1 fill:#fff3e0
|
|
```
|
|
|
|
In **mirror/clone mode**, multiple physical monitors share the same GDI device name.
|
|
QueryDisplayConfig returns multiple paths with the same `GdiDeviceName` but different
|
|
`DevicePath` values, allowing us to distinguish them.
|
|
|
|
##### DisplayPort Daisy Chain (MST - Multi-Stream Transport)
|
|
|
|
**Daisy chaining** allows multiple monitors to be connected in series through a single
|
|
DisplayPort output using MST (Multi-Stream Transport) technology. This creates unique
|
|
challenges for monitor identification.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
GPU["GPU<br/>(Single DP Port)"]
|
|
MonA["Monitor A<br/>(MST Hub)"]
|
|
MonB["Monitor B<br/>(End)"]
|
|
|
|
GPU -->|"DP"| MonA -->|"DP"| MonB
|
|
|
|
subgraph Result["Result: Multiple Logical Displays"]
|
|
D1["DISPLAY1"]
|
|
D2["DISPLAY2"]
|
|
end
|
|
|
|
GPU -.-> Result
|
|
|
|
style GPU fill:#bbdefb
|
|
style MonA fill:#c8e6c9
|
|
style MonB fill:#c8e6c9
|
|
style Result fill:#fff3e0
|
|
```
|
|
|
|
**How Windows Handles MST:**
|
|
|
|
| Aspect | Behavior |
|
|
|--------|----------|
|
|
| **HMONITOR** | Each daisy-chained monitor gets its own HMONITOR |
|
|
| **GDI Device Name** | Each monitor gets a unique GDI name (e.g., `\\.\DISPLAY1`, `\\.\DISPLAY2`) |
|
|
| **Physical Monitor Handle** | Each monitor has its own physical handle for DDC/CI |
|
|
| **Device Path** | Unique for each monitor in the chain |
|
|
| **Hardware ID** | Different if monitors are different models; same if identical models |
|
|
|
|
**MST vs Clone Mode Comparison:**
|
|
|
|
| Property | MST Daisy Chain (Extended Desktop) | Clone/Mirror Mode |
|
|
|----------|-----------------------------------|-------------------|
|
|
| **HMONITOR** | Separate per monitor (HMONITOR_1, HMONITOR_2, ...) | Shared (single HMONITOR_1) |
|
|
| **GDI Device Name** | Unique per monitor (`\\.\DISPLAY1`, `\\.\DISPLAY2`, ...) | Shared (`\\.\DISPLAY1`) |
|
|
| **Physical Handle** | One per HMONITOR (A, B, C) | Multiple per HMONITOR (A, B) |
|
|
| **DevicePath** | Unique per monitor (unique1, unique2, ...) | Unique per monitor (unique1, unique2) |
|
|
| **Behavior** | Each monitor = independent logical display | Multiple monitors share same logical display |
|
|
|
|
**PowerDisplay Handling of MST:**
|
|
|
|
1. **Discovery**: `EnumDisplayMonitors` returns separate HMONITOR for each MST monitor
|
|
2. **Physical Handles**: `GetPhysicalMonitorsFromHMONITOR` returns one handle per HMONITOR
|
|
3. **Matching**: QueryDisplayConfig provides unique DevicePath for each MST target
|
|
4. **DDC/CI**: Each monitor in the chain can be controlled independently via its handle
|
|
|
|
**Identifying Same-Model Monitors in Daisy Chain:**
|
|
|
|
When multiple identical monitors are daisy-chained (same Hardware ID), PowerDisplay
|
|
distinguishes them using:
|
|
|
|
- **MonitorNumber**: Different path index in QueryDisplayConfig (1, 2, 3...)
|
|
- **DevicePath**: Unique system-generated path for each target
|
|
- **Monitor.Id**: Format `DDC_{HardwareId}_{MonitorNumber}` ensures uniqueness
|
|
|
|
Example with two identical Dell U2722D monitors:
|
|
|
|
| Monitor | Id | MonitorNumber |
|
|
|---------|-----|---------------|
|
|
| Monitor 1 | `DDC_DEL41B4_1` | 1 |
|
|
| Monitor 2 | `DDC_DEL41B4_2` | 2 |
|
|
|
|
##### Connection Mode Summary
|
|
|
|
| Mode | HMONITOR | GDI Device Name | Physical Handles | Use Case |
|
|
|------|----------|-----------------|------------------|----------|
|
|
| **Standard** (separate cables) | 1 per monitor | Unique per monitor | 1 per HMONITOR | Most common setup |
|
|
| **Clone/Mirror** | 1 shared | Shared | Multiple per HMONITOR | Presentation, duplication |
|
|
| **MST Daisy Chain** | 1 per monitor | Unique per monitor | 1 per HMONITOR | Reduced cable clutter |
|
|
| **USB-C/Thunderbolt Hub** | 1 per monitor | Unique per monitor | 1 per HMONITOR | Laptop docking |
|
|
|
|
**Key Insight**: From PowerDisplay's perspective, MST daisy chain and standard multi-cable
|
|
setups behave identically - each monitor appears as an independent display with unique
|
|
identifiers. Only clone/mirror mode requires special handling due to shared HMONITOR/GDI names.
|
|
|
|
##### Hardware ID Composition
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
HardwareId["Hardware ID: DEL41B4"]
|
|
|
|
HardwareId --> PnpId["DEL<br/>PnP Manufacturer ID<br/>3 chars, EDID bytes 8-9"]
|
|
HardwareId --> ProductCode["41B4<br/>Product Code<br/>4 hex chars, EDID bytes 10-11"]
|
|
|
|
style HardwareId fill:#fff3e0
|
|
style PnpId fill:#c8e6c9
|
|
style ProductCode fill:#bbdefb
|
|
```
|
|
|
|
The **PnP Manufacturer ID** is a 3-character code assigned by UEFI Forum.
|
|
Common laptop display manufacturers:
|
|
|
|
| PnP ID | Manufacturer |
|
|
|--------|--------------|
|
|
| `BOE` | BOE Technology |
|
|
| `LGD` | LG Display |
|
|
| `AUO` | AU Optronics |
|
|
| `CMN` | Chi Mei Innolux |
|
|
| `SDC` | Samsung Display |
|
|
| `SHP` | Sharp |
|
|
| `LEN` | Lenovo |
|
|
| `DEL` | Dell |
|
|
|
|
##### WMI Instance Name Parsing
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
InstanceName["WMI InstanceName:<br/>DISPLAY\BOE0900\4#amp;10fd3ab1#amp;0#amp;UID265988_0"]
|
|
|
|
InstanceName --> Seg1["Segment 1: DISPLAY<br/>Constant prefix"]
|
|
InstanceName --> Seg2["Segment 2: BOE0900<br/>Hardware ID<br/>Used for matching with QueryDisplayConfig"]
|
|
InstanceName --> Seg3["Segment 3: Device instance<br/>4#amp;10fd3ab1#amp;0#amp;UID265988_0"]
|
|
|
|
style InstanceName fill:#fff3e0
|
|
style Seg1 fill:#e0e0e0
|
|
style Seg2 fill:#c8e6c9
|
|
style Seg3 fill:#e0e0e0
|
|
```
|
|
|
|
##### Monitor Number (Windows Display Settings)
|
|
|
|
The `MonitorNumber` in PowerDisplay corresponds exactly to the number shown in:
|
|
- Windows Settings → System → Display → "Identify" button
|
|
- The number overlay that appears on each display
|
|
|
|
This is derived from the **path index** in `QueryDisplayConfig`:
|
|
- `paths[0]` → Monitor 1
|
|
- `paths[1]` → Monitor 2
|
|
- etc.
|
|
|
|
#### Display Rotation and GDI Device Name
|
|
|
|
The `ChangeDisplaySettingsEx` API requires the **GDI Device Name** to target a specific display:
|
|
|
|
```cpp
|
|
// Correct: Target specific display by GDI name
|
|
ChangeDisplaySettingsEx("\\.\DISPLAY2", &devMode, NULL, 0, NULL);
|
|
|
|
// Wrong: NULL affects primary display only
|
|
ChangeDisplaySettingsEx(NULL, &devMode, NULL, 0, NULL);
|
|
```
|
|
|
|
PowerDisplay stores `GdiDeviceName` in each `Monitor` object specifically for rotation operations.
|
|
|
|
#### Cross-Reference Summary
|
|
|
|
| PowerDisplay Property | DDC/CI Source | WMI Source |
|
|
|-----------------------|---------------|------------|
|
|
| `Monitor.Id` | `"DDC_{HardwareId}_{MonitorNumber}"` | `"WMI_{HardwareId}_{MonitorNumber}"` |
|
|
| `Monitor.Handle` | Physical Monitor Handle | N/A (uses InstanceName) |
|
|
| `Monitor.InstanceName` | N/A | WMI InstanceName |
|
|
| `Monitor.GdiDeviceName` | QueryDisplayConfig | QueryDisplayConfig |
|
|
| `Monitor.MonitorNumber` | QueryDisplayConfig path index | QueryDisplayConfig (matched by HardwareId) |
|
|
| `Monitor.Name` | EDID FriendlyName or Description | PnpIdHelper.GetBuiltInDisplayName() |
|
|
|
|
---
|
|
|
|
### Settings UI and PowerDisplay Interaction Architecture
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph SettingsUI["Settings UI Process"]
|
|
direction TB
|
|
Page["PowerDisplayPage.xaml"]
|
|
VM["PowerDisplayViewModel"]
|
|
Page --> VM
|
|
end
|
|
|
|
subgraph Runner["Runner Process"]
|
|
direction TB
|
|
Exe["PowerToys.exe"]
|
|
Pipe["Named Pipe IPC"]
|
|
Module["PowerDisplayModuleInterface.dll"]
|
|
Pipe --> Exe --> Module
|
|
end
|
|
|
|
subgraph PDApp["PowerDisplay Process"]
|
|
direction TB
|
|
MainVM["MainViewModel"]
|
|
Events["Event Listeners<br/>Refresh / Profile"]
|
|
Events --> MainVM
|
|
end
|
|
|
|
subgraph Storage["File System"]
|
|
direction TB
|
|
Settings[("settings.json")]
|
|
Profiles[("profiles.json")]
|
|
end
|
|
|
|
%% Main flow: Settings UI → Runner → PowerDisplay
|
|
VM -->|"IPC Message"| Pipe
|
|
Module -->|"SetEvent()"| Events
|
|
|
|
%% File access
|
|
VM <-.->|"Read/Write"| Settings
|
|
VM <-.->|"Read/Write"| Profiles
|
|
MainVM <-.->|"Read"| Settings
|
|
MainVM <-.->|"Read/Write"| Profiles
|
|
|
|
style SettingsUI fill:#e3f2fd
|
|
style Runner fill:#fff3e0
|
|
style PDApp fill:#e8f5e9
|
|
style Storage fill:#fffde7
|
|
```
|
|
|
|
**Data Models (in Settings.UI.Library):**
|
|
|
|
| Model | Purpose |
|
|
|-------|---------|
|
|
| `PowerDisplaySettings` | Main settings container with properties |
|
|
| `MonitorInfo` | Per-monitor settings displayed in Settings UI (includes feature visibility flags like `EnableColorTemperature`) |
|
|
|
|
### Windows Events for IPC
|
|
|
|
Event names use fixed GUID suffixes to ensure uniqueness (defined in `shared_constants.h`).
|
|
|
|
| Constant | Direction | Purpose |
|
|
|----------|-----------|---------|
|
|
| `TOGGLE_POWER_DISPLAY_EVENT` | Runner → App | Toggle visibility |
|
|
| `TERMINATE_POWER_DISPLAY_EVENT` | Runner → App | Terminate process |
|
|
| `REFRESH_POWER_DISPLAY_MONITORS_EVENT` | Settings → App | Refresh monitor list |
|
|
| `SETTINGS_UPDATED_POWER_DISPLAY_EVENT` | Settings → App | Notify settings changed (feature visibility, tray icon) |
|
|
| `LightSwitchLightThemeEventName` | LightSwitch → App | Apply light mode profile |
|
|
| `LightSwitchDarkThemeEventName` | LightSwitch → App | Apply dark mode profile |
|
|
|
|
**Profile Application via Named Pipe IPC:**
|
|
|
|
Profile application from Settings UI uses Named Pipe IPC (via Runner's `call_custom_action`) instead of
|
|
Windows Events. When the user clicks "Apply" on a profile in Settings UI, the message is sent through
|
|
the Runner to the Module Interface, which forwards it to PowerDisplay.exe via Named Pipe.
|
|
|
|
**Event Name Format:** `Local\PowerToysPowerDisplay-{EventType}-{GUID}`
|
|
|
|
Example: `Local\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c`
|
|
|
|
---
|
|
|
|
### LightSwitch Profile Integration Architecture
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph LightSwitchModule["LightSwitch Module (C++)"]
|
|
StateManager["LightSwitchStateManager"]
|
|
ThemeEval["Theme Evaluation<br/>(Time/System)"]
|
|
LightSwitchSettings["LightSwitchSettings"]
|
|
NotifyPD["NotifyPowerDisplay(isLight)"]
|
|
end
|
|
|
|
subgraph PowerDisplayModule["PowerDisplay Module (C#)"]
|
|
subgraph App["PowerDisplay App"]
|
|
EventWaiter["NativeEventWaiter<br/>(Background Thread)"]
|
|
LightSwitchSvc["LightSwitchService<br/>(Static Helper)"]
|
|
MainViewModel["MainViewModel"]
|
|
end
|
|
|
|
ProfileService["ProfileService"]
|
|
MonitorVMs["MonitorViewModels"]
|
|
Controllers["IMonitorController"]
|
|
end
|
|
|
|
subgraph WindowsEvents["Windows Events"]
|
|
LightEvent["Local\\PowerToys_LightSwitch_LightTheme"]
|
|
DarkEvent["Local\\PowerToys_LightSwitch_DarkTheme"]
|
|
end
|
|
|
|
subgraph FileSystem["File System"]
|
|
LSSettingsJson["LightSwitch/settings.json<br/>{lightProfile, darkProfile}"]
|
|
PDProfilesJson["PowerDisplay/profiles.json<br/>{profiles: [...]}"]
|
|
end
|
|
|
|
subgraph Hardware["Hardware"]
|
|
Monitors["Connected Monitors"]
|
|
end
|
|
|
|
%% LightSwitch flow
|
|
ThemeEval -->|"Time boundary<br/>or manual"| StateManager
|
|
StateManager --> LightSwitchSettings
|
|
StateManager --> NotifyPD
|
|
NotifyPD -->|"isLight=true"| LightEvent
|
|
NotifyPD -->|"isLight=false"| DarkEvent
|
|
|
|
%% PowerDisplay flow - theme determined from event
|
|
LightEvent -->|"Event signaled"| EventWaiter
|
|
DarkEvent -->|"Event signaled"| EventWaiter
|
|
EventWaiter -->|"isLightMode"| LightSwitchSvc
|
|
LightSwitchSvc -->|"GetProfileForTheme()"| LSSettingsJson
|
|
LightSwitchSvc -->|"Profile name"| MainViewModel
|
|
MainViewModel -->|"LoadProfiles()"| ProfileService
|
|
ProfileService <--> PDProfilesJson
|
|
MainViewModel -->|"ApplyProfileAsync()"| MonitorVMs
|
|
MonitorVMs --> Controllers
|
|
Controllers --> Monitors
|
|
|
|
style LightSwitchModule fill:#ffccbc
|
|
style PowerDisplayModule fill:#c8e6c9
|
|
style App fill:#a5d6a7
|
|
style WindowsEvents fill:#e3f2fd
|
|
style FileSystem fill:#fffde7
|
|
```
|
|
|
|
### LightSwitch Settings JSON Structure
|
|
|
|
```json
|
|
{
|
|
"properties": {
|
|
"apply_monitor_settings": { "value": true },
|
|
"enable_light_mode_profile": { "value": true },
|
|
"light_mode_profile": { "value": "Productivity" },
|
|
"enable_dark_mode_profile": { "value": true },
|
|
"dark_mode_profile": { "value": "Night Mode" }
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow and Communication
|
|
|
|
### Monitor Discovery Flow
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
Start([Start Discovery])
|
|
Start --> MM["MonitorManager.DiscoverMonitorsAsync()"]
|
|
|
|
MM --> DDC["DdcCiController.DiscoverMonitorsAsync()"]
|
|
MM --> WMI["WmiController.DiscoverMonitorsAsync()"]
|
|
|
|
DDC --> Merge["Merge Results"]
|
|
WMI --> Merge
|
|
|
|
Merge --> Sort["Sort by MonitorNumber"]
|
|
Sort --> Update["UpdateMonitorList()"]
|
|
Update --> Check{"RestoreSettingsOnStartup?"}
|
|
Check -->|Yes| Restore["RestoreMonitorSettingsAsync()<br/>(Set hardware values)"]
|
|
Check -->|No| Done
|
|
Restore --> Done([Discovery Complete])
|
|
|
|
style Start fill:#e8f5e9
|
|
style Done fill:#e8f5e9
|
|
style DDC fill:#e3f2fd
|
|
style WMI fill:#fff3e0
|
|
style Restore fill:#fff9c4
|
|
```
|
|
|
|
> **Note:** DDC/CI and WMI discovery run in parallel via `Task.WhenAll`.
|
|
>
|
|
> **Settings Restore:** When `RestoreSettingsOnStartup` is enabled, `RestoreMonitorSettingsAsync()` is called
|
|
> after monitor discovery to restore saved brightness, contrast, color temperature, and volume values
|
|
> to the hardware. The UI remains in "scanning" state until restore completes.
|
|
|
|
#### DDC/CI Discovery (Three-Phase Approach)
|
|
|
|
**Phase 1: Collect Candidates**
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
QDC["QueryDisplayConfig"] --> Match["Match by GDI Name"]
|
|
Enum["EnumDisplayMonitors"] --> GetPhys["GetPhysicalMonitors"] --> Match
|
|
Match --> Candidates["CandidateMonitor List"]
|
|
|
|
style QDC fill:#e3f2fd
|
|
style Enum fill:#e3f2fd
|
|
```
|
|
|
|
**Phase 2: Fetch Capabilities (Parallel)**
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Candidates["CandidateMonitor List"] --> Fetch["Task.WhenAll:<br/>FetchCapabilities<br/>~4s per monitor via I2C"]
|
|
Fetch --> Results["DdcCiValidationResult Array"]
|
|
|
|
style Fetch fill:#fff3e0
|
|
```
|
|
|
|
**Phase 3: Create Monitors**
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Results["Validation Results"] --> Check{"IsValid?"}
|
|
Check -->|Yes| Create["Create Monitor"]
|
|
Create --> Init["Initialize VCP Values:<br/>Brightness, ColorTemp, InputSource"]
|
|
Init --> Add["Add to List"]
|
|
Check -->|No| Skip([Skip])
|
|
|
|
style Create fill:#e8f5e9
|
|
style Init fill:#e8f5e9
|
|
```
|
|
|
|
#### WMI Discovery
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Query["Query WmiMonitorBrightness"] --> Extract["Extract HardwareId<br/>from InstanceName"]
|
|
QDC["QueryDisplayConfig"] --> Match["Match by HardwareId"]
|
|
Extract --> Match
|
|
Match --> Name["Get Display Name<br/>via PnpIdHelper"]
|
|
Name --> Create["Create Monitor<br/>Brightness + WMI"]
|
|
|
|
style Query fill:#fff3e0
|
|
style Create fill:#fff3e0
|
|
```
|
|
|
|
#### Key Differences
|
|
|
|
| Aspect | DDC/CI | WMI |
|
|
|--------|--------|-----|
|
|
| **Target** | External monitors | Internal laptop displays |
|
|
| **Capabilities** | Full VCP support (brightness, contrast, volume, color temp, input) | Brightness only |
|
|
| **Discovery** | Three-phase with parallel I2C fetching | Single WMI query |
|
|
| **Initialization** | Reads current values for all supported VCP codes | Brightness from query result |
|
|
| **Performance** | ~4s per monitor (I2C), parallelized | Fast (~100ms total) |
|
|
|
|
---
|
|
|
|
## Sequence Diagrams
|
|
|
|
### Sequence: Modifying Color Temperature in Flyout UI
|
|
|
|
Color temperature adjustment is now handled directly in the PowerDisplay Flyout UI,
|
|
providing a more responsive user experience without requiring IPC round-trips to Settings UI.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant Flyout as MainWindow (Flyout)
|
|
participant MonitorVM as MonitorViewModel
|
|
participant MonitorManager
|
|
participant Controller as DdcCiController
|
|
participant StateManager as MonitorStateManager
|
|
participant Monitor as Physical Monitor
|
|
|
|
User->>Flyout: Opens PowerDisplay flyout<br/>(via hotkey or tray icon)
|
|
|
|
Note over Flyout: Color temperature switcher visible<br/>(if enabled in Settings)
|
|
|
|
User->>Flyout: Selects color temperature preset<br/>from dropdown (e.g., 6500K)
|
|
|
|
Flyout->>MonitorVM: ColorTemperatureListView_SelectionChanged
|
|
MonitorVM->>MonitorVM: SetColorTemperatureAsync(vcpValue)
|
|
|
|
MonitorVM->>MonitorManager: SetColorTemperatureAsync(monitor, vcpValue)
|
|
|
|
MonitorManager->>Controller: SetColorTemperatureAsync(monitor, vcpValue)
|
|
Controller->>Controller: SetVcpFeatureAsync(VcpCodeColorTemperature)
|
|
Controller->>Monitor: SetVCPFeature(0x14, vcpValue)
|
|
Monitor-->>Controller: OK
|
|
|
|
Controller-->>MonitorManager: MonitorOperationResult.Success
|
|
MonitorManager-->>MonitorVM: Success
|
|
|
|
MonitorVM->>MonitorVM: RefreshAvailableColorPresets()
|
|
Note over MonitorVM: Regenerate ColorTemperatureItem list<br/>with updated IsSelected flags
|
|
|
|
MonitorVM->>StateManager: UpdateMonitorParameter("ColorTemperature", vcpValue)
|
|
|
|
Note over StateManager: Debounced save (2 seconds)
|
|
StateManager->>StateManager: Schedule file write
|
|
|
|
Note over StateManager: After 2s idle
|
|
StateManager->>StateManager: SaveToFile(monitor_state.json)
|
|
|
|
Note over MonitorVM: UI updates to show<br/>selected preset with checkmark
|
|
```
|
|
|
|
**Color Temperature Selection UI:**
|
|
|
|
The color temperature switcher displays a list of available presets (e.g., 5000K, 6500K, sRGB). Each preset
|
|
shows a checkmark icon when selected. The `ColorTemperatureItem` class stores `IsSelected` state, which is
|
|
updated by regenerating the entire `AvailableColorPresets` list after a successful color temperature change.
|
|
This ensures the checkmark displays correctly for the newly selected preset.
|
|
|
|
**Flyout Display Options:**
|
|
|
|
The Flyout UI visibility is controlled by a combination of global settings and per-monitor settings:
|
|
|
|
**Global Settings (in `PowerDisplayProperties`):**
|
|
|
|
| Setting | Default | Description |
|
|
|---------|---------|-------------|
|
|
| `ShowProfileSwitcher` | `true` | Show profile switcher (also requires profiles to exist) |
|
|
| `ShowIdentifyMonitorsButton` | `true` | Show "Identify Monitors" button |
|
|
|
|
**Per-Monitor Settings (in `MonitorInfo`):**
|
|
|
|
| Setting | Default | Description |
|
|
|---------|---------|-------------|
|
|
| `EnableContrast` | `true` (if supported) | Show contrast slider for this monitor |
|
|
| `EnableVolume` | `true` (if supported) | Show volume slider for this monitor |
|
|
| `EnableInputSource` | `true` (if supported) | Show input source selector for this monitor |
|
|
| `EnableRotation` | `true` | Show rotation control for this monitor |
|
|
| `EnableColorTemperature` | `true` (if supported) | Show color temperature switcher for this monitor |
|
|
| `IsHidden` | `false` | Hide this monitor from the flyout entirely |
|
|
|
|
Users can configure per-monitor visibility in Settings UI under the "Monitors" section. Each monitor
|
|
shows checkboxes for the features it supports, allowing fine-grained control over the flyout UI.
|
|
|
|
**Color Temperature Warning Dialog:**
|
|
|
|
When enabling `EnableColorTemperature` for a monitor in Settings UI, a warning dialog is displayed to inform
|
|
users about potential risks. Color temperature changes can cause unpredictable results on some monitors,
|
|
including incorrect colors, display malfunction, or settings that cannot be reverted. The dialog requires
|
|
explicit confirmation before enabling the feature.
|
|
|
|
Implementation notes:
|
|
- The warning dialog only appears when the user explicitly checks the checkbox (not during initial page load)
|
|
- A `_isPageLoaded` flag prevents the dialog from appearing during data binding
|
|
- If the user cancels the dialog, the checkbox is reverted to unchecked state
|
|
|
|
---
|
|
|
|
### Sequence: Creating and Saving a Profile
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant SettingsPage as PowerDisplayPage
|
|
participant ViewModel as PowerDisplayViewModel
|
|
participant ProfileDialog as ProfileEditorDialog
|
|
participant ProfileService
|
|
participant FileSystem as profiles.json
|
|
|
|
User->>SettingsPage: Clicks "Add Profile" button
|
|
SettingsPage->>ViewModel: ShowProfileEditor()
|
|
|
|
ViewModel->>ProfileDialog: Show(monitors, existingProfiles)
|
|
ProfileDialog->>ProfileDialog: Display monitor selection UI
|
|
|
|
User->>ProfileDialog: Enters profile name
|
|
User->>ProfileDialog: Selects monitors to include
|
|
User->>ProfileDialog: Configures settings per monitor<br/>(brightness, contrast, etc.)
|
|
User->>ProfileDialog: Clicks "Save"
|
|
|
|
ProfileDialog->>ProfileDialog: Validate inputs
|
|
Note over ProfileDialog: Check name unique,<br/>at least one monitor selected
|
|
|
|
ProfileDialog-->>ViewModel: ResultProfile (PowerDisplayProfile)
|
|
|
|
ViewModel->>ProfileService: AddOrUpdateProfile(profile)
|
|
|
|
ProfileService->>ProfileService: lock(_lock)
|
|
ProfileService->>FileSystem: Read profiles.json
|
|
FileSystem-->>ProfileService: Existing profiles
|
|
ProfileService->>ProfileService: Add/update profile in collection
|
|
ProfileService->>ProfileService: Set LastUpdated = DateTime.Now
|
|
ProfileService->>FileSystem: Write profiles.json
|
|
FileSystem-->>ProfileService: Success
|
|
ProfileService-->>ViewModel: true
|
|
|
|
ViewModel->>ViewModel: RefreshProfilesList()
|
|
ViewModel-->>SettingsPage: PropertyChanged(Profiles)
|
|
SettingsPage->>SettingsPage: Update UI with new profile
|
|
```
|
|
|
|
---
|
|
|
|
### Sequence: Applying Profile via LightSwitch Theme Change
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant System as Windows System
|
|
participant LightSwitch as LightSwitchStateManager (C++)
|
|
participant WinEvent as Windows Events
|
|
participant EventWaiter as NativeEventWaiter
|
|
participant LSSvc as LightSwitchService
|
|
participant MainVM as MainViewModel
|
|
participant ProfileService
|
|
participant MonitorVM as MonitorViewModel
|
|
participant Controller as IMonitorController
|
|
participant Monitor as Physical Monitor
|
|
|
|
Note over System: Time reaches threshold<br/>or user changes theme
|
|
System->>LightSwitch: Theme change detected
|
|
|
|
LightSwitch->>LightSwitch: EvaluateAndApplyIfNeeded()
|
|
LightSwitch->>LightSwitch: ApplyTheme(isLight)
|
|
|
|
LightSwitch->>LightSwitch: NotifyPowerDisplay(isLight)
|
|
Note over LightSwitch: Check if profile enabled
|
|
|
|
alt isLight == true
|
|
LightSwitch->>WinEvent: SetEvent("Local\\PowerToys_LightSwitch_LightTheme")
|
|
else isLight == false
|
|
LightSwitch->>WinEvent: SetEvent("Local\\PowerToys_LightSwitch_DarkTheme")
|
|
end
|
|
|
|
Note over EventWaiter: Background thread waiting<br/>on both Light and Dark events
|
|
EventWaiter->>WinEvent: WaitAny([lightEvent, darkEvent]) returns index
|
|
|
|
Note over EventWaiter: Theme determined from event:<br/>index 0 = Light, index 1 = Dark
|
|
EventWaiter->>LSSvc: GetProfileForTheme(isLightMode)
|
|
LSSvc->>LSSvc: Read LightSwitch/settings.json
|
|
LSSvc-->>EventWaiter: profileName (or null)
|
|
|
|
EventWaiter->>MainVM: Dispatch to UI thread with profileName
|
|
|
|
MainVM->>ProfileService: LoadProfiles()
|
|
ProfileService-->>MainVM: PowerDisplayProfiles
|
|
|
|
MainVM->>MainVM: Find profile by name
|
|
MainVM->>MainVM: ApplyProfileAsync(profile.MonitorSettings)
|
|
|
|
loop For each ProfileMonitorSetting
|
|
MainVM->>MainVM: Find MonitorViewModel by InternalName
|
|
|
|
alt Brightness specified
|
|
MainVM->>MonitorVM: SetBrightnessAsync(value, immediate=true)
|
|
MonitorVM->>Controller: SetBrightnessAsync(monitor, value)
|
|
Controller->>Monitor: DDC/CI or WMI call
|
|
Monitor-->>Controller: Success
|
|
end
|
|
|
|
alt Contrast specified
|
|
MainVM->>MonitorVM: SetContrastAsync(value, immediate=true)
|
|
MonitorVM->>Controller: SetContrastAsync(monitor, value)
|
|
Controller->>Monitor: SetVCPFeature(0x12, value)
|
|
end
|
|
|
|
alt Volume specified
|
|
MainVM->>MonitorVM: SetVolumeAsync(value, immediate=true)
|
|
MonitorVM->>Controller: SetVolumeAsync(monitor, value)
|
|
Controller->>Monitor: SetVCPFeature(0x62, value)
|
|
end
|
|
|
|
alt ColorTemperature specified
|
|
MainVM->>MonitorVM: SetColorTemperatureAsync(vcpValue)
|
|
MonitorVM->>Controller: SetColorTemperatureAsync(monitor, vcpValue)
|
|
Controller->>Monitor: SetVCPFeature(0x14, vcpValue)
|
|
end
|
|
|
|
alt Orientation specified
|
|
MainVM->>MonitorVM: SetOrientationAsync(orientation)
|
|
MonitorVM->>Controller: SetRotationAsync(monitor, orientation)
|
|
Controller->>Monitor: ChangeDisplaySettingsEx
|
|
end
|
|
end
|
|
|
|
Note over MainVM: await Task.WhenAll(updateTasks)
|
|
MainVM->>MainVM: Log profile application complete
|
|
```
|
|
|
|
---
|
|
|
|
### Sequence: UI Slider Adjustment (Brightness)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant Slider as Brightness Slider
|
|
participant MonitorVM as MonitorViewModel
|
|
participant Debouncer as SimpleDebouncer
|
|
participant MonitorManager
|
|
participant Controller as DdcCiController
|
|
participant StateManager as MonitorStateManager
|
|
participant Monitor as Physical Monitor
|
|
|
|
User->>Slider: Drags slider (continuous)
|
|
|
|
loop During drag (multiple events)
|
|
Slider->>MonitorVM: CurrentBrightness = value
|
|
MonitorVM->>MonitorVM: SetBrightnessAsync(value, immediate=false)
|
|
MonitorVM->>Debouncer: Debounce(300ms)
|
|
Note over Debouncer: Resets timer on each call
|
|
end
|
|
|
|
User->>Slider: Releases slider
|
|
|
|
Note over Debouncer: 300ms elapsed, no new input
|
|
Debouncer->>MonitorVM: Execute debounced action
|
|
|
|
MonitorVM->>MonitorVM: ApplyBrightnessToHardwareAsync()
|
|
MonitorVM->>MonitorManager: SetBrightnessAsync(monitor, finalValue)
|
|
|
|
MonitorManager->>Controller: SetBrightnessAsync(monitor, value)
|
|
|
|
Controller->>Controller: SetVcpFeatureAsync(VcpCodeBrightness)
|
|
Controller->>Monitor: SetVCPFeature(0x10, value)
|
|
Monitor-->>Controller: OK
|
|
|
|
Controller-->>MonitorManager: MonitorOperationResult
|
|
MonitorManager-->>MonitorVM: Success/Failure
|
|
|
|
MonitorVM->>StateManager: UpdateMonitorParameter("Brightness", value)
|
|
|
|
Note over StateManager: Debounced save (2 seconds)
|
|
StateManager->>StateManager: Schedule file write
|
|
|
|
Note over StateManager: After 2s idle
|
|
StateManager->>StateManager: SaveToFile(monitor_state.json)
|
|
```
|
|
|
|
---
|
|
|
|
### Sequence: Module Enable/Disable Lifecycle
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Runner as PowerToys Runner
|
|
participant ModuleInterface as PowerDisplayModule (C++)
|
|
participant PowerDisplayApp as PowerDisplay.exe
|
|
participant MonitorManager
|
|
participant StateManager as MonitorStateManager
|
|
participant EventHandles as Windows Events
|
|
|
|
Note over Runner: User enables PowerDisplay
|
|
Runner->>ModuleInterface: enable()
|
|
|
|
ModuleInterface->>ModuleInterface: m_enabled = true
|
|
ModuleInterface->>ModuleInterface: Trace::EnablePowerDisplay(true)
|
|
|
|
ModuleInterface->>ModuleInterface: is_process_running()
|
|
alt Process not running
|
|
ModuleInterface->>PowerDisplayApp: ShellExecuteExW("PowerToys.PowerDisplay.exe", pid)
|
|
PowerDisplayApp->>PowerDisplayApp: Initialize WinUI 3 App
|
|
PowerDisplayApp->>PowerDisplayApp: RegisterSingletonInstance()
|
|
PowerDisplayApp->>MonitorManager: DiscoverMonitorsAsync()
|
|
|
|
alt RestoreSettingsOnStartup enabled
|
|
PowerDisplayApp->>StateManager: GetMonitorParameters(monitorId)
|
|
StateManager-->>PowerDisplayApp: Saved brightness, contrast, etc.
|
|
PowerDisplayApp->>MonitorManager: SetBrightnessAsync(savedValue)
|
|
PowerDisplayApp->>MonitorManager: SetContrastAsync(savedValue)
|
|
Note over PowerDisplayApp: Restore all saved settings to hardware
|
|
end
|
|
|
|
PowerDisplayApp->>PowerDisplayApp: Start event listeners
|
|
PowerDisplayApp->>EventHandles: SetEvent("Ready")
|
|
end
|
|
|
|
ModuleInterface->>ModuleInterface: m_hProcess = sei.hProcess
|
|
|
|
Note over Runner: User presses hotkey
|
|
Runner->>ModuleInterface: on_hotkey()
|
|
ModuleInterface->>EventHandles: SetEvent(ToggleEvent)
|
|
EventHandles->>PowerDisplayApp: Toggle visibility
|
|
|
|
Note over Runner: User disables PowerDisplay
|
|
Runner->>ModuleInterface: disable()
|
|
|
|
ModuleInterface->>EventHandles: ResetEvent(InvokeEvent)
|
|
ModuleInterface->>EventHandles: SetEvent(TerminateEvent)
|
|
|
|
PowerDisplayApp->>PowerDisplayApp: Receive terminate signal
|
|
PowerDisplayApp->>MonitorManager: Dispose()
|
|
PowerDisplayApp->>PowerDisplayApp: Application.Exit()
|
|
|
|
ModuleInterface->>ModuleInterface: CloseHandle(m_hProcess)
|
|
ModuleInterface->>ModuleInterface: m_enabled = false
|
|
ModuleInterface->>ModuleInterface: Trace::EnablePowerDisplay(false)
|
|
```
|
|
|
|
---
|
|
|
|
## Future Considerations
|
|
|
|
### Already Implemented
|
|
|
|
- **Monitor Hot-Plug**: `DisplayChangeWatcher` uses WinRT DeviceWatcher + DisplayMonitor API with 1-second debouncing
|
|
- **Display Rotation**: `DisplayRotationService` uses Windows ChangeDisplaySettingsEx API
|
|
- **LightSwitch Integration**: Automatic profile application on theme changes via `LightSwitchService`
|
|
- **Monitor Identification**: Overlay windows showing monitor numbers via `IdentifyWindow`
|
|
- **Mirror Mode Support**: Correct orientation sync for multiple monitors sharing the same GDI device name
|
|
|
|
### Potential Future Enhancements
|
|
|
|
1. **Advanced Color Management**: Integration with Windows Color Management APIs (HDR, ICC profiles)
|
|
2. **PIP/PBP Control**: Picture-in-Picture and Picture-by-Picture configuration (VcpCapabilities already parses window capabilities)
|
|
3. **Power State Control**: Monitor power on/off via VCP code 0xD6
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [VESA DDC/CI Standard](https://vesa.org/vesa-standards/)
|
|
- [MCCS (Monitor Control Command Set) Specification](https://vesa.org/vesa-standards/)
|
|
- [Microsoft High-Level Monitor Configuration API](https://learn.microsoft.com/en-us/windows/win32/monitor/high-level-monitor-configuration-api)
|
|
- [WMI Reference](https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-reference)
|
|
- [WmiMonitorBrightness Class](https://learn.microsoft.com/en-us/windows/win32/wmicoreprov/wmimonitorbrightness)
|
|
- [PowerToys Architecture Documentation](../../core/architecture.md)
|