@@ -2218,6 +2218,323 @@ git push origin v1.10.1
22182218
22192219---
22202220
2221- * This session log now comprehensively documents the entire Windows Edge Light journey from initial concept through production release v1.10.1 with Azure code signing and multi-monitor hole punch support.*
2221+ ## Phase 14: Critical Bug Fix - Multi-Monitor DPI Scaling (v1.10.2) - December 3, 2025
2222+
2223+ ** Date** : December 3, 2025 (Evening)
2224+ ** Duration** : ~ 45 minutes
2225+ ** Branch** : ` fix/window-geometry-bug `
2226+
2227+ ### Bug Report
2228+
2229+ ** Issue** : Edge light appearing 1000+ pixels offset when using "Show on All Monitors" mode with mixed DPI displays
2230+
2231+ ** User Scenario** :
2232+ - Setup: 3 x 4K monitors (left, center primary, right) + 1 monitor below
2233+ - Normal operation: All monitors at 4K resolution with 150% DPI scaling
2234+ - Trigger: Changed rightmost monitor to 1080p for Teams presentations (100% DPI)
2235+ - Result: Edge light on "all monitors" mode appeared spanning across TWO monitors, offset significantly to the left
2236+
2237+ ** Visual Impact** :
2238+ ```
2239+ Expected: [Monitor 3] [Monitor 2 Primary] [Monitor 1]
2240+ [ Light ] [ Light ] [ Light ]
2241+
2242+ Actual: [Monitor 3] [Monitor 2 Primary] [Monitor 1]
2243+ [ Light ] [Li|ght Lig|ht] [ ]
2244+ ^^^^^ overlapping between monitors
2245+ ```
2246+
2247+ ### Root Cause Analysis
2248+
2249+ ** Problem Location** : ` CreateMonitorWindow() ` method (line 917-920)
2250+
2251+ ** Bad Code** :
2252+ ``` csharp
2253+ // Used primary monitor's DPI for ALL monitors
2254+ window .Left = workingArea .X / _dpiScaleX ; // ❌ Wrong!
2255+ window .Top = workingArea .Y / _dpiScaleY ; // ❌ Wrong!
2256+ window .Width = workingArea .Width / _dpiScaleX ;
2257+ window .Height = workingArea .Height / _dpiScaleY ;
2258+ ```
2259+
2260+ ** Why It Failed** :
2261+ - ` _dpiScaleX ` and ` _dpiScaleY ` are cached from the ** primary monitor** (Monitor 2)
2262+ - When Monitor 2 is 4K @ 150% DPI: ` _dpiScaleX = 1.5 `
2263+ - When Monitor 1 is 1080p @ 100% DPI: should use ` 1.0 `
2264+ - But code used ` 1.5 ` for Monitor 1, causing position calculation error
2265+
2266+ ** Math Behind the Bug** :
2267+ ```
2268+ Monitor 1 position: X=3840, Width=1920
2269+ Wrong calculation: 3840 / 1.5 = 2560 ❌ (appears at center monitor!)
2270+ Right calculation: 3840 / 1.0 = 3840 ✓ (appears at correct position)
2271+
2272+ Offset: 3840 - 2560 = 1280 pixels to the left!
2273+ ```
2274+
2275+ ### Investigation Process
2276+
2277+ ** Step 1** : Analyzed monitor topology with WMI and Windows Forms APIs
2278+ ``` powershell
2279+ Monitor 1: \\.\DISPLAY1 @ (3840,0) - 1920x1080 (100% DPI after change)
2280+ Monitor 2: \\.\DISPLAY2 @ (0,0) - 2560x1707 PRIMARY (150% DPI)
2281+ Monitor 3: \\.\DISPLAY3 @ (-3840,16) - 2560x1707 (150% DPI)
2282+ Monitor 4: \\.\DISPLAY4 @ (910,2560) - 1536x1024 (100% DPI)
2283+ ```
2284+
2285+ ** Step 2** : Identified the DPI calculation issue
2286+ - Primary monitor DPI cached in ` _dpiScaleX/_dpiScaleY `
2287+ - All additional monitor windows using same cached value
2288+ - No per-monitor DPI calculation before positioning
2289+
2290+ ** Step 3** : Found existing ` Loaded ` event handler that tried to fix it
2291+ - Lines 1030-1056: Recalculates DPI after window loads
2292+ - ** Problem** : Window already positioned incorrectly by that point
2293+ - Causes flashing and doesn't fully resolve mixed-DPI scenarios
2294+
2295+ ### Solution Implementation
2296+
2297+ ** Added Windows API Support** :
2298+ ``` csharp
2299+ [DllImport (" user32.dll" )]
2300+ private static extern IntPtr MonitorFromPoint (POINT pt , uint dwFlags );
2301+
2302+ [DllImport (" shcore.dll" )]
2303+ private static extern int GetDpiForMonitor (IntPtr hmonitor , int dpiType ,
2304+ out uint dpiX , out uint dpiY );
2305+
2306+ private const int MDT_EFFECTIVE_DPI = 0 ;
2307+ private const uint MONITOR_DEFAULTTONEAREST = 2 ;
2308+ ```
2309+
2310+ ** New Helper Method** :
2311+ ``` csharp
2312+ private (double dpiScaleX , double dpiScaleY ) GetDpiForScreen (Screen screen )
2313+ {
2314+ try
2315+ {
2316+ // Get monitor handle for the center of the screen
2317+ var centerPoint = new POINT
2318+ {
2319+ x = screen .Bounds .X + screen .Bounds .Width / 2 ,
2320+ y = screen .Bounds .Y + screen .Bounds .Height / 2
2321+ };
2322+
2323+ IntPtr hMonitor = MonitorFromPoint (centerPoint , MONITOR_DEFAULTTONEAREST );
2324+
2325+ if (hMonitor != IntPtr .Zero )
2326+ {
2327+ int result = GetDpiForMonitor (hMonitor , MDT_EFFECTIVE_DPI ,
2328+ out uint dpiX , out uint dpiY );
2329+ if (result == 0 ) // S_OK
2330+ {
2331+ // Convert from DPI to scale factor (96 DPI = 100% = 1.0)
2332+ return (dpiX / 96 . 0 , dpiY / 96 . 0 );
2333+ }
2334+ }
2335+ }
2336+ catch
2337+ {
2338+ // Fall through to default
2339+ }
2340+
2341+ // Fallback: return 1.0 (100% scaling)
2342+ return (1 . 0 , 1 . 0 );
2343+ }
2344+ ```
2345+
2346+ ** Updated CreateMonitorWindow()** :
2347+ ``` csharp
2348+ // OLD - Used wrong DPI
2349+ window .Left = workingArea .X / _dpiScaleX ;
2350+
2351+ // NEW - Uses correct per-monitor DPI
2352+ var (screenDpiX , screenDpiY ) = GetDpiForScreen (screen );
2353+ window .Left = workingArea .X / screenDpiX ;
2354+ window .Top = workingArea .Y / screenDpiY ;
2355+ window .Width = workingArea .Width / screenDpiX ;
2356+ window .Height = workingArea .Height / screenDpiY ;
2357+ ```
2358+
2359+ ** Optimized MonitorWindowContext Initialization** :
2360+ ``` csharp
2361+ // OLD - Always used primary monitor's DPI
2362+ DpiScaleX = _dpiScaleX ,
2363+ DpiScaleY = _dpiScaleY
2364+
2365+ // NEW - Uses calculated per-monitor DPI
2366+ DpiScaleX = screenDpiX ,
2367+ DpiScaleY = screenDpiY
2368+ ```
2369+
2370+ ** Improved Loaded Event Handler ** :
2371+ ```csharp
2372+ // Only reposition if DPI changed significantly from our initial calculation
2373+ if (Math .Abs (dpiX - ctx .DpiScaleX ) > 0 . 01 ||
2374+ Math .Abs (dpiY - ctx .DpiScaleY ) > 0 . 01 )
2375+ {
2376+ // Recalculate with WPF-reported DPI
2377+ ctx .DpiScaleX = dpiX ;
2378+ ctx .DpiScaleY = dpiY ;
2379+ // ... reposition window
2380+ }
2381+ ```
2382+
2383+ ** Bonus Fix ** : Removed duplicate POINT struct definition (was defined twice )
2384+
2385+ ### Changes Summary
2386+
2387+ ** Files Modified ** : 1 file
2388+ - `WindowsEdgeLight / MainWindow .xaml .cs `: + 72 lines , - 24 lines
2389+
2390+ ** Key Changes ** :
2391+ 1 . ✅ Added `MonitorFromPoint ` P / Invoke (user32 .dll )
2392+ 2 . ✅ Added `GetDpiForMonitor ` P / Invoke (shcore .dll )
2393+ 3 . ✅ Created `GetDpiForScreen ()` helper method
2394+ 4 . ✅ Updated `CreateMonitorWindow ()` to use per - monitor DPI
2395+ 5 . ✅ Optimized `MonitorWindowContext ` initialization
2396+ 6 . ✅ Improved `Loaded ` event to only reposition if needed
2397+ 7 . ✅ Removed duplicate POINT struct definition
2398+
2399+ **Lines of Code**: Net +48 lines
2400+
2401+ ### Testing & Verification
2402+
2403+ **Build Status **: ✅ Success
2404+ ```
2405+ Build succeeded with 4 warnings (pre - existing : WFO0003 , IL3000 )
2406+ 0 Errors
2407+ ```
2408+
2409+ **Runtime Testing **:
2410+ ```
2411+ Setup : 4 monitors with mixed DPI
2412+ - Monitor 1: 1920x1080 @ 100% (1080p presentation mode )
2413+ - Monitor 2: 2560x1707 @ 150% (4K primary )
2414+ - Monitor 3: 2560x1707 @ 150% (4K left )
2415+ - Monitor 4: 1536x1024 @ 100% (bottom )
2416+
2417+ Action : Enable "Show on All Monitors "
2418+ Result : ✅ All edge lights positioned correctly on their respective monitors
2419+ ✅ No offset or overlap between monitors
2420+ ✅ No flashing or repositioning after window loads
2421+ ```
2422+
2423+ **User Confirmation **: "that fixed it very nice job "
2424+
2425+ ### Technical Details
2426+
2427+ **DPI Calculation Accuracy **:
2428+ - Uses `MDT_EFFECTIVE_DPI ` - matches what Windows uses for scaling
2429+ - Returns actual DPI per monitor (96 , 120 , 144 , 192 , etc .)
2430+ - Converts to scale factor : `dpi / 96.0` (e .g ., 144 / 96 = 1.5 = 150%)
2431+
2432+ **Coordinate Transformation **:
2433+ ```
2434+ Physical pixels (from Screen .Bounds ) → WPF DIPs (Device Independent Pixels )
2435+ Formula : physicalPixels / dpiScale = DIPs
2436+
2437+ Example with Monitor 1:
2438+ Physical : X =3840, DPI =96 (1.0 scale )
2439+ WPF DIPs : 3840 / 1.0 = 3840
2440+
2441+ Example with Monitor 2:
2442+ Physical : X =0, DPI =144 (1.5 scale )
2443+ WPF DIPs : 0 / 1.5 = 0
2444+ ```
2445+
2446+ **Performance Impact **:
2447+ - Added one Win32 API call per monitor window creation
2448+ - `GetDpiForMonitor ` is fast (~ microseconds )
2449+ - Eliminates redundant repositioning in `Loaded ` event
2450+ - Net performance improvement due to less repositioning
2451+
2452+ ### Why This Bug Existed
2453+
2454+ **Historical Context **:
2455+ 1. Original code (v0 .6 ) only supported single monitor
2456+ 2. v1 .8-1.9 added multi -monitor support
2457+ 3. v1 .10.0 added "all monitors " mode
2458+ 4. Initial implementation assumed all monitors had same DPI
2459+ 5. Windows increasingly supports mixed -DPI setups (common with laptops + external displays )
2460+
2461+ **Common Misconception **:
2462+ - Developers often assume one DPI scale per system
2463+ - Reality : Windows supports **per -monitor DPI ** since Windows 8.1
2464+ - Each monitor can have different scaling independently
2465+
2466+ ### Impact Assessment
2467+
2468+ **Severity **: High
2469+ - Broke core "all monitors " feature for mixed -DPI setups
2470+ - Visual bug (edge lights in wrong position )
2471+ - Functional impact (lights overlapping , not clickable in right place )
2472+
2473+ **Scope **:
2474+ - Affects users with mixed DPI monitor setups
2475+ - Common scenario : Laptop @ 150% + External 1080p @ 100%
2476+ - Also affects : 4K + 1080p combinations , triple monitor setups
2477+
2478+ **Frequency **:
2479+ - Issue occurs whenever resolution /DPI changes while app running
2480+ - Common when switching monitor modes for presentations
2481+ - Reproducible 100% of the time with mixed DPI
2482+
2483+ ### Commit Details
2484+
2485+ **Branch **: `fix /window -geometry -bug `
2486+ **Commit **: `17b3b4d `
2487+ **Message **: "Fix multi -monitor geometry bug with per -monitor DPI scaling "
2488+
2489+ **Git Stats **:
2490+ ```
2491+ 1 file changed , 72 insertions (+ ), 24 deletions (- )
2492+ ```
2493+
2494+ ### Release Notes (v1.10.2)
2495+
2496+ **Version **: 1.10.2
2497+ **Release Date **: December 3, 2025
2498+ **Type **: Bug fix release
2499+
2500+ **Fixed **:
2501+ - 🐛 Multi -monitor positioning bug with mixed DPI displays
2502+ - Edge lights now correctly positioned on all monitors regardless of DPI differences
2503+ - Eliminated 1000+ pixel offset when switching monitor resolutions
2504+ - Removed window flashing during all -monitors mode activation
2505+
2506+ **Technical **:
2507+ - Added per -monitor DPI calculation using Windows GetDpiForMonitor API
2508+ - Optimized window positioning to calculate correct DPI before creating windows
2509+ - Reduced redundant repositioning in window Loaded events
2510+
2511+ **Compatibility **:
2512+ - No breaking changes
2513+ - Seamless upgrade from v1 .10.1
2514+ - All existing features maintained
2515+
2516+ ---
2517+
2518+ ## Final Status (December 3, 2025 - Evening)
2519+
2520+ **Current Version **: v1 .10.2 (Latest /Production )
2521+ **Status **: Stable , critical bug fixed
2522+ **Build **: Automated , signed , trusted
2523+ **Testing **: Verified on 4-monitor mixed -DPI setup
2524+ **User Impact **: Positive - "that fixed it very nice job "
2525+
2526+ **Project Maturity **: 🟢 Production /Stable
2527+
2528+ ---
2529+
2530+ **Total Project Timeline **: November 14 - December 3, 2025 (19 days )
2531+ **Total Releases **: 19 versions (v0 .1 through v1 .10 . 2 )
2532+ **Lines of Code **: ~3,750 (code + docs + tests )
2533+ **Community PRs **: 8 merged
2534+ **Status **: 🚀 Production Ready
2535+
2536+ ---
2537+
2538+ *This session log now comprehensively documents the entire Windows Edge Light journey from initial concept through production release v1 .10.2 with critical multi -monitor DPI bug fix .*
22222539
22232540
0 commit comments