sdl2-compat: mouse: Accumulate fractional mouse data

From d8f6775971df2843524e58999630c2ed9bfe18cd Mon Sep 17 00:00:00 2001
From: Cameron Gutman <[EMAIL REDACTED]>
Date: Thu, 20 Feb 2025 18:57:24 -0600
Subject: [PATCH] mouse: Accumulate fractional mouse data

Fixes #372
---
 src/sdl2_compat.c | 49 +++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 41 insertions(+), 8 deletions(-)

diff --git a/src/sdl2_compat.c b/src/sdl2_compat.c
index 838f890..f070888 100644
--- a/src/sdl2_compat.c
+++ b/src/sdl2_compat.c
@@ -1028,6 +1028,12 @@ static SDL_mutex *EventWatchListMutex = NULL;
 static SDL2_LogOutputFunction LogOutputFunction2 = NULL;
 static EventFilterWrapperData *EventWatchers2 = NULL;
 static SDL2_bool relative_mouse_mode = SDL2_FALSE;
+static float relative_mouse_state_x_frac = 0.0f;
+static float relative_mouse_state_y_frac = 0.0f;
+static float relative_mouse_event_x_frac = 0.0f;
+static float relative_mouse_event_y_frac = 0.0f;
+static float mouse_wheel_event_x_frac = 0.0f;
+static float mouse_wheel_event_y_frac = 0.0f;
 static SDL_JoystickID *joystick_instance_list = NULL;
 static int num_joystick_instances = 0;
 static SDL_JoystickID *joystick_list = NULL;
@@ -1599,6 +1605,22 @@ static int GetIndexFromAudioDeviceInstance(SDL_AudioDeviceID devid, bool recordi
     return -1;
 }
 
+static int AccumulateFloatValueToInteger(float *frac, float value)
+{
+    float intval;
+
+    /* Reset the accumulated fractional value if the sign changes */
+    if ((*frac < 0.0f && value > 0.0f) || (*frac > 0.0f && value < 0.0f)) {
+        *frac = 0.0f;
+    }
+
+    /* Accumulate the fractional portion that is truncated by integer conversion */
+    *frac += value;
+    *frac = SDL3_modff(*frac, &intval);
+
+    return (int)intval;
+}
+
 /* (current) strategy for SDL_Events:
    in sdl12-compat, we built our own event queue, so when the SDL2 queue is pumped, we
    took the events we cared about and added them to the sdl12-compat queue, and otherwise
@@ -1664,6 +1686,13 @@ static SDL2_Event *Event3to2(const SDL_Event *event3, SDL2_Event *event2)
     case SDL_EVENT_DROP_COMPLETE:
         event2->drop.windowID = event3->drop.windowID;
         break;
+    case SDL_EVENT_WINDOW_MOUSE_ENTER:
+        /* Reset accumulated fractional mouse data when mouse focus changes */
+        relative_mouse_event_x_frac = 0.0f;
+        relative_mouse_event_y_frac = 0.0f;
+        mouse_wheel_event_x_frac = 0.0f;
+        mouse_wheel_event_y_frac = 0.0f;
+        break;
     case SDL_EVENT_MOUSE_MOTION:
         renderer = SDL3_GetRenderer(SDL3_GetWindowFromID(event3->motion.windowID));
         if (renderer) {
@@ -1685,14 +1714,14 @@ static SDL2_Event *Event3to2(const SDL_Event *event3, SDL2_Event *event2)
             motion->state = (Uint8)event3->motion.state;
             motion->x = (Sint32)event3->motion.x;
             motion->y = (Sint32)event3->motion.y;
-            motion->xrel = (Sint32)event3->motion.xrel;
-            motion->yrel = (Sint32)event3->motion.yrel;
+            motion->xrel = AccumulateFloatValueToInteger(&relative_mouse_event_x_frac, event3->motion.xrel);
+            motion->yrel = AccumulateFloatValueToInteger(&relative_mouse_event_y_frac, event3->motion.yrel);
         } else {
             SDL2_MouseMotionEvent *motion = &event2->motion;
             motion->x = (Sint32)event3->motion.x;
             motion->y = (Sint32)event3->motion.y;
-            motion->xrel = (Sint32)event3->motion.xrel;
-            motion->yrel = (Sint32)event3->motion.yrel;
+            motion->xrel = AccumulateFloatValueToInteger(&relative_mouse_event_x_frac, event3->motion.xrel);
+            motion->yrel = AccumulateFloatValueToInteger(&relative_mouse_event_y_frac, event3->motion.yrel);
         }
         break;
     case SDL_EVENT_MOUSE_BUTTON_DOWN:
@@ -1738,8 +1767,8 @@ static SDL2_Event *Event3to2(const SDL_Event *event3, SDL2_Event *event2)
             wheel->y = (Sint32)(event3->wheel.x * 120);
         } else {
             SDL2_MouseWheelEvent *wheel = &event2->wheel;
-            wheel->x = (Sint32)event3->wheel.x;
-            wheel->y = (Sint32)event3->wheel.y;
+            wheel->x = AccumulateFloatValueToInteger(&mouse_wheel_event_x_frac, event3->wheel.x);
+            wheel->y = AccumulateFloatValueToInteger(&mouse_wheel_event_y_frac, event3->wheel.y);
             wheel->preciseX = event3->wheel.x;
             wheel->preciseY = event3->wheel.y;
             wheel->mouseX = (Sint32)event3->wheel.mouse_x;
@@ -2651,8 +2680,8 @@ SDL_GetRelativeMouseState(int *x, int *y)
 {
     float fx, fy;
     Uint32 ret = SDL3_GetRelativeMouseState(&fx, &fy);
-    if (x) *x = (int)fx;
-    if (y) *y = (int)fy;
+    if (x) *x = AccumulateFloatValueToInteger(&relative_mouse_state_x_frac, fx);
+    if (y) *y = AccumulateFloatValueToInteger(&relative_mouse_state_y_frac, fy);
     return ret;
 }
 
@@ -5974,6 +6003,10 @@ static void PostInitSubsystem(SDL_InitFlags new_flags)
         (void)SDL_EventState(SDL_EVENT_TEXT_INPUT, SDL2_DISABLE);
         (void)SDL_EventState(SDL_EVENT_TEXT_EDITING, SDL2_DISABLE);
         (void)SDL_EventState(SDL2_SYSWMEVENT, SDL2_DISABLE);
+
+        /* SDL_GetRelativeMouseState() resets when the event subsystem initializes */
+        relative_mouse_state_x_frac = 0.0f;
+        relative_mouse_state_y_frac = 0.0f;
     }
 
     if (new_flags & SDL_INIT_VIDEO) {