Skip to content

Commit 3f1793e

Browse files
committed
Merge fix/window-geometry-bug - v1.10.2 release
Fixed critical multi-monitor DPI scaling bug causing 1000+ pixel offsets. Added per-monitor DPI calculation using GetDpiForMonitor API. Changes: - Added GetDpiForScreen() helper method - Updated CreateMonitorWindow() to use per-monitor DPI - Optimized window positioning logic - Documented in Phase 14 of session log Verified on 4-monitor mixed-DPI setup (4K@150% + 1080p@100%)
2 parents 2643208 + e117625 commit 3f1793e

2 files changed

Lines changed: 390 additions & 25 deletions

File tree

SESSION_LOG.md

Lines changed: 318 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)