bevyengine / bevyengine/bevy

MouseWheel PixelDelta is in physical pixels while CursorMoved is logical

Open Beginner friendly
#25,304 0 comments 0 reactions 0 assignees View on GitHub
A-Input C-Bug S-Needs-Investigation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version

`bevy_winit` 0.18.1. Also verified unchanged in 0.19.0 and on `main` (7b2b909).

## Relevant system information

macOS 15 (Darwin 25.5.0), a 2× Retina display and a 1× display attached to the same machine.

## What you did

Read `MouseWheel` messages and used `event.x` / `event.y` alongside `CursorMoved` positions, on a window that can be dragged between a 1× and a 2× display.

## What went wrong

`MouseWheel` carrying `MouseScrollUnit::Pixel` is reported in **physical** pixels, while every neighboring pointer quantity is converted to **logical** pixels. The two therefore disagree by the window's scale factor on any HiDPI display.

The three arms sit next to each other in `crates/bevy_winit/src/state.rs`, and the middle one is the odd one out:

- [`CursorMoved` (L294)](https://github.com/bevyengine/bevy/blob/7b2b90997ae2e73558ace09cc065d31a2c24b266/crates/bevy_winit/src/state.rs#L294) — divides by `scale_factor()`
- [`MouseWheel` / `PixelDelta` (L342)](https://github.com/bevyengine/bevy/blob/7b2b90997ae2e73558ace09cc065d31a2c24b266/crates/bevy_winit/src/state.rs#L342) — **`p.x as f32`, no conversion**
- [`Touch` (L356)](https://github.com/bevyengine/bevy/blob/7b2b90997ae2e73558ace09cc065d31a2c24b266/crates/bevy_winit/src/state.rs#L356) — calls `.to_logical(scale_factor())`

Measured: one synthesized pixel-unit scroll gesture of the same magnitude reports

```
1× display: MouseWheel { unit: Pixel, x: 0.0, y: 4.0 }
2× display: MouseWheel { unit: Pixel, x: 0.0, y: 8.0 }
```

so the same physical gesture scrolls twice as far on the Retina panel, and moving one window between the two monitors changes scroll speed with no other input.

Two consequences we hit downstream:

1. **Anything mixing wheel deltas with pointer positions is in mismatched units.** `bevy_egui`, for instance, divides the pointer position by the context scale factor but forwards the wheel delta untouched, so on HiDPI egui receives a delta that is `pixels_per_point` times too large for the coordinate space its pointer lives in.
2. **The inflated delta crosses magnitude-based heuristics.** egui classifies a wheel event as smooth-trackpad input via `delta.length() < 8.0`; the doubled delta falls on the far side of that threshold, so a HiDPI display silently gets the coarse accumulate-and-decay scroll path where a 1× display gets direct smooth scrolling. Any consumer with a similar magnitude test inherits the same split.

## Additional information

This looks distinct from the existing scroll issues, which are all about the *ratio between the two units* rather than about physical-vs-logical:

- #5823 — Line vs Pixel magnitudes vary by platform
- #21140 — wasm scroll too fast, Pixel vs Line confusion
- #24508 — `MouseScrollUnit` handling is unprincipled (closed)

Those remain true whatever the scale factor is; this one is specifically that a `Pixel` delta is not in the same space as `CursorMoved`.

Presumably the fix is to make the `PixelDelta` arm match its two neighbors:

```rust
event::MouseScrollDelta::PixelDelta(p) => {
let p = p.to_logical::(win.resolution.scale_factor() as f64);
self.bevy_window_events.send(MouseWheel {
unit: MouseScrollUnit::Pixel,
x: p.x,
y: p.y,
window,
phase,
});
}
```

That is a behavior change for anyone who has already compensated by hand, so it may want a migration note. Happy to open a PR if the approach looks right.

Contributor guide

Open the contributing guide

Research direction

Start in crates/bevy_winit/src/state.rs at the MouseWheel PixelDelta arm around line 342, and compare it with the CursorMoved and Touch conversions nearby. Verify the reported delta uses the window's scale factor consistently with pointer coordinates, then validate behavior when moving a window between 1× and 2× displays and consider whether a migration note is needed.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
game-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.