SDL: Added a hint SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE to control whether to use system mouse acceleration on raw relative motion.

From 92b3c53c92971e685254fd89f89ce6bde8cea60e Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 22 Aug 2022 16:25:25 -0700
Subject: [PATCH] Added a hint SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE to control
 whether to use system mouse acceleration on raw relative motion.

This is currently only implemented on Windows, and "Enhanced pointer
precision" mode is not quite correct.
---
 include/SDL_hints.h                   |  11 +++
 src/events/SDL_mouse.c                | 125 ++++++++++++++++++++++++--
 src/events/SDL_mouse_c.h              |   8 ++
 src/video/windows/SDL_windowsevents.c |   7 ++
 src/video/windows/SDL_windowsmouse.c  | 105 ++++++++++++++++++++++
 src/video/windows/SDL_windowsmouse.h  |   1 +
 6 files changed, 249 insertions(+), 8 deletions(-)

diff --git a/include/SDL_hints.h b/include/SDL_hints.h
index d99fe1373f5..738f55044b0 100644
--- a/include/SDL_hints.h
+++ b/include/SDL_hints.h
@@ -1106,6 +1106,17 @@ extern "C" {
  */
 #define SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE    "SDL_MOUSE_RELATIVE_SPEED_SCALE"
 
+/**
+ *  \brief  A variable controlling whether the system mouse acceleration curve is used for relative mouse motion.
+ *
+ *  This variable can be set to the following values:
+ *    "0"       - Relative mouse motion will be unscaled (the default)
+ *    "1"       - Relative mouse motion will be scaled using the system mouse acceleration curve.
+ *
+ *    If SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE is set, that will override the system speed scale.
+ */
+#define SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE    "SDL_MOUSE_RELATIVE_SYSTEM_SCALE"
+
 /**
  *  \brief  A variable controlling whether a motion event should be generated for mouse warping in relative mode.
  *
diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c
index 76433fc6502..ffdda915461 100644
--- a/src/events/SDL_mouse.c
+++ b/src/events/SDL_mouse.c
@@ -83,8 +83,10 @@ SDL_MouseNormalSpeedScaleChanged(void *userdata, const char *name, const char *o
     SDL_Mouse *mouse = (SDL_Mouse *)userdata;
 
     if (hint && *hint) {
+        mouse->enable_normal_speed_scale = SDL_TRUE;
         mouse->normal_speed_scale = (float)SDL_atof(hint);
     } else {
+        mouse->enable_normal_speed_scale = SDL_FALSE;
         mouse->normal_speed_scale = 1.0f;
     }
 }
@@ -95,12 +97,22 @@ SDL_MouseRelativeSpeedScaleChanged(void *userdata, const char *name, const char
     SDL_Mouse *mouse = (SDL_Mouse *)userdata;
 
     if (hint && *hint) {
+        mouse->enable_relative_speed_scale = SDL_TRUE;
         mouse->relative_speed_scale = (float)SDL_atof(hint);
     } else {
+        mouse->enable_relative_speed_scale = SDL_FALSE;
         mouse->relative_speed_scale = 1.0f;
     }
 }
 
+static void SDLCALL
+SDL_MouseRelativeSystemScaleChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
+{
+    SDL_Mouse *mouse = (SDL_Mouse *)userdata;
+
+    mouse->enable_relative_system_scale = SDL_GetStringBoolean(hint, SDL_FALSE);
+}
+
 static void SDLCALL
 SDL_TouchMouseEventsChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 {
@@ -189,6 +201,9 @@ SDL_MouseInit(void)
     SDL_AddHintCallback(SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE,
                         SDL_MouseRelativeSpeedScaleChanged, mouse);
 
+    SDL_AddHintCallback(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE,
+                        SDL_MouseRelativeSystemScaleChanged, mouse);
+
     SDL_AddHintCallback(SDL_HINT_TOUCH_MOUSE_EVENTS,
                         SDL_TouchMouseEventsChanged, mouse);
 
@@ -344,7 +359,10 @@ SDL_SendMouseMotion(SDL_Window * window, SDL_MouseID mouseID, int relative, int
 static int
 GetScaledMouseDelta(float scale, int value, float *accum)
 {
-    if (scale != 1.0f) {
+    if (value && scale != 1.0f) {
+        if ((value > 0) != (*accum > 0)) {
+            *accum = 0.0f;
+        }
         *accum += scale * value;
         if (*accum >= 0.0f) {
             value = (int)SDL_floor(*accum);
@@ -356,6 +374,100 @@ GetScaledMouseDelta(float scale, int value, float *accum)
     return value;
 }
 
+static float
+CalculateSystemScale(SDL_Mouse *mouse, int *x, int *y)
+{
+    int i;
+    int n = mouse->num_system_scale_values;
+    float *v = mouse->system_scale_values;
+    float speed, coef, scale;
+
+    /* If we're using a single scale value, return that */
+    if (n == 1) {
+        return v[0];
+    }
+
+    speed = SDL_sqrtf((float)(*x * *x) + (*y * *y));
+    for (i = 0; i < (n - 2); i += 2) {
+        if (speed < v[i + 2]) {
+            break;
+        }
+    }
+    if (i == (n - 2)) {
+        scale = v[n - 1];
+    } else if (speed <= v[i]) {
+        scale = v[i + 1];
+    } else {
+        coef = (speed - v[i]) / (v[i + 2] - v[i]);
+        scale = v[i + 1] + (coef * (v[i + 3] - v[i + 1]));
+    }
+    SDL_Log("speed = %.2f, scale = %.2f\n", speed, scale);
+    return scale;
+}
+
+/* You can set either a single scale, or a set of {speed, scale} values in ascending order */
+int
+SDL_SetMouseSystemScale(int num_values, const float *values)
+{
+    SDL_Mouse *mouse = SDL_GetMouse();
+    float *v;
+
+    if (num_values == mouse->num_system_scale_values &&
+        SDL_memcmp(values, mouse->system_scale_values, num_values * sizeof(*values)) == 0) {
+        /* Nothing has changed */
+        return 0;
+    }
+
+    if (num_values < 1) {
+        return SDL_SetError("You must have at least one scale value");
+    }
+
+    if (num_values > 1) {
+        /* Validate the values */
+        int i;
+
+        if (num_values < 4 || (num_values % 2) != 0) {
+            return SDL_SetError("You must pass a set of {speed, scale} values");
+        }
+
+        for (i = 0; i < (num_values - 2); i += 2) {
+            if (values[i] >= values[i + 2]) {
+                return SDL_SetError("Speed values must be in ascending order");
+            }
+        }
+    }
+
+    v = (float *)SDL_realloc(mouse->system_scale_values, num_values * sizeof(*values));
+    if (!v) {
+        return SDL_OutOfMemory();
+    }
+    SDL_memcpy(v, values, num_values * sizeof(*values));
+
+    mouse->num_system_scale_values = num_values;
+    mouse->system_scale_values = v;
+    return 0;
+}
+
+static void
+GetScaledMouseDeltas(SDL_Mouse *mouse, int *x, int *y)
+{
+    if (mouse->relative_mode) {
+        if (mouse->enable_relative_speed_scale) {
+            *x = GetScaledMouseDelta(mouse->relative_speed_scale, *x, &mouse->scale_accum_x);
+            *y = GetScaledMouseDelta(mouse->relative_speed_scale, *y, &mouse->scale_accum_y);
+        } else if (mouse->enable_relative_system_scale && mouse->num_system_scale_values > 0) {
+            float relative_system_scale = CalculateSystemScale(mouse, x, y);
+            *x = GetScaledMouseDelta(relative_system_scale, *x, &mouse->scale_accum_x);
+            *y = GetScaledMouseDelta(relative_system_scale, *y, &mouse->scale_accum_y);
+        }
+    } else {
+        if (mouse->enable_normal_speed_scale) {
+            *x = GetScaledMouseDelta(mouse->normal_speed_scale, *x, &mouse->scale_accum_x);
+            *y = GetScaledMouseDelta(mouse->normal_speed_scale, *y, &mouse->scale_accum_y);
+        }
+    }
+}
+
 static int
 SDL_PrivateSendMouseMotion(SDL_Window * window, SDL_MouseID mouseID, int relative, int x, int y)
 {
@@ -405,13 +517,7 @@ SDL_PrivateSendMouseMotion(SDL_Window * window, SDL_MouseID mouseID, int relativ
     }
 
     if (relative) {
-        if (mouse->relative_mode) {
-            x = GetScaledMouseDelta(mouse->relative_speed_scale, x, &mouse->scale_accum_x);
-            y = GetScaledMouseDelta(mouse->relative_speed_scale, y, &mouse->scale_accum_y);
-        } else {
-            x = GetScaledMouseDelta(mouse->normal_speed_scale, x, &mouse->scale_accum_x);
-            y = GetScaledMouseDelta(mouse->normal_speed_scale, y, &mouse->scale_accum_y);
-        }
+        GetScaledMouseDeltas(mouse, &x, &y);
         xrel = x;
         yrel = y;
         x = (mouse->last_x + xrel);
@@ -818,6 +924,9 @@ SDL_MouseQuit(void)
     SDL_DelHintCallback(SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE,
                         SDL_MouseRelativeSpeedScaleChanged, mouse);
 
+    SDL_DelHintCallback(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE,
+                        SDL_MouseRelativeSystemScaleChanged, mouse);
+
     SDL_DelHintCallback(SDL_HINT_TOUCH_MOUSE_EVENTS,
                         SDL_TouchMouseEventsChanged, mouse);
 
diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h
index 6b937b5dce7..e9dda276bf4 100644
--- a/src/events/SDL_mouse_c.h
+++ b/src/events/SDL_mouse_c.h
@@ -92,8 +92,13 @@ typedef struct
     SDL_bool relative_mode;
     SDL_bool relative_mode_warp;
     SDL_bool relative_mode_warp_motion;
+    SDL_bool enable_normal_speed_scale;
     float normal_speed_scale;
+    SDL_bool enable_relative_speed_scale;
     float relative_speed_scale;
+    SDL_bool enable_relative_system_scale;
+    int num_system_scale_values;
+    float *system_scale_values;
     float scale_accum_x;
     float scale_accum_y;
     Uint32 double_click_time;
@@ -141,6 +146,9 @@ extern void SDL_SetMouseFocus(SDL_Window * window);
 /* Update the mouse capture window */
 extern int SDL_UpdateMouseCapture(SDL_bool force_release);
 
+/* You can set either a single scale, or a set of {speed, scale} values in sorted order */
+extern int SDL_SetMouseSystemScale(int num_values, const float *values);
+
 /* Send a mouse motion event */
 extern int SDL_SendMouseMotion(SDL_Window * window, SDL_MouseID mouseID, int relative, int x, int y);
 
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index 60a563e1890..caba8b4c316 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -1628,6 +1628,13 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
             return 0;
         }
         break;
+
+    case WM_SETTINGCHANGE:
+        if (wParam == SPI_SETMOUSE || wParam == SPI_SETMOUSESPEED) {
+            WIN_UpdateMouseSystemScale();
+        }
+        break;
+
 #endif /*!defined(__XBOXONE__) && !defined(__XBOXSERIES__)*/
     }
 
diff --git a/src/video/windows/SDL_windowsmouse.c b/src/video/windows/SDL_windowsmouse.c
index a52c4407948..2ef0f14520b 100644
--- a/src/video/windows/SDL_windowsmouse.c
+++ b/src/video/windows/SDL_windowsmouse.c
@@ -363,6 +363,8 @@ WIN_InitMouse(_THIS)
     SDL_SetDefaultCursor(WIN_CreateDefaultCursor());
 
     SDL_blank_cursor = WIN_CreateBlankCursor();
+
+    WIN_UpdateMouseSystemScale();
 }
 
 void
@@ -379,6 +381,109 @@ WIN_QuitMouse(_THIS)
     }
 }
 
+/* For a great description of how the enhanced mouse curve works, see:
+ * https://superuser.com/questions/278362/windows-mouse-acceleration-curve-smoothmousexcurve-and-smoothmouseycurve
+ * http://www.esreality.com/?a=post&id=1846538/
+ */
+static SDL_bool
+LoadFiveFixedPointFloats(BYTE *bytes, float *values)
+{
+    int i;
+
+    for (i = 0; i < 5; ++i) {
+        float fraction = (float)((Uint16) bytes[1] << 8 | bytes[0]) / 65535.0f;
+        float value = (float)(((Uint16)bytes[3] << 8) | bytes[2]) + fraction;
+        *values++ = value;
+        bytes += 8;
+    }
+    return SDL_TRUE;
+}
+
+static void
+WIN_SetEnhancedMouseScale(int mouse_speed)
+{
+    float scale = (float) mouse_speed / 10.0f;
+    HKEY hKey;
+    DWORD dwType = REG_BINARY;
+    BYTE value[40];
+    DWORD length = sizeof(value);
+    int i;
+    float xpoints[5];
+    float ypoints[5];
+    float scale_points[10];
+    const int dpi = 96; // FIXME, how do we handle different monitors with different DPI?
+    const float display_factor = 3.5f * (150.0f / dpi);
+
+    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Control Panel\\Mouse", 0, KEY_READ, &hKey)  == ERROR_SUCCESS) {
+        if (RegQueryValueExW(hKey, L"SmoothMouseXCurve", 0, &dwType, value, &length) == ERROR_SUCCESS &&
+            LoadFiveFixedPointFloats(value, xpoints) &&
+            RegQueryValueExW(hKey, L"SmoothMouseYCurve", 0, &dwType, value, &length) == ERROR_SUCCESS &&
+            LoadFiveFixedPointFloats(value, ypoints)) {
+            for (i = 0; i < 5; ++i) {
+                float gain;
+                if (xpoints[i] > 0.0f) {
+                    gain = (ypoints[i] / xpoints[i]) * scale;
+                } else {
+                    gain = 0.0f;
+                }
+                scale_points[i * 2] = xpoints[i];
+                scale_points[i * 2 + 1] = gain / display_factor;
+                //SDL_Log("Point %d = %f,%f\n", i, scale_points[i * 2], scale_points[i * 2 + 1]);
+            }
+            SDL_SetMouseSystemScale(SDL_arraysize(scale_points), scale_points);
+        }
+        RegCloseKey(hKey);
+    }
+}
+
+static void
+WIN_SetLinearMouseScale(int mouse_speed)
+{
+    static float mouse_speed_scale[] = {
+        0.0f,
+        1 / 32.0f,
+        1 / 16.0f,
+        1 / 8.0f,
+        2 / 8.0f,
+        3 / 8.0f,
+        4 / 8.0f,
+        5 / 8.0f,
+        6 / 8.0f,
+        7 / 8.0f,
+        1.0f,
+        1.25f,
+        1.5f,
+        1.75f,
+        2.0f,
+        2.25f,
+        2.5f,
+        2.75f,
+        3.0f,
+        3.25f,
+        3.5f
+    };
+
+    if (mouse_speed > 0 && mouse_speed < SDL_arraysize(mouse_speed_scale)) {
+        SDL_SetMouseSystemScale(1, &mouse_speed_scale[mouse_speed]);
+    }
+}
+
+void
+WIN_UpdateMouseSystemScale()
+{
+    int mouse_speed;
+    int params[3] = { 0, 0, 0 };
+
+    if (SystemParametersInfo(SPI_GETMOUSESPEED, 0, &mouse_speed, 0) &&
+        SystemParametersInfo(SPI_GETMOUSE, 0, params, 0)) {
+        if (params[2]) {
+            WIN_SetEnhancedMouseScale(mouse_speed);
+        } else {
+            WIN_SetLinearMouseScale(mouse_speed);
+        }
+    }
+}
+
 #endif /* SDL_VIDEO_DRIVER_WINDOWS */
 
 /* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/video/windows/SDL_windowsmouse.h b/src/video/windows/SDL_windowsmouse.h
index fd1a8a323b0..e67f706c923 100644
--- a/src/video/windows/SDL_windowsmouse.h
+++ b/src/video/windows/SDL_windowsmouse.h
@@ -29,6 +29,7 @@ extern HCURSOR SDL_cursor;
 extern void WIN_InitMouse(_THIS);
 extern void WIN_QuitMouse(_THIS);
 extern void WIN_SetCursorPos(int x, int y);
+extern void WIN_UpdateMouseSystemScale();
 
 #endif /* SDL_windowsmouse_h_ */