QEMU on Wayland: mouse fixes and integer scaling

Why the guest mouse drifts or hits an invisible dead zone, why the display comes out blurry under fractional display scaling, and how to get pixel-perfect integer scaling.

Patches on GitHub Branch (QEMU master)

If you landed here from a search engine, one of these is probably your problem:

1. The GTK display cannot hold the pointer on Wayland

QEMU's GTK frontend emulates a relative-motion mouse: it grabs the pointer, takes the difference between consecutive absolute positions, and warps the pointer back to the middle when it approaches an edge. Two things go wrong on Wayland.

gdk_seat_grab() has no confine_to argument, so the pointer is never actually confined to the window. Under XWayland, X11 pointer grabs stop delivering motion once the pointer leaves the surface, so the guest cursor simply freezes — at a different place every time, because the motion is relative.

And the re-centering check in gd_motion_event() is written against the monitor geometry rather than the widget. With the guest in a window on a 3840x2160 screen it never fires at all. Instrumented here: 306 checks, 0 hits, 0 warps.

This is a known structural problem, not a small bug. The QEMU maintainer called the approach “clunky at best” and recommended using the low-level Wayland protocols instead; KDE filed the same symptom as bug 450818 and closed it as a compositor-side matter.

The fix is to stop using the GTK display. -display sdl uses SDL2's native pointer-constraints and relative-pointer support and behaves correctly on Wayland. No patch can reasonably rescue the GTK path here.

One real GTK bug is patched anyway: gd_grab_update() discards the GdkGrabStatus that gdk_seat_grab() returns, and gd_grab_pointer() sets ptr_owner unconditionally right after. A failed grab therefore leaves QEMU feeding relative motion to the guest while the host pointer roams freely over other windows, with nothing to indicate why the two cursors came apart.

2. The fraction of scaled relative motion is thrown away

Both ui/gtk.c and ui/sdl2.c scale relative motion from window coordinates into guest pixels and store the result in an int. When the window is larger than the guest — zoom-to-fit, or any manual enlargement — that factor is below one. A one-pixel movement of the mouse becomes zero, on every event.

So the guest pointer stands still while you move the mouse slowly, and falls short of the distance actually travelled when you move it fast. Truncation is toward zero, so left and up lose the fraction differently from right and down, and the pointer drifts. At 1360x768 in a 1876-wide window the factor is 0.72.

The two frontends are fixed differently. ui/gtk.c keeps the remainder and folds it into the next event. ui/sdl2.c stops scaling relative motion at all: a position is a point on the screen and has to be expressed in guest pixels, but relative motion is what the mouse reports and does not depend on the resolution the guest happens to be in. Dividing it by the window-to-guest ratio — which commit 30aa105640 started doing, in a patch whose subject and body are about positions — means the mouse has to travel N times as far at an Nx zoom.

That leaves the two frontends inconsistent, which is deliberate: ui/gtk.c derives relative motion from consecutive absolute positions, so removing the scaling there means keeping the delta in widget coordinates. That is a larger change and is not attempted here.

3. Fractional display scaling silently undoes your filter choice

QEMU's SDL window is created without SDL_WINDOW_ALLOW_HIGHDPI, so SDL only ever draws it at its logical size. On a compositor running at a fractional scale, the compositor then resamples that up to physical pixels using its own filter — which destroys whatever scaling mode QEMU chose.

On a display at 1.45x, measured against the guest framebuffer: the window content matches a bilinear upscale before the patch and a nearest one after it. Fraction of adjacent identical pixels: 88.9% before, 97.5% after.

Asking for the real pixel density has a consequence. The drawable and the window are no longer the same size, and the GL paths in ui/sdl2-gl.c set the viewport from the window size. Since GL counts from the bottom left, that draws the guest small in that corner. The patch takes the drawable size there instead.

4. The scaler option

The last patch adds scaler= to the SDL display:

qemu-system-ppc  -display sdl,scaler=integerplus  ...
qemu-system-i386 -display sdl,scaler=integer      ...
modebehaviour
linearthe existing bilinear stretch (default)
nearestnearest-neighbour; sharp, but some guest pixels end up wider than others
integerscale by the largest whole number that fits and letterbox the rest, so every guest pixel is the same size
integerplusscale by a whole number into a render target with no interpolation, then stretch that to fill the window

integer and integerplus tolerate the window falling up to two pixels short of a whole multiple. That is not arbitrary slack: a fractional-scale compositor rounds the surface to whole physical pixels, so a window sized to be exactly N times the guest comes back a pixel short — and taking the floor() there drops a whole step, turning 2x into 1x and half the window into border. Measured here, a 3x request of 2400x1800 became 1655x1241 logical and came back as 2400x1799.

The option is defined on DisplayOptions rather than DisplaySDL, so other display backends can implement it too.

The GL path does not implement the scaler. With gl=on the guest is stretched over the window with a plain bilinear filter regardless of scaler=. Use the default 2D path.

5. The PS/2 mouse ignores the sample rate the guest selected

A PS/2 mouse is sampled at a fixed rate, and the guest picks that rate with AUX_SET_SAMPLE. QEMU records the value and then ignores it: every host pointer event is turned into a packet and sent straight away.

Host pointing devices report far faster than any PS/2 mouse. 1000Hz is common on a gaming mouse, and on one here 84% of the events carry a total movement of two counts or less. A guest that only looks at the deltas does not care, because the counts still add up. A guest that derives the pointer speed from the counts in a single packet does. Windows' "enhance pointer precision" is exactly that: a guest that asked for 100 samples a second and instead gets 1000 packets of one count each reads that as the slowest possible movement, and keeps the pointer at the bottom of its acceleration curve. The pointer crawls, and the lower the guest resolution the worse it looks.

The fix accumulates the motion and releases it at the interval the guest asked for, which is what the hardware would have done. Button changes are not delayed, a sample rate of zero — nothing asked for yet — still passes everything through, and remote (polled) mode is untouched.

Measured on a Windows XP guest: packets drop from around 1000/s to a 96/s ceiling, and the mean counts per packet go from about 1 to 7.55. Mac OS 9 is unaffected, because ADB is polled by the guest and QEMU already accumulates motion between polls.

The patches

Six commits, each building on its own and checkpatch-clean. Two series, the same changes against two bases:

git am /path/to/patches/qemu-master/00*.patch
ui/gtk: keep the fraction when scaling relative motion
ui/gtk: do not take ownership of the pointer if the grab failed
ui/sdl: render at the output's pixel density
ui/sdl: do not scale relative pointer motion
ui: add a scaler option for the SDL display
hw/input/ps2: send packets at the sample rate the guest asked for

The last one is independent of the display work and touches hw/input rather than ui/. It is kept in the same series only because it came out of chasing the same "the mouse feels wrong" report.

Tested on

Ubuntu 26.04, KDE Plasma on Wayland at 1.45 display scale, SDL 2.32.10, with a mac99 Mac OS 9.2.2 guest and q35 Windows XP and Windows 98 SE guests.

Not submitted upstream. Offered as-is for anyone hitting the same problems. The patches are against QEMU and carry QEMU's licence (GPLv2).