Skip to content

fix: set_outer_position uses target monitor's scale factor#4505

Open
natepiano wants to merge 6 commits intorust-windowing:masterfrom
natepiano:fix/set-outer-position-scale-factor-v0.31
Open

fix: set_outer_position uses target monitor's scale factor#4505
natepiano wants to merge 6 commits intorust-windowing:masterfrom
natepiano:fix/set-outer-position-scale-factor-v0.31

Conversation

@natepiano
Copy link

Summary

set_outer_position converts between physical and logical coordinates using the window's current monitor's scale factor. When the window is on one monitor and the requested position is on a different monitor with a different scale factor, the conversion uses the wrong scale factor and the window lands at the wrong position.

For example: a window is on a 1x monitor. The caller passes physical coordinates targeting a position on a 2x monitor. On macOS, the Physical → Logical conversion divides by 1.0 (the current monitor's scale) instead of 2.0 (the target monitor's scale), producing logical coordinates that are 2x too large. The window overshoots dramatically.

The platform determines which coordinate type triggers the bug:

  • macOS converts Physical → Logical, so the bug manifests with Physical input
  • Windows converts Logical → Physical, so the bug manifests with Logical input

I did not apply this fix to the X11 backend. X11 typically reports the same scale factor for all monitors (via the global Xft.dpi setting), so the bug does not manifest in practice.

Approach

I'm not deeply familiar with winit's internal patterns, so I tried to keep the API surface minimal and make the logic easy to review:

  • resolve_scale_factor() in winit-core/src/monitor.rs is a pure function that determines which monitor contains a given position and returns its scale factor. It has 16 unit tests covering physical/logical inputs, single/multi-monitor layouts, and edge cases.
  • MonitorBounds is a simple struct that resolve_scale_factor operates on, decoupled from platform-specific monitor types.
  • Each platform crate (macOS, Windows) adds a small scale_factor_for() helper that collects monitor bounds and calls resolve_scale_factor, falling back to the current window's scale factor if no monitor matches — preserving existing behavior.
  • The one-line change in each platform's set_outer_position: self.scale_factor()self.scale_factor_for(&position).

Example

cross_monitor_position demonstrates and verifies the fix. It moves a window between monitors with different scale factors and compares the target position against the actual position. With this PR applied, the example passes on both macOS and Windows. The example's doc comment includes instructions for reverting the one-line fix to reproduce the bug.

Testing

Tested on macOS (1x + 2x) and Windows (1x + 1.5x).

Fixes #4440

set_outer_position converts between physical and logical coordinates
using the window's current scale factor. When the target position is
on a different monitor with a different scale factor, this produces
incorrect coordinates and the window lands at the wrong position.

Add resolve_scale_factor() to winit-core that determines which monitor
a position targets, and use it in the macOS, Windows, and X11
implementations of set_outer_position.

Add cross_monitor_position example to demonstrate and verify the fix.
- Added documentation section explaining how to force X11 backend on Wayland systems
- Includes shell command to unset WAYLAND_DISPLAY environment variable for testing
- Improves usability of the example on Linux systems with Wayland as default session

Why: Users on modern Linux distributions may have Wayland set as the default display
server, which can affect monitor positioning behavior. Explicit instructions help users
reproduce consistent test results across different window manager configurations.

Impact: Makes the example more accessible and provides clear guidance for cross-platform
testing on Linux systems.

Also includes minor formatting improvements:
- Reformatted long lines in function signatures for consistency
- Adjusted print statement formatting for better readability
X11 does not support per-monitor scale factors, so the
scale_factor_for fix is a no-op on this platform.
- X11 does not expose per-monitor scale factors, so the cross-monitor
  scale factor bug does not apply — remove all X11 mentions from the
  example docs and runtime output
- Remove the Wayland-override instructions for forcing the X11 backend
- Remove X11 row from the "reproducing the bug" platform table
- Includes minor formatting changes in monitor.rs test helpers from
  stable cargo fmt (no logic changes)
github-merge-queue bot pushed a commit to bevyengine/bevy that referenced this pull request Mar 9, 2026
## Summary

When a bevy app starts, winit creates the window on a default monitor.
If the app then sets the window's position to a location on a different
monitor with a different scale factor, both position and size may be
applied in the same pass through `changed_windows()`.

Currently, size is processed first (via `request_inner_size`), then
position (via `set_outer_position`). Since the window hasn't moved yet
when `request_inner_size` runs, it uses the default monitor's scale
factor for the size calculation — not the scale factor of the monitor
the window is about to move to. This produces the wrong size when the
two monitors have different scale factors.

This PR moves the position block above the size block so the window is
on the target monitor before `request_inner_size` runs.

## Context

This change is a **no-op with current released versions of winit**.
Today, winit's `set_outer_position` has a bug where it uses the window's
current monitor's scale factor instead of the target monitor's
([rust-windowing/winit#4505](rust-windowing/winit#4505)),
so the window doesn't reliably land at the correct position regardless
of ordering.

Once winit merges that fix and bevy takes a dependency on a version that
includes it, the ordering will matter: `set_outer_position` will
correctly move the window to the target monitor, and
`request_inner_size` needs to run *after* that move so it sees the
correct `scale_factor()`.

The change is safe and backwards-compatible — it just reorders two
independent blocks within the same function.

## Testing

Tested on macOS (1x + 2x) and Windows (1x + 1.5x) with the patched
winit. Window appears at the correct position and size on the target
monitor.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

set_outer_position and request_inner_size use current monitor's scale factor instead of target monitor's on macOS

1 participant