SDL: Pass the event timestamp for joystick events

From 73f4aeee6a2affe2777acf513a745142ec81ebf6 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 3 Dec 2022 11:15:50 -0800
Subject: [PATCH] Pass the event timestamp for joystick events

This allows the application to get more fine grained information about controller event timing, and group events that happened together.
---
 src/core/linux/SDL_evdev.c                    | 102 +++---
 src/core/linux/SDL_evdev.h                    |   3 +
 src/joystick/SDL_gamecontroller.c             |  53 +--
 src/joystick/SDL_joystick.c                   |  35 +-
 src/joystick/SDL_joystick_c.h                 |  12 +-
 src/joystick/android/SDL_sysjoystick.c        |  17 +-
 src/joystick/apple/SDL_mfijoystick.m          |  36 ++-
 src/joystick/bsd/SDL_bsdjoystick.c            |  23 +-
 src/joystick/darwin/SDL_iokitjoystick.c       |   7 +-
 src/joystick/emscripten/SDL_sysjoystick.c     |   5 +-
 src/joystick/haiku/SDL_haikujoystick.cc       |   7 +-
 src/joystick/hidapi/SDL_hidapi_gamecube.c     |  52 +--
 src/joystick/hidapi/SDL_hidapi_luna.c         |  92 +++---
 src/joystick/hidapi/SDL_hidapi_ps3.c          | 135 ++++----
 src/joystick/hidapi/SDL_hidapi_ps4.c          |  53 +--
 src/joystick/hidapi/SDL_hidapi_ps5.c          | 106 +++---
 src/joystick/hidapi/SDL_hidapi_shield.c       | 101 +++---
 src/joystick/hidapi/SDL_hidapi_stadia.c       |  47 +--
 src/joystick/hidapi/SDL_hidapi_steam.c        |  50 +--
 src/joystick/hidapi/SDL_hidapi_switch.c       | 304 +++++++++---------
 src/joystick/hidapi/SDL_hidapi_wii.c          |  55 ++--
 src/joystick/hidapi/SDL_hidapi_xbox360.c      |  43 +--
 src/joystick/hidapi/SDL_hidapi_xbox360w.c     |  43 +--
 src/joystick/hidapi/SDL_hidapi_xboxone.c      | 159 ++++-----
 src/joystick/linux/SDL_sysjoystick.c          |  48 +--
 src/joystick/n3ds/SDL_sysjoystick.c           |  42 ++-
 src/joystick/ps2/SDL_sysjoystick.c            |   5 +-
 src/joystick/psp/SDL_sysjoystick.c            |   7 +-
 src/joystick/virtual/SDL_virtualjoystick.c    |   7 +-
 src/joystick/vita/SDL_sysjoystick.c           |  15 +-
 src/joystick/windows/SDL_dinputjoystick.c     |  28 +-
 src/joystick/windows/SDL_rawinputjoystick.c   |  45 ++-
 .../windows/SDL_windows_gaming_input.c        |  11 +-
 src/joystick/windows/SDL_xinputjoystick.c     |  32 +-
 src/timer/SDL_timer.c                         |  10 +
 src/timer/SDL_timer_c.h                       |   1 +
 36 files changed, 951 insertions(+), 840 deletions(-)

diff --git a/src/core/linux/SDL_evdev.c b/src/core/linux/SDL_evdev.c
index f337999979a1..a8e96cfaef67 100644
--- a/src/core/linux/SDL_evdev.c
+++ b/src/core/linux/SDL_evdev.c
@@ -38,6 +38,7 @@
 #include <sys/ioctl.h>
 #include <linux/input.h>
 
+#include "../../timer/SDL_timer_c.h"
 #include "../../events/SDL_events_c.h"
 #include "../../events/SDL_scancode_tables_c.h"
 #include "../../core/linux/SDL_evdev_capabilities.h"
@@ -290,32 +291,34 @@ void SDL_EVDEV_Poll(void)
         while ((len = read(item->fd, events, (sizeof events))) > 0) {
             len /= sizeof(events[0]);
             for (i = 0; i < len; ++i) {
+                struct input_event *event = &events[i];
+
                 /* special handling for touchscreen, that should eventually be
                    used for all devices */
                 if (item->out_of_sync && item->is_touchscreen &&
-                    events[i].type == EV_SYN && events[i].code != SYN_REPORT) {
+                    event->type == EV_SYN && event->code != SYN_REPORT) {
                     break;
                 }
 
-                switch (events[i].type) {
+                switch (event->type) {
                 case EV_KEY:
-                    if (events[i].code >= BTN_MOUSE && events[i].code < BTN_MOUSE + SDL_arraysize(EVDEV_MouseButtons)) {
-                        mouse_button = events[i].code - BTN_MOUSE;
-                        if (events[i].value == 0) {
-                            SDL_SendMouseButton(0, mouse->focus, (SDL_MouseID)item->fd, SDL_RELEASED, EVDEV_MouseButtons[mouse_button]);
-                        } else if (events[i].value == 1) {
-                            SDL_SendMouseButton(0, mouse->focus, (SDL_MouseID)item->fd, SDL_PRESSED, EVDEV_MouseButtons[mouse_button]);
+                    if (event->code >= BTN_MOUSE && event->code < BTN_MOUSE + SDL_arraysize(EVDEV_MouseButtons)) {
+                        mouse_button = event->code - BTN_MOUSE;
+                        if (event->value == 0) {
+                            SDL_SendMouseButton(SDL_EVDEV_GetEventTimestamp(event), mouse->focus, (SDL_MouseID)item->fd, SDL_RELEASED, EVDEV_MouseButtons[mouse_button]);
+                        } else if (event->value == 1) {
+                            SDL_SendMouseButton(SDL_EVDEV_GetEventTimestamp(event), mouse->focus, (SDL_MouseID)item->fd, SDL_PRESSED, EVDEV_MouseButtons[mouse_button]);
                         }
                         break;
                     }
 
-                    /* BTH_TOUCH event value 1 indicates there is contact with
+                    /* BTN_TOUCH event value 1 indicates there is contact with
                        a touchscreen or trackpad (earlist finger's current
                        position is sent in EV_ABS ABS_X/ABS_Y, switching to
                        next finger after earlist is released) */
-                    if (item->is_touchscreen && events[i].code == BTN_TOUCH) {
+                    if (item->is_touchscreen && event->code == BTN_TOUCH) {
                         if (item->touchscreen_data->max_slots == 1) {
-                            if (events[i].value) {
+                            if (event->value) {
                                 item->touchscreen_data->slots[0].delta = EVDEV_TOUCH_SLOTDELTA_DOWN;
                             } else {
                                 item->touchscreen_data->slots[0].delta = EVDEV_TOUCH_SLOTDELTA_UP;
@@ -325,30 +328,30 @@ void SDL_EVDEV_Poll(void)
                     }
 
                     /* Probably keyboard */
-                    scan_code = SDL_EVDEV_translate_keycode(events[i].code);
+                    scan_code = SDL_EVDEV_translate_keycode(event->code);
                     if (scan_code != SDL_SCANCODE_UNKNOWN) {
-                        if (events[i].value == 0) {
-                            SDL_SendKeyboardKey(0, SDL_RELEASED, scan_code);
-                        } else if (events[i].value == 1 || events[i].value == 2 /* key repeated */) {
-                            SDL_SendKeyboardKey(0, SDL_PRESSED, scan_code);
+                        if (event->value == 0) {
+                            SDL_SendKeyboardKey(SDL_EVDEV_GetEventTimestamp(event), SDL_RELEASED, scan_code);
+                        } else if (event->value == 1 || event->value == 2 /* key repeated */) {
+                            SDL_SendKeyboardKey(SDL_EVDEV_GetEventTimestamp(event), SDL_PRESSED, scan_code);
                         }
                     }
-                    SDL_EVDEV_kbd_keycode(_this->kbd, events[i].code, events[i].value);
+                    SDL_EVDEV_kbd_keycode(_this->kbd, event->code, event->value);
                     break;
                 case EV_ABS:
-                    switch (events[i].code) {
+                    switch (event->code) {
                     case ABS_MT_SLOT:
                         if (!item->is_touchscreen) { /* FIXME: temp hack */
                             break;
                         }
-                        item->touchscreen_data->current_slot = events[i].value;
+                        item->touchscreen_data->current_slot = event->value;
                         break;
                     case ABS_MT_TRACKING_ID:
                         if (!item->is_touchscreen) { /* FIXME: temp hack */
                             break;
                         }
-                        if (events[i].value >= 0) {
-                            item->touchscreen_data->slots[item->touchscreen_data->current_slot].tracking_id = events[i].value;
+                        if (event->value >= 0) {
+                            item->touchscreen_data->slots[item->touchscreen_data->current_slot].tracking_id = event->value;
                             item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta = EVDEV_TOUCH_SLOTDELTA_DOWN;
                         } else {
                             item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta = EVDEV_TOUCH_SLOTDELTA_UP;
@@ -358,7 +361,7 @@ void SDL_EVDEV_Poll(void)
                         if (!item->is_touchscreen) { /* FIXME: temp hack */
                             break;
                         }
-                        item->touchscreen_data->slots[item->touchscreen_data->current_slot].x = events[i].value;
+                        item->touchscreen_data->slots[item->touchscreen_data->current_slot].x = event->value;
                         if (item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta == EVDEV_TOUCH_SLOTDELTA_NONE) {
                             item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta = EVDEV_TOUCH_SLOTDELTA_MOVE;
                         }
@@ -367,7 +370,7 @@ void SDL_EVDEV_Poll(void)
                         if (!item->is_touchscreen) { /* FIXME: temp hack */
                             break;
                         }
-                        item->touchscreen_data->slots[item->touchscreen_data->current_slot].y = events[i].value;
+                        item->touchscreen_data->slots[item->touchscreen_data->current_slot].y = event->value;
                         if (item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta == EVDEV_TOUCH_SLOTDELTA_NONE) {
                             item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta = EVDEV_TOUCH_SLOTDELTA_MOVE;
                         }
@@ -376,7 +379,7 @@ void SDL_EVDEV_Poll(void)
                         if (!item->is_touchscreen) { /* FIXME: temp hack */
                             break;
                         }
-                        item->touchscreen_data->slots[item->touchscreen_data->current_slot].pressure = events[i].value;
+                        item->touchscreen_data->slots[item->touchscreen_data->current_slot].pressure = event->value;
                         if (item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta == EVDEV_TOUCH_SLOTDELTA_NONE) {
                             item->touchscreen_data->slots[item->touchscreen_data->current_slot].delta = EVDEV_TOUCH_SLOTDELTA_MOVE;
                         }
@@ -386,10 +389,10 @@ void SDL_EVDEV_Poll(void)
                             if (item->touchscreen_data->max_slots != 1) {
                                 break;
                             }
-                            item->touchscreen_data->slots[0].x = events[i].value;
+                            item->touchscreen_data->slots[0].x = event->value;
                         } else if (!item->relative_mouse) {
                             /* FIXME: Normalize to input device's reported input range (EVIOCGABS) */
-                            item->mouse_x = events[i].value;
+                            item->mouse_x = event->value;
                         }
                         break;
                     case ABS_Y:
@@ -397,10 +400,10 @@ void SDL_EVDEV_Poll(void)
                             if (item->touchscreen_data->max_slots != 1) {
                                 break;
                             }
-                            item->touchscreen_data->slots[0].y = events[i].value;
+                            item->touchscreen_data->slots[0].y = event->value;
                         } else if (!item->relative_mouse) {
                             /* FIXME: Normalize to input device's reported input range (EVIOCGABS) */
-                            item->mouse_y = events[i].value;
+                            item->mouse_y = event->value;
                         }
                         break;
                     default:
@@ -408,49 +411,50 @@ void SDL_EVDEV_Poll(void)
                     }
                     break;
                 case EV_REL:
-                    switch (events[i].code) {
+                    switch (event->code) {
                     case REL_X:
                         if (item->relative_mouse) {
-                            item->mouse_x += events[i].value;
+                            item->mouse_x += event->value;
                         }
                         break;
                     case REL_Y:
                         if (item->relative_mouse) {
-                            item->mouse_y += events[i].value;
+                            item->mouse_y += event->value;
                         }
                         break;
                     case REL_WHEEL:
                         if (!item->high_res_wheel) {
-                            item->mouse_wheel += events[i].value;
+                            item->mouse_wheel += event->value;
                         }
                         break;
                     case REL_WHEEL_HI_RES:
                         SDL_assert(item->high_res_wheel);
-                        item->mouse_wheel += events[i].value;
+                        item->mouse_wheel += event->value;
                         break;
                     case REL_HWHEEL:
                         if (!item->high_res_hwheel) {
-                            item->mouse_hwheel += events[i].value;
+                            item->mouse_hwheel += event->value;
                         }
                         break;
                     case REL_HWHEEL_HI_RES:
                         SDL_assert(item->high_res_hwheel);
-                        item->mouse_hwheel += events[i].value;
+                        item->mouse_hwheel += event->value;
                         break;
                     default:
                         break;
                     }
                     break;
                 case EV_SYN:
-                    switch (events[i].code) {
+                    switch (event->code) {
                     case SYN_REPORT:
                         /* Send mouse axis changes together to ensure consistency and reduce event processing overhead */
                         if (item->mouse_x != 0 || item->mouse_y != 0) {
-                            SDL_SendMouseMotion(0, mouse->focus, (SDL_MouseID)item->fd, item->relative_mouse, item->mouse_x, item->mouse_y);
+                            SDL_SendMouseMotion(SDL_EVDEV_GetEventTimestamp(event), mouse->focus, (SDL_MouseID)item->fd, item->relative_mouse, item->mouse_x, item->mouse_y);
                             item->mouse_x = item->mouse_y = 0;
                         }
                         if (item->mouse_wheel != 0 || item->mouse_hwheel != 0) {
-                            SDL_SendMouseWheel(0, mouse->focus, (SDL_MouseID)item->fd,
+                            SDL_SendMouseWheel(SDL_EVDEV_GetEventTimestamp(event),
+                                               mouse->focus, (SDL_MouseID)item->fd,
                                                item->mouse_hwheel / (item->high_res_hwheel ? 120.0f : 1.0f),
                                                item->mouse_wheel / (item->high_res_wheel ? 120.0f : 1.0f),
                                                SDL_MOUSEWHEEL_NORMAL);
@@ -480,16 +484,16 @@ void SDL_EVDEV_Poll(void)
                              * be window-relative in that case. */
                             switch (item->touchscreen_data->slots[j].delta) {
                             case EVDEV_TOUCH_SLOTDELTA_DOWN:
-                                SDL_SendTouch(0, item->fd, item->touchscreen_data->slots[j].tracking_id, NULL, SDL_TRUE, norm_x, norm_y, norm_pressure);
+                                SDL_SendTouch(SDL_EVDEV_GetEventTimestamp(event), item->fd, item->touchscreen_data->slots[j].tracking_id, NULL, SDL_TRUE, norm_x, norm_y, norm_pressure);
                                 item->touchscreen_data->slots[j].delta = EVDEV_TOUCH_SLOTDELTA_NONE;
                                 break;
                             case EVDEV_TOUCH_SLOTDELTA_UP:
-                                SDL_SendTouch(0, item->fd, item->touchscreen_data->slots[j].tracking_id, NULL, SDL_FALSE, norm_x, norm_y, norm_pressure);
+                                SDL_SendTouch(SDL_EVDEV_GetEventTimestamp(event), item->fd, item->touchscreen_data->slots[j].tracking_id, NULL, SDL_FALSE, norm_x, norm_y, norm_pressure);
                                 item->touchscreen_data->slots[j].tracking_id = -1;
                                 item->touchscreen_data->slots[j].delta = EVDEV_TOUCH_SLOTDELTA_NONE;
                                 break;
                             case EVDEV_TOUCH_SLOTDELTA_MOVE:
-                                SDL_SendTouchMotion(0, item->fd, item->touchscreen_data->slots[j].tracking_id, NULL, norm_x, norm_y, norm_pressure);
+                                SDL_SendTouchMotion(SDL_EVDEV_GetEventTimestamp(event), item->fd, item->touchscreen_data->slots[j].tracking_id, NULL, norm_x, norm_y, norm_pressure);
                                 item->touchscreen_data->slots[j].delta = EVDEV_TOUCH_SLOTDELTA_NONE;
                                 break;
                             default:
@@ -870,6 +874,22 @@ static int SDL_EVDEV_device_removed(const char *dev_path)
     return -1;
 }
 
+Uint64 SDL_EVDEV_GetEventTimestamp(struct input_event *event)
+{
+    Uint64 timestamp;
+
+    /* The kernel internally has nanosecond timestamps, but converts it
+       to microseconds when delivering the events */
+    timestamp = event->time.tv_sec;
+    timestamp *= SDL_NS_PER_SECOND;
+    timestamp += SDL_US_TO_NS(event->time.tv_usec);
+
+    /* Let's assume for now that we're using the same time base */
+    timestamp -= SDL_GetTickStartNS();
+
+    return timestamp;
+}
+
 #endif /* SDL_INPUT_LINUXEV */
 
 /* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/core/linux/SDL_evdev.h b/src/core/linux/SDL_evdev.h
index 005ec0ee3a0a..6c0abe17f818 100644
--- a/src/core/linux/SDL_evdev.h
+++ b/src/core/linux/SDL_evdev.h
@@ -26,9 +26,12 @@
 
 #ifdef SDL_INPUT_LINUXEV
 
+struct input_event;
+
 extern int SDL_EVDEV_Init(void);
 extern void SDL_EVDEV_Quit(void);
 extern void SDL_EVDEV_Poll(void);
+extern Uint64 SDL_EVDEV_GetEventTimestamp(struct input_event *event);
 
 #endif /* SDL_INPUT_LINUXEV */
 
diff --git a/src/joystick/SDL_gamecontroller.c b/src/joystick/SDL_gamecontroller.c
index f027186306b6..0d62e24c90f2 100644
--- a/src/joystick/SDL_gamecontroller.c
+++ b/src/joystick/SDL_gamecontroller.c
@@ -206,8 +206,8 @@ static void SDLCALL SDL_GameControllerIgnoreDevicesExceptChanged(void *userdata,
 }
 
 static ControllerMapping_t *SDL_PrivateAddMappingForGUID(SDL_JoystickGUID jGUID, const char *mappingString, SDL_bool *existing, SDL_ControllerMappingPriority priority);
-static int SDL_PrivateGameControllerAxis(SDL_GameController *gamecontroller, SDL_GameControllerAxis axis, Sint16 value);
-static int SDL_PrivateGameControllerButton(SDL_GameController *gamecontroller, SDL_GameControllerButton button, Uint8 state);
+static int SDL_PrivateGameControllerAxis(Uint64 timestamp, SDL_GameController *gamecontroller, SDL_GameControllerAxis axis, Sint16 value);
+static int SDL_PrivateGameControllerButton(Uint64 timestamp, SDL_GameController *gamecontroller, SDL_GameControllerButton button, Uint8 state);
 
 static SDL_bool HasSameOutput(SDL_ExtendedGameControllerBind *a, SDL_ExtendedGameControllerBind *b)
 {
@@ -222,16 +222,16 @@ static SDL_bool HasSameOutput(SDL_ExtendedGameControllerBind *a, SDL_ExtendedGam
     }
 }
 
-static void ResetOutput(SDL_GameController *gamecontroller, SDL_ExtendedGameControllerBind *bind)
+static void ResetOutput(Uint64 timestamp, SDL_GameController *gamecontroller, SDL_ExtendedGameControllerBind *bind)
 {
     if (bind->outputType == SDL_CONTROLLER_BINDTYPE_AXIS) {
-        SDL_PrivateGameControllerAxis(gamecontroller, bind->output.axis.axis, 0);
+        SDL_PrivateGameControllerAxis(timestamp, gamecontroller, bind->output.axis.axis, 0);
     } else {
-        SDL_PrivateGameControllerButton(gamecontroller, bind->output.button, SDL_RELEASED);
+        SDL_PrivateGameControllerButton(timestamp, gamecontroller, bind->output.button, SDL_RELEASED);
     }
 }
 
-static void HandleJoystickAxis(SDL_GameController *gamecontroller, int axis, int value)
+static void HandleJoystickAxis(Uint64 timestamp, SDL_GameController *gamecontroller, int axis, int value)
 {
     int i;
     SDL_ExtendedGameControllerBind *last_match;
@@ -262,7 +262,7 @@ static void HandleJoystickAxis(SDL_GameController *gamecontroller, int axis, int
 
     if (last_match && (match == NULL || !HasSameOutput(last_match, match))) {
         /* Clear the last input that this axis generated */
-        ResetOutput(gamecontroller, last_match);
+        ResetOutput(timestamp, gamecontroller, last_match);
     }
 
     if (match) {
@@ -271,7 +271,7 @@ static void HandleJoystickAxis(SDL_GameController *gamecontroller, int axis, int
                 float normalized_value = (float)(value - match->input.axis.axis_min) / (match->input.axis.axis_max - match->input.axis.axis_min);
                 value = match->output.axis.axis_min + (int)(normalized_value * (match->output.axis.axis_max - match->output.axis.axis_min));
             }
-            SDL_PrivateGameControllerAxis(gamecontroller, match->output.axis.axis, (Sint16)value);
+            SDL_PrivateGameControllerAxis(timestamp, gamecontroller, match->output.axis.axis, (Sint16)value);
         } else {
             Uint8 state;
             int threshold = match->input.axis.axis_min + (match->input.axis.axis_max - match->input.axis.axis_min) / 2;
@@ -280,13 +280,13 @@ static void HandleJoystickAxis(SDL_GameController *gamecontroller, int axis, int
             } else {
                 state = (value >= threshold) ? SDL_PRESSED : SDL_RELEASED;
             }
-            SDL_PrivateGameControllerButton(gamecontroller, match->output.button, state);
+            SDL_PrivateGameControllerButton(timestamp, gamecontroller, match->output.button, state);
         }
     }
     gamecontroller->last_match_axis[axis] = match;
 }
 
-static void HandleJoystickButton(SDL_GameController *gamecontroller, int button, Uint8 state)
+static void HandleJoystickButton(Uint64 timestamp, SDL_GameController *gamecontroller, int button, Uint8 state)
 {
     int i;
 
@@ -298,16 +298,16 @@ static void HandleJoystickButton(SDL_GameController *gamecontroller, int button,
             button == binding->input.button) {
             if (binding->outputType == SDL_CONTROLLER_BINDTYPE_AXIS) {
                 int value = state ? binding->output.axis.axis_max : binding->output.axis.axis_min;
-                SDL_PrivateGameControllerAxis(gamecontroller, binding->output.axis.axis, (Sint16)value);
+                SDL_PrivateGameControllerAxis(timestamp, gamecontroller, binding->output.axis.axis, (Sint16)value);
             } else {
-                SDL_PrivateGameControllerButton(gamecontroller, binding->output.button, state);
+                SDL_PrivateGameControllerButton(timestamp, gamecontroller, binding->output.button, state);
             }
             break;
         }
     }
 }
 
-static void HandleJoystickHat(SDL_GameController *gamecontroller, int hat, Uint8 value)
+static void HandleJoystickHat(Uint64 timestamp, SDL_GameController *gamecontroller, int hat, Uint8 value)
 {
     int i;
     Uint8 last_mask, changed_mask;
@@ -322,12 +322,12 @@ static void HandleJoystickHat(SDL_GameController *gamecontroller, int hat, Uint8
             if ((changed_mask & binding->input.hat.hat_mask) != 0) {
                 if (value & binding->input.hat.hat_mask) {
                     if (binding->outputType == SDL_CONTROLLER_BINDTYPE_AXIS) {
-                        SDL_PrivateGameControllerAxis(gamecontroller, binding->output.axis.axis, (Sint16)binding->output.axis.axis_max);
+                        SDL_PrivateGameControllerAxis(timestamp, gamecontroller, binding->output.axis.axis, (Sint16)binding->output.axis.axis_max);
                     } else {
-                        SDL_PrivateGameControllerButton(gamecontroller, binding->output.button, SDL_PRESSED);
+                        SDL_PrivateGameControllerButton(timestamp, gamecontroller, binding->output.button, SDL_PRESSED);
                     }
                 } else {
-                    ResetOutput(gamecontroller, binding);
+                    ResetOutput(timestamp, gamecontroller, binding);
                 }
             }
         }
@@ -345,18 +345,19 @@ static void RecenterGameController(SDL_GameController *gamecontroller)
 {
     SDL_GameControllerButton button;
     SDL_GameControllerAxis axis;
+    Uint64 timestamp = SDL_GetTicksNS();
 
     CHECK_GAMECONTROLLER_MAGIC(gamecontroller, );
 
     for (button = (SDL_GameControllerButton)0; button < SDL_CONTROLLER_BUTTON_MAX; button++) {
         if (SDL_GameControllerGetButton(gamecontroller, button)) {
-            SDL_PrivateGameControllerButton(gamecontroller, button, SDL_RELEASED);
+            SDL_PrivateGameControllerButton(timestamp, gamecontroller, button, SDL_RELEASED);
         }
     }
 
     for (axis = (SDL_GameControllerAxis)0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) {
         if (SDL_GameControllerGetAxis(gamecontroller, axis) != 0) {
-            SDL_PrivateGameControllerAxis(gamecontroller, axis, 0);
+            SDL_PrivateGameControllerAxis(timestamp, gamecontroller, axis, 0);
         }
     }
 }
@@ -372,7 +373,7 @@ static int SDLCALL SDL_GameControllerEventWatcher(void *userdata, SDL_Event *eve
         SDL_GameController *controllerlist = SDL_gamecontrollers;
         while (controllerlist) {
             if (controllerlist->joystick->instance_id == event->jaxis.which) {
-                HandleJoystickAxis(controllerlist, event->jaxis.axis, event->jaxis.value);
+                HandleJoystickAxis(event->common.timestamp, controllerlist, event->jaxis.axis, event->jaxis.value);
                 break;
             }
             controllerlist = controllerlist->next;
@@ -384,7 +385,7 @@ static int SDLCALL SDL_GameControllerEventWatcher(void *userdata, SDL_Event *eve
         SDL_GameController *controllerlist = SDL_gamecontrollers;
         while (controllerlist) {
             if (controllerlist->joystick->instance_id == event->jbutton.which) {
-                HandleJoystickButton(controllerlist, event->jbutton.button, event->jbutton.state);
+                HandleJoystickButton(event->common.timestamp, controllerlist, event->jbutton.button, event->jbutton.state);
                 break;
             }
             controllerlist = controllerlist->next;
@@ -395,7 +396,7 @@ static int SDLCALL SDL_GameControllerEventWatcher(void *userdata, SDL_Event *eve
         SDL_GameController *controllerlist = SDL_gamecontrollers;
         while (controllerlist) {
             if (controllerlist->joystick->instance_id == event->jhat.which) {
-                HandleJoystickHat(controllerlist, event->jhat.hat, event->jhat.value);
+                HandleJoystickHat(event->common.timestamp, controllerlist, event->jhat.hat, event->jhat.value);
                 break;
             }
             controllerlist = controllerlist->next;
@@ -2857,7 +2858,7 @@ void SDL_GameControllerQuitMappings(void)
 /*
  * Event filter to transform joystick events into appropriate game controller ones
  */
-static int SDL_PrivateGameControllerAxis(SDL_GameController *gamecontroller, SDL_GameControllerAxis axis, Sint16 value)
+static int SDL_PrivateGameControllerAxis(Uint64 timestamp, SDL_GameController *gamecontroller, SDL_GameControllerAxis axis, Sint16 value)
 {
     int posted;
 
@@ -2869,7 +2870,7 @@ static int SDL_PrivateGameControllerAxis(SDL_GameController *gamecontroller, SDL
     if (SDL_GetEventState(SDL_CONTROLLERAXISMOTION) == SDL_ENABLE) {
         SDL_Event event;
         event.type = SDL_CONTROLLERAXISMOTION;
-        event.common.timestamp = 0;
+        event.common.timestamp = timestamp;
         event.caxis.which = gamecontroller->joystick->instance_id;
         event.caxis.axis = axis;
         event.caxis.value = value;
@@ -2882,7 +2883,7 @@ static int SDL_PrivateGameControllerAxis(SDL_GameController *gamecontroller, SDL
 /*
  * Event filter to transform joystick events into appropriate game controller ones
  */
-static int SDL_PrivateGameControllerButton(SDL_GameController *gamecontroller, SDL_GameControllerButton button, Uint8 state)
+static int SDL_PrivateGameControllerButton(Uint64 timestamp, SDL_GameController *gamecontroller, SDL_GameControllerButton button, Uint8 state)
 {
     int posted;
 #if !SDL_EVENTS_DISABLED
@@ -2905,7 +2906,6 @@ static int SDL_PrivateGameControllerButton(SDL_GameController *gamecontroller, S
         /* Invalid state -- bail */
         return 0;
     }
-    event.common.timestamp = 0;
 #endif /* !SDL_EVENTS_DISABLED */
 
     if (button == SDL_CONTROLLER_BUTTON_GUIDE) {
@@ -2930,6 +2930,7 @@ static int SDL_PrivateGameControllerButton(SDL_GameController *gamecontroller, S
     posted = 0;
 #if !SDL_EVENTS_DISABLED
     if (SDL_GetEventState(event.type) == SDL_ENABLE) {
+        event.common.timestamp = timestamp;
         event.cbutton.which = gamecontroller->joystick->instance_id;
         event.cbutton.button = button;
         event.cbutton.state = state;
@@ -2986,7 +2987,7 @@ void SDL_GameControllerHandleDelayedGuideButton(SDL_Joystick *joystick)
     SDL_GameController *controllerlist = SDL_gamecontrollers;
     while (controllerlist) {
         if (controllerlist->joystick == joystick) {
-            SDL_PrivateGameControllerButton(controllerlist, SDL_CONTROLLER_BUTTON_GUIDE, SDL_RELEASED);
+            SDL_PrivateGameControllerButton(0, controllerlist, SDL_CONTROLLER_BUTTON_GUIDE, SDL_RELEASED);
             break;
         }
         controllerlist = controllerlist->next;
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index d46f32438cce..25bc84109b76 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -1380,29 +1380,30 @@ static void UpdateEventsForDeviceRemoval(int device_index, Uint32 type)
 void SDL_PrivateJoystickForceRecentering(SDL_Joystick *joystick)
 {
     int i, j;
+    Uint64 timestamp = SDL_GetTicksNS();
 
     CHECK_JOYSTICK_MAGIC(joystick, );
 
     /* Tell the app that everything is centered/unpressed... */
     for (i = 0; i < joystick->naxes; i++) {
         if (joystick->axes[i].has_initial_value) {
-            SDL_PrivateJoystickAxis(joystick, i, joystick->axes[i].zero);
+            SDL_PrivateJoystickAxis(timestamp, joystick, i, joystick->axes[i].zero);
         }
     }
 
     for (i = 0; i < joystick->nbuttons; i++) {
-        SDL_PrivateJoystickButton(joystick, i, SDL_RELEASED);
+        SDL_PrivateJoystickButton(timestamp, joystick, i, SDL_RELEASED);
     }
 
     for (i = 0; i < joystick->nhats; i++) {
-        SDL_PrivateJoystickHat(joystick, i, SDL_HAT_CENTERED);
+        SDL_PrivateJoystickHat(timestamp, joystick, i, SDL_HAT_CENTERED);
     }
 
     for (i = 0; i < joystick->ntouchpads; i++) {
         SDL_JoystickTouchpadInfo *touchpad = &joystick->touchpads[i];
 
         for (j = 0; j < touchpad->nfingers; ++j) {
-            SDL_PrivateJoystickTouchpad(joystick, i, j, SDL_RELEASED, 0.0f, 0.0f, 0.0f);
+            SDL_PrivateJoystickTouchpad(timestamp, joystick, i, j, SDL_RELEASED, 0.0f, 0.0f, 0.0f);
         }
     }
 }
@@ -1449,7 +1450,7 @@ void SDL_PrivateJoystickRemoved(SDL_JoystickID device_instance)
     }
 }
 
-int SDL_PrivateJoystickAxis(S

(Patch may be truncated, please check the link at the top of this post.)