From ab06f570a8e852cf8a1e12106bd08ad54136ba72 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <slouken@libsdl.org>
Date: Sun, 10 Dec 2017 09:17:33 -0800
Subject: [PATCH] Workaround for bug 3931 - spurious SDL_MOUSEMOTION events
 with SDL_HINT_MOUSE_RELATIVE_MODE_WARP 1 since Windows 10 Fall Creators
 update

Elis?e Maurer

The attached minimal program sets the SDL_HINT_MOUSE_RELATIVE_MODE_WARP to 1, enables relative mouse mode then logs all SDL_MOUSEMOTION xrel values as they happen.

When moving the mouse exclusively to the right:

 * On a Windows 10 installation before Fall Creators update (for instance, Version	10.0.15063 Build 15063), only positive values are reported, as expected
 * On a Windows 10 installation after Fall Creators update (for instance, Version 10.0.16299 Update 16299), a mix of positive and negative values are reported.

3 different people have reproduced this bug and have confirmed it started to happen after the Fall Creators update was installed. It happens with SDL 2.0.7 as well as latest default branch as of today.

It seems like some obscure (maybe unintended) Windows behavior change? Haven't been able to pin it down more yet.

(To force-upgrade a Windows installation to the Fall Creators update, you can use the update assistant at https://www.microsoft.com/en-us/software-download/windows10)

Eric Wasylishen

Broken GetCursorPos / SetCursorPos based games on Win 10 fall creators are not limited to SDL.. I just tested winquake.exe (original 1997 exe) and it now has "jumps" in the mouse input if you try to look around in a circle. It uses GetCursorPos/SetCursorPos by default. Switching WinQuake to use directinput (-dinput flag) seems to get rid of the jumps.

Daniel Gibson

A friend tested on Win10 1607 (which is before the Fall Creators Update) and the  the bug doesn't occur there, so the regression that SetCursorPos() doesn't reliably generate mouse events was indeed introduced with that update.
I even reproduced it in a minimal WinAPI-only application (https://gist.github.com/DanielGibson/b5b033c67b9137f0280af9fc53352c68), the weird thing is that if you don't do anything each "frame" (i.e. the mainloop only polls the events and does nothing else), there are a lot of mouse events with the coordinates you passed to SetCursorPos(), but when sleeping for 10ms in each iteration of the mainloop, those events basically don't happen anymore. Which is bad, because in games the each iteration of the mainloop usually takes 16ms..

I have a patch now that I find acceptable.
It checks for the windows version with RtlGetVersion() (https://msdn.microsoft.com/en-us/library/windows/hardware/ff561910.aspx) and only if it's >= Win10 build 16299, enables the workaround.
All code is in video/windows/SDL_windowsevents.c
and the workaround is, that for each WM_MOUSEMOVE event, "if(isWin10FCUorNewer && mouseID != SDL_TOUCH_MOUSEID && mouse->relative_mode_warp)", an addition mouse move event is generated with the coordinates of the center of the screen
(SDL_SendMouseMotion(data->window, mouseID, 0, center_x, center_y);) - which is exactly what would happen if windows generated those reliably itself.
This will cause SDL_PrivateSendMouseMotion() to set mouse->last_x = center_x; and mouse->last_y = center_y; so the next mouse relative mouse event will be calculated correctly.

If Microsoft ever fixes this bug, the IsWin10FCUorNewer() function would have to
be adjusted to also check for a maximum version, so the workaround is then disabled again.
---
 src/video/windows/SDL_windowsevents.c | 56 +++++++++++++++++++++++++++
 1 file changed, 56 insertions(+)

diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index b12c0ce31..40696b634 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -359,6 +359,11 @@ ShouldGenerateWindowCloseOnAltF4(void)
     return !SDL_GetHintBoolean(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, SDL_FALSE);
 }
 
+/* Win10 "Fall Creators Update" introduced the bug that SetCursorPos() (as used by SDL_WarpMouseInWindow())
+   doesn't reliably generate WM_MOUSEMOVE events anymore (see #3931) which breaks relative mouse mode via warping.
+   This is used to implement a workaround.. */
+static SDL_bool isWin10FCUorNewer = SDL_FALSE;
+
 LRESULT CALLBACK
 WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
 {
@@ -476,6 +481,17 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
             if (!mouse->relative_mode || mouse->relative_mode_warp) {
                 SDL_MouseID mouseID = (((GetMessageExtraInfo() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH) ? SDL_TOUCH_MOUSEID : 0);
                 SDL_SendMouseMotion(data->window, mouseID, 0, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+                if (isWin10FCUorNewer && mouseID != SDL_TOUCH_MOUSEID && mouse->relative_mode_warp) {
+                    /* To work around #3931, Win10 bug introduced in Fall Creators Update, where
+                       SetCursorPos() (SDL_WarpMouseInWindow()) doesn't reliably generate mouse events anymore,
+                       after each windows mouse event generate a fake event for the middle of the window
+                       if relative_mode_warp is used */
+                    int center_x = 0, center_y = 0;
+                    SDL_GetWindowSize(data->window, &center_x, &center_y);
+                    center_x /= 2;
+                    center_y /= 2;
+                    SDL_SendMouseMotion(data->window, mouseID, 0, center_x, center_y);
+                }
             }
         }
         /* don't break here, fall through to check the wParam like the button presses */
@@ -1051,6 +1067,44 @@ WIN_PumpEvents(_THIS)
     }
 }
 
+/* to work around #3931, a bug introduced in Win10 Fall Creators Update (build nr. 16299)
+   we need to detect the windows version. this struct and the function below does that.
+   usually this struct and the corresponding function (RtlGetVersion) are in <Ntddk.h>
+   but here we just load it dynamically */
+struct SDL_WIN_OSVERSIONINFOW {
+    ULONG dwOSVersionInfoSize;
+    ULONG dwMajorVersion;
+    ULONG dwMinorVersion;
+    ULONG dwBuildNumber;
+    ULONG dwPlatformId;
+    WCHAR szCSDVersion[128];
+};
+
+static SDL_bool
+IsWin10FCUorNewer(void)
+{
+    typedef LONG(WINAPI* RtlGetVersionPtr)(struct SDL_WIN_OSVERSIONINFOW*);
+    struct SDL_WIN_OSVERSIONINFOW info;
+    SDL_zero(info);
+
+    HMODULE handle = GetModuleHandleW(L"ntdll.dll");
+    if (handle) {
+        RtlGetVersionPtr getVersionPtr = (RtlGetVersionPtr)GetProcAddress(handle, "RtlGetVersion");
+        if (getVersionPtr != NULL) {
+            info.dwOSVersionInfoSize = sizeof(info);
+            if (getVersionPtr(&info) == 0) { /* STATUS_SUCCESS == 0 */
+                if (   (info.dwMajorVersion == 10 && info.dwMinorVersion == 0 && info.dwBuildNumber >= 16299)
+                    || (info.dwMajorVersion == 10 && info.dwMinorVersion > 0)
+                    || (info.dwMajorVersion > 10) )
+                {
+                    return SDL_TRUE;
+                }
+            }
+        }
+    }
+    return SDL_FALSE;
+}
+
 static int app_registered = 0;
 LPTSTR SDL_Appname = NULL;
 Uint32 SDL_Appstyle = 0;
@@ -1115,6 +1169,8 @@ SDL_RegisterApp(char *name, Uint32 style, void *hInst)
         return SDL_SetError("Couldn't register application class");
     }
 
+    isWin10FCUorNewer = IsWin10FCUorNewer();
+
     app_registered = 1;
     return 0;
 }