Skip to content

fix: preserve DIP size on programmatic cross-DPI setBounds on Windows#51645

Open
gdavidkov wants to merge 1 commit into
electron:mainfrom
gdavidkov:gdavidkov/fix-cross-dpi-setbounds-double-rescale
Open

fix: preserve DIP size on programmatic cross-DPI setBounds on Windows#51645
gdavidkov wants to merge 1 commit into
electron:mainfrom
gdavidkov:gdavidkov/fix-cross-dpi-setbounds-double-rescale

Conversation

@gdavidkov
Copy link
Copy Markdown
Contributor

Description of Change

On Windows with multiple monitors at different DPI scaling, calling
BrowserWindow.setBounds() with coordinates on a different-DPI monitor
returns the wrong outer size. Requesting an 800×600 DIP window on a
1.375×/1.1× mixed-DPI setup currently returns ~734×550 DIP from getBounds().

Before fix — window visibly shrinks on the cross-DPI move:

cross-dpi-set-bounds-bug

After fix — outer matches the request within ±2 DIP (ScaleToEnclosingRect
rounding at non-integer DSFs):
cross-dpi-set-bounds-fix

Root cause

DesktopWindowTreeHostWin::SetBoundsInDIP converts the requested DIP rect to
pixels using the destination display's DSF, then ::SetWindowPos moves the
HWND. Windows fires WM_DPICHANGED synchronously during the move, and
HWNDMessageHandler::OnDpiChanged applies the OS-suggested lParam rect
verbatim — but Windows computed that rect as current_rect * new_dpi / old_dpi, so the already-destination-DPI-sized pixels we just sent get
rescaled by that ratio again, leaving the final outer off.

The mismatch is amplified by Windows 10+ text-scale: Chromium's
display::Display::device_scale_factor() folds the text-scale multiplier in
(1.25 × 1.10 = 1.375), but WM_DPICHANGED's wParam reports only the
display-DPI bucket (120, i.e. 1.25×). Any MulDiv-style cancellation
against Windows' DPI is therefore unsound.

Fix

Cache the destination-DPI pixel rect in SetBoundsInDIP just before
::SetWindowPos. OnDpiChanged substitutes the cached size for the
OS-suggested size; position still comes from lParam so the window lands
where Windows placed it. The cache is cleared right after
SetBoundsInPixels returns (WM_DPICHANGED is dispatched synchronously),
so it cannot leak into a later unrelated DPI change. No DPI math is
involved on purpose — passing through the already-correct pixel size
sidesteps the Chromium-DSF vs. Windows-DPI mismatch entirely.

See the patch commit message for the full analysis.

Checklist

Release Notes

Notes: Fixed an issue where BrowserWindow.setBounds() returned the wrong size when targeting a monitor with a different DPI scaling than the window's current monitor on Windows.

When BrowserWindow.setBounds() targets coordinates on a monitor with a
different display scale factor than the window's current monitor, the
final outer size ends up off by source/destination — e.g., a 400 DIP
width requested on a 1.375x/1.1x mixed-DPI setup gets reported back as
~366 DIP. Pre-fix on the dpi-fiddle harness, an 800 DIP outer requested
on the secondary returns 734 DIP; post-fix it returns 802 DIP (within
ScaleToEnclosingRect rounding at non-integer DSFs).

Adds a Chromium patch that caches the destination-DPI pixel rect in
DesktopWindowTreeHostWin::SetBoundsInDIP and substitutes its size in
HWNDMessageHandler::OnDpiChanged for the OS-suggested size, sidestepping
Windows' double-rescale of WM_DPICHANGED's suggested rect. See the patch
commit message for the full root-cause analysis.

Adds a regression spec under BrowserWindow.setBounds that exercises the
cross-DPI move when a secondary monitor at a different DSF is available
(skips otherwise).

Notes: Fixed an issue where BrowserWindow.setBounds() returned the wrong
size when targeting a monitor with a different DPI scaling than the
window's current monitor on Windows.
@gdavidkov gdavidkov requested a review from a team as a code owner May 15, 2026 15:11
@electron-cation electron-cation Bot added new-pr 🌱 PR opened recently labels May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-pr 🌱 PR opened recently

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant