-
Notifications
You must be signed in to change notification settings - Fork 0
VcXsrv SDL2 Relative Mouse Mode Fix
Objective: Fix VcXsrv's missing support for SDL2 relative mouse mode, covering both the data layer (
XI_RawMotionevents) and the window management layer (cursor confinement and hiding), so that SDL2 programs can achieve a complete capture/release experience withoutSDL_MOUSE_RELATIVE_MODE_WARP=1.Date: 2026-07-02
Upstream:https://github.com/marchaesen/vcxsrv
Issue: #77 | PR: #78
Dev Branch:https://github.com/vivimillin/vcxsrv/tree/feature/raw-input-mouse
When running SDL2 programs (AssaultCube, etc.) under VcXsrv + WSL with relative mouse mode enabled:
| Configuration | Mouse Behavior | Audio Behavior |
|---|---|---|
SDL_MOUSE_RELATIVE_MODE_WARP=0 (default) |
Cursor points to sky/ground or locks to corners, uncontrollable | Normal |
SDL_MOUSE_RELATIVE_MODE_WARP=1 (manual workaround) |
Mouse locks correctly, free aim control | Severe stutter during high-frequency movement |
When SDL2's X11 backend enables relative mouse mode, it prefers XI_RawMotion events from the XInput2 extension for relative displacement:
SDL_SetRelativeMouseMode(SDL_TRUE)
→ XIGrabDevice() // Grab input device
→ XISelectEvents(XI_RawMotion) // Register relative motion event listener
// On mouse movement
X server → XI_RawMotion(dx, dy) // Hardware-level relative displacement
→ SDL2 extracts event.motion.xrel / yrel
VcXsrv's XInput2 extension is fully present at the protocol layer (XIGrabDevice, XISelectEvents both succeed), and X11_SetRelativeMouseMode returns success;
But the Windows input layer (hw/xwin) never passes mouse relative displacement to the X server. XI_RawMotion events are completely missing at the data layer.
| Layer | VcXsrv Status | Note |
|---|---|---|
| XInput2 protocol extension | Fully supported |
libXi fully compiled, XIGrabDevice and XISelectEvents work normally |
| XI_RawMotion event generation | Completely missing |
hw/xwin layer only handles WM_MOUSEMOVE (absolute coordinates), never passes mouse relative displacement to X server |
SDL2's auto-fallback mechanism only triggers when SetRelativeMouseMode returns failure. Here the function returns success (it only checks whether XInput2 is initialized), so the auto-fallback is not triggered. SDL2 mistakenly believes relative mode is available, but in reality receives no relative displacement data at all.
GetPointerEvents() generates RawDeviceEvent (i.e. XI_RawMotion) for all MotionNotify events unless the POINTER_NORAW flag is set.
winEnqueueMotion() uses POINTER_ABSOLUTE | POINTER_SCREEN, without setting POINTER_NORAW. This causes absolute coordinates from WM_MOUSEMOVE (e.g. x=500, y=300) to also generate XI_RawMotion(500, 300), mixing with true relative increments and producing the "gun pointing at sky/ground" chaos in SDL2.
SDL2 queries the valuator mode of the master device (Virtual core pointer, device 2), not the slave device (Windows pointer, device 6).
InitPointerDeviceStruct sets the slave to Relative, but the master remains at the default Absolute. In Absolute mode, SDL2 calculates deltas using prev - current, but current is a relative increment rather than an absolute position, producing completely wrong values.
When SDL_MOUSE_RELATIVE_MODE_WARP=1 is set manually, SDL2 bypasses the XI2 path and uses the legacy XWarpPointer to simulate relative mode:
Mouse moves → absolute coordinates arrive at SDL2
→ SDL2 calculates delta
→ XWarpPointer(center) resets cursor
→ triggers new MotionNotify
→ one X11 round-trip per movement
Under WSL2's VM NAT networking, each round-trip takes 2-5ms. High-frequency mouse operations generate hundreds of warp calls per second, blocking the main thread and causing audio underrun.
After the Raw Input data layer is fixed, SDL2 relative mouse mode can already receive correct XI_RawMotion data. But VcXsrv's DDX layer (hw/xwin) has no Windows-side logic to respond to X11 pointer grab events: XGrabPointer is handled at the DIX layer, but Windows ClipCursor and ShowCursor are never called. This causes:
| Symptom | Description |
|---|---|
| Cursor not confined | Mouse can still move out of window during capture |
| Cursor not hidden | System cursor remains visible during capture |
| Accidental resize trigger | Cursor at window edge drags sizing border and changes window size |
| Cursor skipping escape | Rapid mouse movement briefly escapes the confined area (Windows silently cancels ClipCursor) |
Use the Windows Raw Input API to obtain hardware-level relative displacement and feed it into the X server's relative event pipeline; simultaneously hook grab callbacks to implement cursor confinement and hiding.
Mouse moves
→ Windows generates WM_INPUT message
→ GetRawInputData() extracts lLastX, lLastY (relative displacement)
→ winEnqueueRawMotion(dx, dy)
→ QueuePointerEvents(..., POINTER_RELATIVE)
→ X server generates XI_RawMotion
→ SDL2 receives (dx, dy) correctly
→ No XWarpPointer needed, no message storm
Files to modify:
| File | Changes |
|---|---|
xorg-server/hw/xwin/win.h |
Add winEnqueueRawMotion() declaration |
xorg-server/hw/xwin/winmouse.c |
Add winEnqueueRawMotion() implementation; define POINTER_NORAW; fix master valuator mode |
xorg-server/hw/xwin/winwndproc.c |
Register Raw Input device + handle WM_INPUT messages |
Four coordinated changes:
-
Receive: Register Windows
WM_INPUT(Raw Input), capture hardware-level relative displacementlLastX/lLastY -
Inject: Add
winEnqueueRawMotion(), feed relative increments intoQueuePointerEvents(..., POINTER_RELATIVE) -
Pollution prevention: Add
POINTER_NORAWtowinEnqueueMotion(), prevent absolute events from generatingXI_RawMotion -
Semantic fix: Set master pointer valuator mode to
Relative, so SDL2 correctly parses raw values
VcXsrv never implemented cursor confinement and hiding on grab. The fix:
-
Hook master/slave device grab callbacks: Replace
ActivateGrab/DeactivateGrab, triggerClipCursorandShowCursoron confining grab -
Respect
confineTo: OnlyClipCursorandHideCursorwhengrab->confineTois non-NULL -
Map X11 window to Windows hwnd: Use
winGetWindowPriv()via grab'sconfineTowindow to get preciseHWND, avoidingGetForegroundWindow()startup race - 1px padding: Shrink clipping rectangle by 1px, preventing cursor from landing on sizing border
-
10ms timer: Re-apply
ClipCursor, defending against Windows silently canceling it (cursor skipping, Win key, etc.) -
Handle
WM_ACTIVATEAPP: Temporarily releaseClipCursoron app deactivation (Alt+Tab), restore on activation
Files to modify:
| File | Changes |
|---|---|
xorg-server/hw/xwin/win.h |
Add WIN_CLIPCURSOR_TIMER_ID constant; declare g_hwndGrabWindow extern |
xorg-server/hw/xwin/winmouse.c |
Hook master/slave ActivateGrab/DeactivateGrab; implement ClipCursor + ShowCursor logic; timer management |
xorg-server/hw/xwin/winwndproc.c |
Handle WIN_CLIPCURSOR_TIMER_ID in WM_TIMER; update WM_ACTIVATEAPP for grab-aware cursor management; remove erroneous cursor release code in WM_KILLFOCUS
|
| File | Raw Input Related | Cursor Lock Related |
|---|---|---|
xorg-server/hw/xwin/win.h |
winEnqueueRawMotion() declaration |
WIN_CLIPCURSOR_TIMER_ID constant; g_hwndGrabWindow extern |
xorg-server/hw/xwin/winmouse.c |
POINTER_NORAW definition; master valuator mode fix; winEnqueueRawMotion() implementation |
Hook master/slave ActivateGrab/DeactivateGrab; ClipCursor/ShowCursor logic; timer management |
xorg-server/hw/xwin/winwndproc.c |
WM_CREATE registers Raw Input device; WM_INPUT handler |
WM_TIMER re-applies ClipCursor; WM_ACTIVATEAPP grab-aware rewrite; remove WM_KILLFOCUS erroneous code |
All code below is based on the marchaesen/vcxsrv main branch, consistent with the diff attached to issue #77 / PR #78.
Location: After winEnqueueMotion declaration (around line 793)
void
winEnqueueMotion(int x, int y);
void /* NEW */
winEnqueueRawMotion(int dx, int dy); /* NEW */
/*
* winscrinit.c
*/Location: After #include (around line 28)
/* Prevent POINTER_ABSOLUTE events from generating XI_RawMotion.
* Only POINTER_RELATIVE events should produce XI_RawMotion for
* XInput2 relative-mode clients (SDL2, etc.). */
#ifndef POINTER_NORAW
#define POINTER_NORAW 0x20
#endif
POINTER_NORAWis checked in Xorg'sdix/getevents.c, but VcXsrv's headers do not export this constant. Value0x20is the Xorg internal convention.
Location: In DEVICE_INIT case, after InitPointerDeviceStruct() call and before free(map) (around line 260)
/* XInput2 clients query the master pointer's valuator mode
* (Virtual core pointer, device 2) to determine how to
* interpret XI_RawMotion values. Mode must be Relative for
* SDL2 relative mouse mode to work correctly.
* The slave (Windows pointer, device 6) mode is set
* automatically by InitPointerDeviceStruct, but the master
* mode must be updated explicitly.
*/
if (inputInfo.pointer && inputInfo.pointer->valuator &&
inputInfo.pointer->valuator->numAxes >= 2) {
inputInfo.pointer->valuator->axes[0].mode = Relative;
inputInfo.pointer->valuator->axes[1].mode = Relative;
}
inputInfois an X Server global variable, declared ininputstr.h, whichwinmouse.cindirectly includes viainputstr.h.Relativehas value1, defined inXI.h.
Add valuator_mask_zero() and POINTER_NORAW to the original function:
void
winEnqueueMotion(int x, int y)
{
int valuators[2];
ValuatorMask mask;
valuator_mask_zero(&mask); /* NEW */
valuators[0] = x;
valuators[1] = y;
valuator_mask_set_range(&mask, 0, 2, valuators);
QueuePointerEvents(g_pwinPointer, MotionNotify, 0,
POINTER_ABSOLUTE | POINTER_SCREEN | POINTER_NORAW, /* ADD POINTER_NORAW */
&mask);
}
valuator_mask_zero()ensures no garbage bits in the stack-allocatedValuatorMask.POINTER_NORAWpreventsGetPointerEvents()from generatingRawDeviceEvent(XI_RawMotion) for this absolute coordinate event.
Add after winEnqueueMotion():
/*
* Enqueue a raw motion event with relative coordinates.
*
* WM_INPUT delivers hardware-level relative displacement (lLastX/Y)
* via GetRawInputData. We inject it into the X server's
* POINTER_RELATIVE pipeline so XInput2 generates XI_RawMotion
* events for clients in relative mouse mode.
*/
void
winEnqueueRawMotion(int dx, int dy)
{
int valuators[2];
ValuatorMask mask;
valuator_mask_zero(&mask);
valuators[0] = dx;
valuators[1] = dy;
valuator_mask_set_range(&mask, 0, 2, valuators);
QueuePointerEvents(g_pwinPointer, MotionNotify, 0,
POINTER_RELATIVE, &mask);
}
POINTER_RELATIVE(withoutPOINTER_NORAW) allowsGetPointerEvents()to generateRawDeviceEvent, which becomes theXI_RawMotionevent delivered to SDL2.
Insert after tray icon initialization and before return 0:
/* Register for WM_INPUT (Raw Input) mouse events.
* These provide hardware-level relative displacement
* (lLastX/Y), independent of cursor position or screen
* boundaries, needed for XInput2 XI_RawMotion.
* dwFlags=0: receive only when focused; do NOT use
* RIDEV_NOLEGACY (breaks WM_MOUSEMOVE for absolute mode).
*/
{
RAWINPUTDEVICE rid;
rid.usUsagePage = 0x01; /* Generic Desktop Controls */
rid.usUsage = 0x02; /* Mouse */
rid.dwFlags = 0; /* Focused window only */
rid.hwndTarget = hwnd;
if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) {
ErrorF("winWindowProc - WM_CREATE: "
"RegisterRawInputDevices failed (error %lu)\n",
GetLastError());
}
}
rid.dwFlags = 0is deliberate:
- No
RIDEV_INPUTSINK: avoid receiving input when window is unfocused, preventing duplicate events- No
RIDEV_NOLEGACY: preserveWM_MOUSEMOVEfor absolute coordinate mode
Insert before case WM_MOUSEMOVE::
case WM_INPUT:
{
RAWINPUT raw;
UINT rawSize = sizeof(raw);
/* We can't do anything without privates or pointer */
if (s_pScreenPriv == NULL || s_pScreenInfo->fIgnoreInput)
break;
if (g_pwinPointer == NULL)
break;
if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw,
&rawSize, sizeof(RAWINPUTHEADER)) == (UINT)-1) {
ErrorF("winWindowProc - WM_INPUT: GetRawInputData failed\n");
break;
}
if (raw.header.dwType == RIM_TYPEMOUSE) {
LONG dx = raw.data.mouse.lLastX;
LONG dy = raw.data.mouse.lLastY;
if (dx != 0 || dy != 0) {
/* Filter absolute-position devices (touchscreens,
* tablets). They use the same Raw Input API but
* deliver absolute coordinates, not relative deltas. */
if (!(raw.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
winEnqueueRawMotion((int)dx, (int)dy);
}
}
}
/* Must break (not return 0) so DefWindowProc cleans up
* Raw Input resources. Return 0 causes resource leak and
* eventual Raw Input failure. */
break;
}Must use
break, neverreturn 0:DefWindowProc()is responsible for cleaning upWM_INPUTinternal resources.return 0skips cleanup, the Raw Input queue gradually fills up, andGetRawInputDataeventually stops working.
Location: After #define WIN_POLLING_MOUSE_TIMER_ID 2 (around line 88)
#define WIN_CLIPCURSOR_TIMER_ID 3Location: After extern DeviceIntPtr g_pwinKeyboard; (around line 531)
extern HWND g_hwndGrabWindow;Location: After #include "inpututils.h" (around line 28)
#include "winwindow.h" /* for winGetWindowPriv, winPrivWinRec */
winwindow.hprovides thewinPrivWinRecstructure andwinGetWindowPriv()macro for obtaining the WindowsHWNDfrom an X11WindowPtr.
Location: After static CARD8 const *g_winMouseButtonMap = NULL; (around line 35), before winMouseProc function
/* Master device grab callback hooks (for Core XGrabPointer) */
static void (*winOrigMasterActivateGrab)(DeviceIntPtr, GrabPtr, TimeStamp, Bool) = NULL;
static void (*winOrigMasterDeactivateGrab)(DeviceIntPtr) = NULL;
/* Slave device grab callback hooks (for XI2 XGrabDevice) */
static void (*winOrigSlaveActivateGrab)(DeviceIntPtr, GrabPtr, TimeStamp, Bool) = NULL;
static void (*winOrigSlaveDeactivateGrab)(DeviceIntPtr) = NULL;
/* HWND of the vcxsrv window that currently has the mouse. */
HWND g_hwndGrabWindow = NULL;
/* Timer ID for periodic ClipCursor re-apply */
static UINT_PTR g_grabTimerId = 0;Then add the following helper functions immediately after (before winMouseProc):
/*
* Apply cursor constraint to foreground window
*/
static void
winApplyCursorConstraint(void)
{
HWND hwnd = g_hwndGrabWindow;
RECT rect;
if (!hwnd)
hwnd = GetForegroundWindow();
if (!hwnd)
return;
GetClientRect(hwnd, &rect);
ClientToScreen(hwnd, (LPPOINT)&rect.left);
ClientToScreen(hwnd, (LPPOINT)&rect.right);
/* 1px padding: keep cursor away from sizing border */
rect.left += 1;
rect.top += 1;
rect.right -= 1;
rect.bottom -= 1;
ClipCursor(&rect);
}
/*
* Release cursor constraint
*/
static void
winReleaseCursorConstraint(void)
{
ClipCursor(NULL);
}
/*
* Force-hide cursor during grab
*/
static void
winGrabHideCursor(void)
{
while (ShowCursor(FALSE) >= 0)
;
}
/*
* Force-show cursor after grab release
*/
static void
winGrabShowCursor(void)
{
while (ShowCursor(TRUE) < 0)
;
}Location: After helper functions, before winMouseProc
/*
* Master device ActivateGrab hook.
* SDL2 X11 backend uses XGrabPointer (Core protocol), which operates
* on the MASTER pointer device. This hook handles that path.
*/
static void
winMasterActivateGrab(DeviceIntPtr dev, GrabPtr grab, TimeStamp time, Bool autoGrab)
{
if (winOrigMasterActivateGrab)
winOrigMasterActivateGrab(dev, grab, time, autoGrab);
/* Only confine cursor position if grab specifies a confine window. */
if (grab && grab->confineTo) {
/* Map X11 grab window to Windows hwnd for precise constraint. */
WindowPtr pGrabWin = grab->confineTo ? grab->confineTo : grab->window;
winPrivWinPtr pWinPriv = winGetWindowPriv(pGrabWin);
if (pWinPriv && pWinPriv->hWnd) {
g_hwndGrabWindow = pWinPriv->hWnd;
}
winApplyCursorConstraint();
/* Windows may silently cancel ClipCursor (cursor skipping,
* Win key, system notifications, etc.). This timer defends against it. */
if (!g_grabTimerId && g_hwndGrabWindow) {
g_grabTimerId = SetTimer(g_hwndGrabWindow, WIN_CLIPCURSOR_TIMER_ID, 10, NULL);
}
winGrabHideCursor();
}
/* confineTo = NULL: no Windows-side effect */
}
/*
* Master device DeactivateGrab hook.
*/
static void
winMasterDeactivateGrab(DeviceIntPtr dev)
{
/* dev->deviceGrab.grab points to the grab being released. */
GrabPtr grab = dev->deviceGrab.grab;
/* Confining grab: release Windows cursor state */
if (grab && grab->confineTo) {
winReleaseCursorConstraint();
winGrabShowCursor();
/* Stop periodic timer */
if (g_grabTimerId) {
KillTimer(g_hwndGrabWindow, g_grabTimerId);
g_grabTimerId = 0;
}
}
if (winOrigMasterDeactivateGrab)
winOrigMasterDeactivateGrab(dev);
}Location: After winMasterDeactivateGrab, before winMouseProc
/*
* Slave device ActivateGrab hook.
* XI2 XGrabDevice may operate on slave devices directly.
*/
static void
winSlaveActivateGrab(DeviceIntPtr dev, GrabPtr grab, TimeStamp time, Bool autoGrab)
{
if (winOrigSlaveActivateGrab)
winOrigSlaveActivateGrab(dev, grab, time, autoGrab);
/* Only confine cursor position if grab specifies a confine window. */
if (grab && grab->confineTo) {
/* Map X11 grab window to Windows hwnd for precise constraint. */
WindowPtr pGrabWin = grab->confineTo ? grab->confineTo : grab->window;
winPrivWinPtr pWinPriv = winGetWindowPriv(pGrabWin);
if (pWinPriv && pWinPriv->hWnd) {
g_hwndGrabWindow = pWinPriv->hWnd;
}
winApplyCursorConstraint();
/* Windows may silently cancel ClipCursor (cursor skipping,
* Win key, system notifications, etc.). This timer defends against it. */
if (!g_grabTimerId && g_hwndGrabWindow) {
g_grabTimerId = SetTimer(g_hwndGrabWindow, WIN_CLIPCURSOR_TIMER_ID, 10, NULL);
}
winGrabHideCursor();
}
/* confineTo = NULL: no Windows-side effect */
}
/*
* Slave device DeactivateGrab hook.
*/
static void
winSlaveDeactivateGrab(DeviceIntPtr dev)
{
/* dev->deviceGrab.grab points to the grab being released. */
GrabPtr grab = dev->deviceGrab.grab;
/* Confining grab: release Windows cursor state */
if (grab && grab->confineTo) {
winReleaseCursorConstraint();
winGrabShowCursor();
/* Stop periodic timer */
if (g_grabTimerId) {
KillTimer(g_hwndGrabWindow, g_grabTimerId);
g_grabTimerId = 0;
}
}
if (winOrigSlaveDeactivateGrab)
winOrigSlaveDeactivateGrab(dev);
}Location: After InitPointerDeviceStruct() call and before free(map) (around line 225)
/* Hook master device (for Core XGrabPointer used by SDL2) */
{
DeviceIntPtr master = inputInfo.pointer;
if (master && master != pDeviceInt) {
winOrigMasterActivateGrab = master->deviceGrab.ActivateGrab;
winOrigMasterDeactivateGrab = master->deviceGrab.DeactivateGrab;
master->deviceGrab.ActivateGrab = winMasterActivateGrab;
master->deviceGrab.DeactivateGrab = winMasterDeactivateGrab;
}
}
/* Hook slave device too (for XI2 XGrabDevice) */
winOrigSlaveActivateGrab = pDeviceInt->deviceGrab.ActivateGrab;
winOrigSlaveDeactivateGrab = pDeviceInt->deviceGrab.DeactivateGrab;
pDeviceInt->deviceGrab.ActivateGrab = winSlaveActivateGrab;
pDeviceInt->deviceGrab.DeactivateGrab = winSlaveDeactivateGrab;Location: In case DEVICE_OFF:, after pDevice->on = FALSE; (around line 247)
case DEVICE_OFF:
pDevice->on = FALSE;
/* Force release cursor constraint on device shutdown */
winReleaseCursorConstraint();
winGrabShowCursor();
if (g_grabTimerId) {
KillTimer(g_hwndGrabWindow, g_grabTimerId);
g_grabTimerId = 0;
}
break;In the existing case WM_TIMER: switch (wParam), after case WIN_POLLING_MOUSE_TIMER_ID: (around line 1050)
case WIN_CLIPCURSOR_TIMER_ID:
{
if (inputInfo.pointer &&
inputInfo.pointer->deviceGrab.grab &&
g_hwndGrabWindow) {
/* Windows may silently cancel it (cursor skipping,
* Win key, system notifications, etc.). */
RECT rect;
GetClientRect(g_hwndGrabWindow, &rect);
ClientToScreen(g_hwndGrabWindow, (LPPOINT)&rect.left);
ClientToScreen(g_hwndGrabWindow, (LPPOINT)&rect.right);
/* 1px padding: keep cursor away from sizing border */
rect.left += 1;
rect.top += 1;
rect.right -= 1;
rect.bottom -= 1;
ClipCursor(&rect);
}
}Location: In case WM_KILLFOCUS: (around lines 1174-1180)
Remove the following code:
/* REMOVE this code — keyboard focus loss does not mean grab release */
/* Safety: release cursor constraint if focus leaves while grabbed */
if (inputInfo.pointer && inputInfo.pointer->deviceGrab.grab) {
ClipCursor(NULL);
while (ShowCursor(TRUE) < 0)
;
}After removal, WM_KILLFOCUS only handles keyboard logic:
case WM_KILLFOCUS:
if (s_pScreenPriv == NULL || s_pScreenInfo->fIgnoreInput)
break;
/* Release any pressed keys */
winKeybdReleaseKeys();
/* Remove our keyboard hook if it is installed */
winRemoveKeyboardHookLL();
return 0;Location: Entire case WM_ACTIVATEAPP: block (around line 1228)
Before (original code):
case WM_ACTIVATEAPP:
/* ... guard checks ... */
s_pScreenPriv->fActive = wParam;
/* Reshow the Windows mouse cursor if we are being deactivated */
if (g_fSoftwareCursor && !s_pScreenPriv->fActive && !g_fCursor) {
g_fCursor = TRUE;
ShowCursor(TRUE);
}
/* ... pwinActivateApp call ... */
return 0;After:
case WM_ACTIVATEAPP:
if (s_pScreenPriv == NULL || s_pScreenInfo->fIgnoreInput)
break;
winDebug("winWindowProc - WM_ACTIVATEAPP\n");
/* Activate or deactivate */
s_pScreenPriv->fActive = wParam;
if (!s_pScreenPriv->fActive) {
/* DEACTIVATING */
if (inputInfo.pointer && inputInfo.pointer->deviceGrab.grab) {
ClipCursor(NULL);
while (ShowCursor(TRUE) < 0)
;
}
else {
/* Original logic: restore hover-hidden cursor */
if (g_fSoftwareCursor && !g_fCursor) {
g_fCursor = TRUE;
ShowCursor(TRUE);
}
}
}
else {
/* ACTIVATING */
if (inputInfo.pointer && inputInfo.pointer->deviceGrab.grab) {
if (g_hwndGrabWindow) {
RECT rect;
GetClientRect(g_hwndGrabWindow, &rect);
ClientToScreen(g_hwndGrabWindow, (LPPOINT)&rect.left);
ClientToScreen(g_hwndGrabWindow, (LPPOINT)&rect.right);
/* 1px padding: keep cursor away from sizing border */
rect.left += 1;
rect.top += 1;
rect.right -= 1;
rect.bottom -= 1;
ClipCursor(&rect);
}
while (ShowCursor(FALSE) >= 0)
;
}
}
/* Call engine specific screen activation/deactivation function */
(*s_pScreenPriv->pwinActivateApp) (s_pScreen);
return 0;dix/getevents.c's GetPointerEvents() generates RawDeviceEvent (i.e. XI_RawMotion) for all MotionNotify unless POINTER_NORAW is set:
if ((flags & POINTER_NORAW) == 0) {
raw = &events->raw_event; // Create XI_RawMotion
set_raw_valuators(raw, &mask, TRUE, raw->valuators.data_raw);
}Without POINTER_NORAW, WM_MOUSEMOVE (absolute coordinates x=500, y=300) generates XI_RawMotion(500, 300), mixing with the true relative increment XI_RawMotion(2, -1) from WM_INPUT, causing erratic behavior in SDL2.
SDL2 queries the master device (device 2) valuator mode:
// SDL_x11xinput2.c
devinfo = xinput2_get_device_info(videodata, rawev->deviceid);
// deviceid = 2 (Virtual core pointer, master)
devinfo->relative[axis] = (v->mode == XIModeRelative);When mode is Absolute, SDL2 takes the prev - current branch:
processed = devinfo->prev_coords[i] - coords[i];
// prev = last increment(2), current = new increment(3)
// processed = 2 - 3 = -1 // Completely wrong!When mode is Relative, SDL2 uses the value directly:
processed = coords[i]; // Use dx=3 directly, correctValuatorMask is a stack-allocated struct. valuator_mask_set_range() sets bits but does not clear existing ones. Without zero(), residual bits on the stack are read by set_raw_valuators() as extra axis data, polluting the XI_RawMotion valuator array.
Windows documentation explicitly states: WM_INPUT handling must have DefWindowProc() complete internal resource release. return 0 bypasses DefWindowProc(), causing resource leakage that eventually stops Raw Input from working.
-
RIDEV_NOLEGACYwould blockWM_MOUSEMOVE, breaking existing functionality that depends on absolute coordinates -
RIDEV_INPUTSINKwould allow unfocused windows to receive input, producing duplicate events -
dwFlags = 0keepsWM_INPUTandWM_MOUSEMOVEcoexisting, serving relative mode and absolute mode respectively
SDL2's X11 backend uses XGrabPointer (Core protocol), which operates on the master pointer device (inputInfo.pointer, device 2 "Virtual core pointer"), not the slave device (device 6 "Windows pointer"). Hooking ActivateGrab/DeactivateGrab on the slave device would never trigger for Core grab.
X11 automatically creates an implicit grab (confineTo = NULL) when a mouse button is pressed. An explicit grab (like SDL2 relative mode) sets confineTo = window. Both implicit and explicit grab Activate/Deactivate events are received by the hook function.
Correct approach: Only set ClipCursor + HideCursor when grab->confineTo != NULL; implicit grabs with confineTo = NULL perform no Windows-side cursor operations. Unconditionally calling HideCursor would incorrectly hide the cursor in scenarios where it should remain visible (normal mouse operation).
ShowCursor is a reference-counted API. A single call may fail to hide the cursor when the initial count is greater than 0. The while loop pushes the count below zero, ensuring the cursor is hidden regardless of initial state. This is independent of the g_fCursor mechanism in winwndproc.c.
The 10ms timer is a pragmatic workaround given that VcXsrv cannot use SetCursorPos(center) (which would break X11 cursor semantics). Game engines typically reset the cursor to center every frame via SetCursorPos, so cursor skip is fundamentally not an issue for them.
The original code released ClipCursor in WM_KILLFOCUS. This is wrong: keyboard focus loss does not mean grab release. The grab remains active at the DIX layer, and the cursor should remain confined. WM_ACTIVATEAPP(deactivate) handles the legitimate case of entire application deactivation.
xinput test-xi2 --rootMove the mouse. Output should show:
EVENT type 17 (RawMotion)
device: 2 (6)
detail: 0
flags:
valuators:
0: 2.00 (2.00)
1: -1.00 (-1.00)
Values should match actual mouse movement direction and speed.
Without setting the WARP variable:
- Launch AssaultCube
- Click to enter capture mode
- Cursor should be hidden and confined within the window
- Move mouse freely to control aim, should not escape
- Press Escape or click to release — cursor should restore and move freely
| Test | Expected Result |
|---|---|
| Alt+Tab switch | Cursor temporarily released during capture; re-locked upon return |
| Rapid mouse movement | Occasional momentary escape, pulled back by timer within 10ms |
| Window edge | No accidental resize (1px padding prevents reaching sizing border) |
| Non-capture program (xterm) | Normal mouse behavior, unaffected |
xterm &Normal (non-capture) mouse behavior should be completely unaffected.