Driver-level display profile manager for Windows 11
Replaces the volatile Win+P projection menu with a deterministic, hardware-keyed configuration engine built on the CCD API and EDID correlation.
Windows Display Driver Model (WDDM) assigns volatile target IDs to connected monitors. These identifiers are not persisted across:
- Sleep/Hibernate cycles (S3/S4 power state transitions)
- Hot-plug events (cable disconnection and reconnection)
- Driver resets (TDR recovery, driver updates)
As a result, Windows cannot reliably restore multi-monitor layouts. A user with three displays may wake their machine to find monitors mirrored, repositioned, or assigned incorrect refresh rates — a state the OS considers correct because the volatile identifiers have been reassigned.
The built-in Win+P projection menu offers only four static topology presets (PC screen only, Duplicate, Extend, Second screen only) and has no concept of per-display geometry or persistent hardware identity. It is architecturally incapable of solving this problem.
DispEx operates below the Win32 EnumDisplayDevices / ChangeDisplaySettings API surface, directly interfacing with the Connecting and Configuring Displays (CCD) pipeline — the same kernel-facing API used by the Windows display topology manager.
Instead of relying on volatile WDDM target IDs, DispEx constructs a Composite Hardware Key for each monitor:
CompositeKey = {EDID_ManufacturerID}-{EDID_ProductCode}-{WMI_SerialNumber}
─────────────────────────────────────────────────────────
Extracted via CCD API Correlated via WmiMonitorID
(DisplayConfigGetDeviceInfo) (System.Management / WMI)
This key is invariant across power state transitions and hot-plug events. It uniquely identifies a physical display panel regardless of which port, adapter, or target ID the OS assigns to it.
When a user saves a profile, DispEx captures the full geometric state from the live CCD mode buffers:
| Parameter | Source | Stability |
|---|---|---|
| Resolution (W×H) | DISPLAYCONFIG_SOURCE_MODE |
Per-session |
| Desktop position (X, Y) | DISPLAYCONFIG_SOURCE_MODE.position |
Per-session |
| Refresh rate (N/D) | DISPLAYCONFIG_TARGET_MODE.vSyncFreq |
Per-session |
| Primary display | Path array index [0] | Per-session |
| Hardware identity | EDID + WMI Composite Key | Permanent |
On restore, the engine performs reverse matching: it queries the current topology, builds Composite Keys for every active target, then patches the live DISPLAYCONFIG_MODE_INFO buffers with the saved geometry. The patched arrays are applied atomically via SetDisplayConfig with SDC_USE_SUPPLIED_DISPLAY_CONFIG.
┌──────────────────────────────────────────────────────┐
│ MainWindow.xaml │
│ (DesktopAcrylic flyout overlay) │
├──────────────────────────────────────────────────────┤
│ MainViewModel ──► ProfileListViewModel │
│ ├─ SaveCurrentTopologyCommand │
│ ├─ ApplyProfileCommand │
│ ├─ DeleteProfileCommand │
│ ├─ OpenDisplaySettingsCommand │
│ └─ SaveProfileOrderAsync() │
├──────────────────────────────────────────────────────┤
│ KeyboardHookService │ WeakReferenceMessenger │
│ (WH_KEYBOARD_LL) │ (ToggleFlyout/Notify) │
├──────────────┬───────────┴───────────┬───────────────┤
│ CcdDisplay │ WmiHardware │ ProfileApply │ Json │
│ Manager │ Provider │ Service │ Store │
├──────────────┴──────────────┴────────────────┴───────┤
│ Win32 CCD API (CsWin32 P/Invoke) │
│ WMI (System.Management) │
└──────────────────────────────────────────────────────┘
A WH_KEYBOARD_LL hook intercepts the Win+P keystroke at the lowest user-mode level, suppressing the default Windows projection menu. The hook callback is GC-pinned for the application's lifetime and protected by a try-catch guard to prevent silent hook removal by the OS on unhandled exceptions. A 300ms debounce filter prevents rapid re-triggering.
The overlay is rendered as a borderless WinUI 3 tool window (OverlappedPresenter.CreateForToolWindow) with DesktopAcrylicBackdrop. It is hidden from the taskbar and Alt+Tab, positioned at the right edge of the primary display, and toggled by the keyboard hook.
All CCD API interactions use ArrayPool<T> for buffer management with exception-filter-based return guarantees (catch when pattern). EDID manufacturer IDs are decoded via bit manipulation on stack-allocated values. Span<T> is used throughout to avoid intermediate array copies.
Profiles are persisted to %LocalAppData%\DispEx\profiles.json using System.Text.Json source generation (JsonSerializerContext), eliminating all reflection overhead. File writes use atomic temp-file-then-rename operations with orphaned file cleanup on failure.
The presentation layer uses CommunityToolkit.Mvvm source generators ([ObservableProperty], [RelayCommand], [NotifyCanExecuteChangedFor]) for zero-boilerplate property change notification and command binding. All IsBusy state transitions are marshaled to the UI thread via DispatcherQueue.
If a previously working profile suddenly stops applying correctly, it is highly likely caused by a recent Windows System Update or a Graphics Driver (GPU) Update.
These updates can reset or alter the internal hardware paths and display identifiers used by the Windows Display Driver Model (WDDM). When this happens, the paths saved in your profiles.json no longer match the current system state.
Solution: Simply arrange your displays manually in the Windows Display Settings to your desired layout, and overwrite the broken profile (save a new one with the same name, then delete the old one). DispEx will capture and save the newly updated hardware paths.
A new gear button (⚙) in the bottom action bar launches the native Windows 11 display settings panel (ms-settings:display) with a single click. Useful for fine-tuning resolution, scaling, or refresh rate alongside DispEx profile management — without navigating through Settings manually.
Implementation: Windows.System.Launcher.LaunchUriAsync invoked via [RelayCommand] in ProfileListViewModel.
Profiles in the list can now be reordered by drag-and-drop. Simply grab a profile card and move it to the desired position — the new order is automatically persisted to the JSON storage file and survives application restarts.
Engineering details:
| Aspect | Implementation |
|---|---|
| Drag mechanism | WinUI 3 native ListView reorder: CanDragItems, CanReorderItems, AllowDrop |
| Persistence trigger | DragItemsCompleted event → SaveProfileOrderAsync() |
| Thread safety | Save is dispatched via DispatcherQueue.TryEnqueue with DispatcherQueuePriority.Low, ensuring it executes only after the ListView completes all internal RemoveAt/Insert mutations and layout passes. This eliminates race conditions between the UI reorder pipeline and the file I/O without blocking the UI thread |
| Atomicity | The full ordered list is written atomically through JsonProfileStorage.SaveAsync (temp-file → rename pattern) |
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | .NET 8, C# 12 | AOT-ready, Span<T>, source generators |
| UI | WinUI 3 (Windows App SDK 1.8) | DesktopAcrylic, compiled x:Bind |
| Interop | CsWin32 0.3.x | Type-safe P/Invoke for CCD API |
| MVVM | CommunityToolkit.Mvvm 8.4.2 | Source-generated ObservableObject |
| Serialization | System.Text.Json (source gen) | AOT-compatible, reflection-free |
| Hardware ID | System.Management (WMI) | WmiMonitorID serial number query |
| DI | Microsoft.Extensions.DependencyInjection | Service lifetime management |
- Windows 11 (build 22621 or later)
- .NET 8.0 SDK
- Windows App SDK 1.8 Runtime (download)
git clone https://github.com/YOUR_USERNAME/DispEx.git
cd DispEx
dotnet build -c Releasedotnet publish -c Release -r win-x64 --self-contained `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:WindowsAppSDKSelfContained=trueThe output will be in bin\Release\net8.0-windows10.0.22621.0\win-x64\publish\.
DispEx requires no installation. To run at logon with the privileges needed for global keyboard hooks:
# Create a Task Scheduler entry (elevated)
$action = New-ScheduledTaskAction -Execute "C:\Tools\DispEx\DispEx.exe"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME `
-RunLevel Highest -LogonType Interactive
Register-ScheduledTask -TaskName "DispEx" `
-Action $action -Trigger $trigger -Principal $principal `
-Description "Display Profile Manager — Win+P override"DispEx/
├── Infrastructure/
│ └── ProfileJsonContext.cs # Source-generated JSON serializer
├── Messages/
│ └── AppMessages.cs # Typed messenger messages
├── Models/
│ ├── MonitorHardware.cs # EDID + WMI hardware records
│ └── DisplayProfile.cs # Profile domain model
├── ViewModels/
│ ├── MainViewModel.cs # Root data context
│ ├── ProfileListViewModel.cs # CRUD + Apply + Reorder commands
│ ├── ProfileItemViewModel.cs # Profile list item
│ └── MonitorItemViewModel.cs # Display detail projection
├── Services/
│ ├── CcdDisplayManager.cs # CCD API wrapper (ArrayPool)
│ ├── WmiHardwareProvider.cs # WMI monitor identity
│ ├── ProfileApplyService.cs # Reverse matching engine
│ ├── JsonProfileStorage.cs # Atomic JSON persistence
│ └── KeyboardHookService.cs # WH_KEYBOARD_LL hook
├── App.xaml / App.xaml.cs # DI container, lifecycle
├── MainWindow.xaml / .cs # Flyout overlay, hook lifecycle
├── NativeMethods.txt # CsWin32 symbol manifest
├── app.manifest # DPI awareness, UAC
└── DispEx.csproj # Project configuration
This project is licensed under the MIT License — see the LICENSE file for details.