Skip to content

AlexG-4W/DispEx

Repository files navigation

DispEx

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.

.NET 8 WinUI 3 C# 12 License: MIT Platform Version


The Problem

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.

The Solution

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.

Stable Hardware Identification

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.

scr1

Profile Capture & Restore

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.

Architecture

┌──────────────────────────────────────────────────────┐
│                    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)                  │
└──────────────────────────────────────────────────────┘

Key Features

Global Win+P Interception

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.

Hardware-Accelerated UI

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.

Zero-Allocation Interop

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.

Source-Generated Serialization

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.

MVVM with Source Generators

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.

⚠️ Troubleshooting: Profile Application Issues

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.

What's New in v1.2

⚙ Quick Access to Windows Display Settings

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.

↕ Drag & Drop Profile Reordering

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)

Technology Stack

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

Build & Deployment

Prerequisites

  • Windows 11 (build 22621 or later)
  • .NET 8.0 SDK
  • Windows App SDK 1.8 Runtime (download)

Build

git clone https://github.com/YOUR_USERNAME/DispEx.git
cd DispEx
dotnet build -c Release

Publish (self-contained, single file)

dotnet publish -c Release -r win-x64 --self-contained `
    -p:PublishSingleFile=true `
    -p:IncludeNativeLibrariesForSelfExtract=true `
    -p:WindowsAppSDKSelfContained=true

The output will be in bin\Release\net8.0-windows10.0.22621.0\win-x64\publish\.

Autostart with Elevated Privileges

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"

Project Structure

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

License

This project is licensed under the MIT License — see the LICENSE file for details.

About

Driver-level display profile manager for Windows 11

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages