There is no ([not yet?](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/33...)) dedicated protocol for requesting pointer warps, but it's possible to request one with `zwp_locked_pointer_v1.set_cursor_position_hint` which may be performed when the pointer is unlocked. The idea is to quickly lock/set position/unlock. This is used by SDL (in some cases: [SDL_MOUSE_EMULATE_WARP_WITH_RELATIVE](https://wiki.libsdl.org/SDL3/SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE)) and Xwayland. The limitation is/will be that the compositor will ignore requests to warp the pointer outside of surface bounds.
What I'd like to have working is the auto cursor placement feature of some applications, where they just want to call SetCursorPos towards specific UI elements inside their single window, and usually they'll be unclipped and have a visible cursor while doing this.
This patch also happens to allow mouselook to work in applications which use SetCursorPos for mouselook and aren't getting covered by the heuristics that turn on Wayland relative motion (for some reason the driver thinks the cursor is visible). Seems to affect Half-Life (GoldSrc) on my system. Though force enabling relative motion with an environment variable in a custom build or somehow improving the heuristics will also fix that.
~~A suface/pointer can only have one lock or confinement, and this implementation depends on the pointer lock. So if needed, I temporarily unlock/unconfine in order to do the warp then relock/reconfine. I modified `wayland_pointer_update_constraint` so I don't have to temporarily disable relative motion during this in case it's enabled. I think it's safe/a no-op to attempt pointer warps while using relative motion, as according to the protocol the warp will not generate a relative motion event, and while there will be a wl_pointer motion event, they're ignored while relative motion is being used.~~
-- v5: winewayland: Implement SetCursorPos via pointer lock.
From: Attila Fidan dev@print0.net
--- dlls/winewayland.drv/wayland_pointer.c | 35 +++++++++++++++++++++++--- dlls/winewayland.drv/waylanddrv.h | 2 ++ dlls/winewayland.drv/waylanddrv_main.c | 1 + 3 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/dlls/winewayland.drv/wayland_pointer.c b/dlls/winewayland.drv/wayland_pointer.c index 52aaa337aac..e8e31252024 100644 --- a/dlls/winewayland.drv/wayland_pointer.c +++ b/dlls/winewayland.drv/wayland_pointer.c @@ -758,9 +758,12 @@ static void wayland_pointer_update_constraint(struct wl_surface *wl_surface, return; }
- needs_lock = wl_surface && (confine_rect || covers_vscreen) && - !pointer->cursor.wl_surface; - needs_confine = wl_surface && confine_rect && pointer->cursor.wl_surface; + needs_lock = wl_surface && (((confine_rect || covers_vscreen) && + !pointer->cursor.wl_surface) || pointer->pending_warp); + needs_confine = wl_surface && confine_rect && pointer->cursor.wl_surface && + !pointer->pending_warp; + + pointer->pending_warp = FALSE;
if (!needs_confine && pointer->zwp_confined_pointer_v1) { @@ -879,6 +882,22 @@ void WAYLAND_SetCursor(HWND hwnd, HCURSOR hcursor) wayland_set_cursor(hwnd, hcursor, TRUE); }
+/*********************************************************************** + * WAYLAND_SetCursorPos + */ +BOOL WAYLAND_SetCursorPos(INT x, INT y) +{ + struct wayland_pointer *pointer = &process_wayland.pointer; + TRACE("warping to %d,%d\n", x, y); + + pthread_mutex_lock(&pointer->mutex); + pointer->pending_warp = TRUE; + pthread_mutex_unlock(&pointer->mutex); + + reapply_cursor_clipping(); + return TRUE; +} + /*********************************************************************** * WAYLAND_ClipCursor */ @@ -913,6 +932,16 @@ BOOL WAYLAND_ClipCursor(const RECT *clip, BOOL reset) wayland_win_data_release(data);
pthread_mutex_lock(&pointer->mutex); + if (wl_surface && pointer->pending_warp) + { + /* If a warp is pending, ensure the pointer is locked at least + * temporarily before updating the position hint. It'll be unlocked + * after setting the position hint if it wasn't locked previously. */ + wayland_pointer_update_constraint(wl_surface, + clip ? &confine_rect : NULL, + covers_vscreen); + } + if (wl_surface && hwnd == pointer->constraint_hwnd && pointer->zwp_locked_pointer_v1) { zwp_locked_pointer_v1_set_cursor_position_hint( diff --git a/dlls/winewayland.drv/waylanddrv.h b/dlls/winewayland.drv/waylanddrv.h index 5c5ce5bf130..43641976d93 100644 --- a/dlls/winewayland.drv/waylanddrv.h +++ b/dlls/winewayland.drv/waylanddrv.h @@ -106,6 +106,7 @@ struct wayland_pointer struct zwp_relative_pointer_v1 *zwp_relative_pointer_v1; HWND focused_hwnd; HWND constraint_hwnd; + BOOL pending_warp; uint32_t enter_serial; uint32_t button_serial; struct wayland_cursor cursor; @@ -396,6 +397,7 @@ LRESULT WAYLAND_DesktopWindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); void WAYLAND_DestroyWindow(HWND hwnd); BOOL WAYLAND_SetIMECompositionRect(HWND hwnd, RECT rect); void WAYLAND_SetCursor(HWND hwnd, HCURSOR hcursor); +BOOL WAYLAND_SetCursorPos(INT x, INT y); void WAYLAND_SetWindowText(HWND hwnd, LPCWSTR text); LRESULT WAYLAND_SysCommand(HWND hwnd, WPARAM wparam, LPARAM lparam, const POINT *pos); UINT WAYLAND_UpdateDisplayDevices(const struct gdi_device_manager *device_manager, void *param); diff --git a/dlls/winewayland.drv/waylanddrv_main.c b/dlls/winewayland.drv/waylanddrv_main.c index 633b2f4a043..220d5e51863 100644 --- a/dlls/winewayland.drv/waylanddrv_main.c +++ b/dlls/winewayland.drv/waylanddrv_main.c @@ -42,6 +42,7 @@ static const struct user_driver_funcs waylanddrv_funcs = .pKbdLayerDescriptor = WAYLAND_KbdLayerDescriptor, .pReleaseKbdTables = WAYLAND_ReleaseKbdTables, .pSetCursor = WAYLAND_SetCursor, + .pSetCursorPos = WAYLAND_SetCursorPos, .pSetWindowText = WAYLAND_SetWindowText, .pSysCommand = WAYLAND_SysCommand, .pUpdateDisplayDevices = WAYLAND_UpdateDisplayDevices,
I also patched xwayland on my machine to allow it when visible for winex11, since I think it's a nice feature.
I think going against the intended use by the protocol in this particular case is ok, and SDL also does this for `WarpMouse*`, except it may instead treat it as a heuristic to enable relative motion when the cursor is hidden. Maybe that's also something that would be useful for winewayland? From a quick glance I think [this](https://gitlab.collabora.com/alf/wine/-/commit/234bec66c3248a2e3a25495361385...) from the experimental branch does a similar thing. Though I would like actual warping for visible cursors if possible (unless there are ideas to enable locked pointer/relative motion all the time and render the cursor client-side, which was mentioned in XDC 2023, but I assume there is no future plans to switch the driver to this model, and we will be sticking to heuristics), for the auto cursor placement feature of some applications.
But I don't mind if we should wait for a dedicated protocol to possibly come along before continuing.
Alexandros Frantzis (@afrantzis) commented about dlls/winewayland.drv/wayland_pointer.c:
wayland_win_data_release(data); pthread_mutex_lock(&pointer->mutex);
- if (wl_surface && pointer->pending_warp)
- {
/* If a warp is pending, ensure the pointer is locked at least
* temporarily before updating the position hint. It'll be unlocked
* after setting the position hint if it wasn't locked previously. */
wayland_pointer_update_constraint(wl_surface,
clip ? &confine_rect : NULL,
covers_vscreen);
Since the warp emulation relies on multiple calls on this function to work, I think it's clearer if we implemented the logic completely externally. Perhaps something along the lines of:
``` static void wayland_pointer_update_constraint(struct wl_surface *wl_surface, RECT *confine_rect, BOOL covers_vscreen, BOOL force_lock) ...
if (wl_surface && pointer->pending_warp) { wayland_pointer_update_constraint(wl_surface, NULL, FALSE, TRUE); pointer->pending_warp = FALSE; } ```
As a bonus `wayland_pointer_update_constraint` function remains idempotent (for the same arguments) in terms of the final constraint state.
Alexandros Frantzis (@afrantzis) commented about dlls/winewayland.drv/wayland_pointer.c:
wayland_set_cursor(hwnd, hcursor, TRUE);
}
+/***********************************************************************
WAYLAND_SetCursorPos
- */
+BOOL WAYLAND_SetCursorPos(INT x, INT y) +{
- struct wayland_pointer *pointer = &process_wayland.pointer;
- TRACE("warping to %d,%d\n", x, y);
- pthread_mutex_lock(&pointer->mutex);
- pointer->pending_warp = TRUE;
- pthread_mutex_unlock(&pointer->mutex);
- reapply_cursor_clipping();
I think it should be fine to skip warp emulation completely if relative motion is already enabled, since the emulation won't have any effect anyway.
This avoids the overhead of performing the cursor clipping (see https://gitlab.winehq.org/wine/wine/-/merge_requests/7352#note_95733) when our lock/relative-motion heuristics have kicked in.
On Fri Feb 28 12:09:09 2025 +0000, Attila Fidan wrote:
I also patched xwayland on my machine to allow it when visible for winex11, since I think it's a nice feature. I think going against the intended use by the protocol in this particular case is ok, and SDL also does this for `WarpMouse*`, except it may instead treat it as a heuristic to enable relative motion when the cursor is hidden. Maybe that's also something that would be useful for winewayland? From a quick glance I think [this](https://gitlab.collabora.com/alf/wine/-/commit/234bec66c3248a2e3a25495361385...) from the experimental branch does a similar thing. Though I would like actual warping for visible cursors if possible (unless there are ideas to enable locked pointer/relative motion all the time and render the cursor client-side, which was mentioned in XDC 2023, but I assume there is no future plans to switch the driver to this model, and we will be sticking to heuristics), for the auto cursor placement feature of some applications. But I don't mind if we should wait for a dedicated protocol to possibly come along before continuing.
Although performing the lock/unlock dance to warp the pointer is indeed somewhat of a hack, I agree that there is enough prior-art here to make acceptable. The main caveat is that this is a best-effort approach, since the compositor is not guaranteed to honor the lock immediately. However it seems to work well enough in practice, so I am fine with this, at least until we get a proper warp protocol.
We could also feed `SetCursorPos` into our heuristics if needed, but let's see how far we can get with just warping, before we add more heuristic complexity.
I'm testing patches for https://bugs.winehq.org/show_bug.cgi?id=57430. In short: when running certain MMOs in windowed winewayland mode the camera control (which is a temporary mouselook with the cursor disappearing) shows erratic movements depending on where it was started in the window.
This PR sounded like it could mitigate the issue. Testing wine 10.2 + this PR !7353 the issue is unfortunately still reproducible. screen recordings of the issue:
wine 10.2 + !7353 in WoW WotLK windowed mode: https://streamable.com/5pt0j9 wine 10.2 + !7353 in Guild Wars 2 windowed mode: https://streamable.com/vwnp0q
while in windowed mode, where the erratic mouselook is seen, the trace is showing: `0128:trace:waylanddrv:pointer_handle_motion_internal hwnd=0x3006a wayland_xy=1272.34,198.56 screen_xy=2468,812`
while in fullscreen mode, where mouselook is working as expected, the trace is showing: `0128:trace:waylanddrv:relative_pointer_v1_relative_motion hwnd=0x3006a wayland_dxdy=1.00,0.00 screen_dxdy=1,0`
Excuse me if this PR is completely unrelated.
On Sun Mar 2 11:33:36 2025 +0000, Mershl wrote:
~scratch my initial report, applying a patch correctly is hard~ I'm testing patches for https://bugs.winehq.org/show_bug.cgi?id=57430. For WoW Wrath of the Lich King: This PR fixes the issue of mouselook which lead to erratic camera movements while in winewayland windowed mode. The window of WoW WotLK is not moveable in winewayland windowed mode when grabbing it at the title bar, but that's a separate issue. For Guild Wars 2: The mouselook issue in Guild Wars 2 while in winewayland windowed mode is not affected by this PR. Screencast of the issue: https://streamable.com/vwnp0q The WoW issue might also have been fixed by !7352, which I only tested now.
I replied about GW2 in bug 57430.