SDL: Added automatic gamepad mapping for the GameInput driver

From 8f0f14c31227d6635eb01e901b6e26c9db276382 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 17 Feb 2024 17:41:30 -0800
Subject: [PATCH] Added automatic gamepad mapping for the GameInput driver

---
 src/joystick/gdk/SDL_gameinputjoystick.c | 145 ++++++++++++++++-------
 1 file changed, 105 insertions(+), 40 deletions(-)

diff --git a/src/joystick/gdk/SDL_gameinputjoystick.c b/src/joystick/gdk/SDL_gameinputjoystick.c
index 64e444f659ca..b04d2447d41f 100644
--- a/src/joystick/gdk/SDL_gameinputjoystick.c
+++ b/src/joystick/gdk/SDL_gameinputjoystick.c
@@ -54,7 +54,6 @@ typedef struct joystick_hwdata
 {
     GAMEINPUT_InternalDevice *devref;
     GameInputRumbleParams rumbleParams;
-    Uint64 lastTimestamp;
 } GAMEINPUT_InternalJoystickHwdata;
 
 
@@ -453,52 +452,39 @@ static int GAMEINPUT_JoystickSetSensorsEnabled(SDL_Joystick *joystick, SDL_bool
 static void GAMEINPUT_JoystickUpdate(SDL_Joystick *joystick)
 {
     static WORD s_XInputButtons[] = {
-        GameInputGamepadA, GameInputGamepadB, GameInputGamepadX, GameInputGamepadY,
-        GameInputGamepadLeftShoulder, GameInputGamepadRightShoulder, GameInputGamepadView, GameInputGamepadMenu,
-        GameInputGamepadLeftThumbstick, GameInputGamepadRightThumbstick,
-        0 /* Guide button is not supported on Xbox so ignore that... */
+        GameInputGamepadA,                  /* SDL_GAMEPAD_BUTTON_SOUTH */
+        GameInputGamepadB,                  /* SDL_GAMEPAD_BUTTON_EAST */
+        GameInputGamepadX,                  /* SDL_GAMEPAD_BUTTON_WEST */
+        GameInputGamepadY,                  /* SDL_GAMEPAD_BUTTON_NORTH */
+        GameInputGamepadView,               /* SDL_GAMEPAD_BUTTON_BACK */
+        0, /* The guide button is not available */
+        GameInputGamepadMenu,               /* SDL_GAMEPAD_BUTTON_START */
+        GameInputGamepadLeftThumbstick,     /* SDL_GAMEPAD_BUTTON_LEFT_STICK */
+        GameInputGamepadRightThumbstick,    /* SDL_GAMEPAD_BUTTON_RIGHT_STICK */
+        GameInputGamepadLeftShoulder,       /* SDL_GAMEPAD_BUTTON_LEFT_SHOULDER */
+        GameInputGamepadRightShoulder,      /* SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER */
     };
     Uint8 btnidx = 0, btnstate = 0, hat = 0;
     GAMEINPUT_InternalJoystickHwdata *hwdata = joystick->hwdata;
     IGameInputDevice *device = hwdata->devref->device;
     IGameInputReading *reading = NULL;
-    uint64_t ts = 0;
+    Uint64 timestamp = SDL_GetTicksNS();
     GameInputGamepadState state;
-    HRESULT hR = IGameInput_GetCurrentReading(g_pGameInput,
-        GameInputKindGamepad,
-        device,
-        &reading
-    );
+    HRESULT hR;
+
 
+    hR = IGameInput_GetCurrentReading(g_pGameInput, GameInputKindGamepad, device, &reading);
     if (FAILED(hR)) {
         /* don't SetError here since there can be a legitimate case when there's no reading avail */
         return;
     }
 
-    /* GDKX private docs for GetTimestamp: "The microsecond timestamp describing when the input was made." */
-    /* SDL expects a nanosecond timestamp, so I guess US_TO_NS should be used here? */
-    ts = SDL_US_TO_NS(IGameInputReading_GetTimestamp(reading));
-
-    if (((!hwdata->lastTimestamp) || (ts != hwdata->lastTimestamp)) && IGameInputReading_GetGamepadState(reading, &state)) {
-        /* `state` is now valid */
-
-#define tosint16(_TheValue) ((Sint16)(((_TheValue) < 0.0f) ? ((_TheValue) * 32768.0f) : ((_TheValue) * 32767.0f)))
-        SDL_SendJoystickAxis(ts, joystick, 0, tosint16(state.leftThumbstickX));
-        SDL_SendJoystickAxis(ts, joystick, 1, tosint16(state.leftThumbstickY));
-        SDL_SendJoystickAxis(ts, joystick, 2, tosint16(state.leftTrigger));
-        SDL_SendJoystickAxis(ts, joystick, 3, tosint16(state.rightThumbstickX));
-        SDL_SendJoystickAxis(ts, joystick, 4, tosint16(state.rightThumbstickY));
-        SDL_SendJoystickAxis(ts, joystick, 5, tosint16(state.rightTrigger));
-#undef tosint16
+    /* FIXME: See if we can get the delta between the reading timestamp and current time and apply the offset to timestamp */
 
-        for (btnidx = 0; btnidx < (Uint8)SDL_arraysize(s_XInputButtons); ++btnidx) {
-            if (s_XInputButtons[btnidx] == 0) {
-                btnstate = SDL_RELEASED;
-            } else {
-                btnstate = (state.buttons & s_XInputButtons[btnidx]) ? SDL_PRESSED : SDL_RELEASED;
-            }
-
-            SDL_SendJoystickButton(ts, joystick, btnidx, btnstate);
+    if (IGameInputReading_GetGamepadState(reading, &state)) {
+        for (btnidx = 0; btnidx < SDL_arraysize(s_XInputButtons); ++btnidx) {
+            btnstate = (state.buttons & s_XInputButtons[btnidx]) ? SDL_PRESSED : SDL_RELEASED;
+            SDL_SendJoystickButton(timestamp, joystick, btnidx, btnstate);
         }
 
         if (state.buttons & GameInputGamepadDPadUp) {
@@ -513,15 +499,32 @@ static void GAMEINPUT_JoystickUpdate(SDL_Joystick *joystick)
         if (state.buttons & GameInputGamepadDPadRight) {
             hat |= SDL_HAT_RIGHT;
         }
-        SDL_SendJoystickHat(ts, joystick, 0, hat);
+        SDL_SendJoystickHat(timestamp, joystick, 0, hat);
 
-        /* Xbox doesn't let you obtain the power level, pretend we're always full */
-        SDL_SendJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL);
-
-        hwdata->lastTimestamp = ts;
+#define CONVERT_AXIS(v) (Sint16)(((v) < 0.0f) ? ((v)*32768.0f) : ((v)*32767.0f))
+        SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, CONVERT_AXIS(state.leftThumbstickX));
+        SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, CONVERT_AXIS(-state.leftThumbstickY));
+        SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, CONVERT_AXIS(state.rightThumbstickX));
+        SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, CONVERT_AXIS(-state.rightThumbstickY));
+#undef CONVERT_AXIS
+#define CONVERT_TRIGGER(v) (Sint16)((v)*65535.0f - 32768.0f)
+        SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, CONVERT_TRIGGER(state.leftTrigger));
+        SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, CONVERT_TRIGGER(state.rightTrigger));
+#undef CONVERT_TRIGGER
     }
 
     IGameInputReading_Release(reading);
+
+#if 0
+    /* FIXME: We can poll this at a much lower rate */
+    GameInputBatteryState battery_state;
+    SDL_zero(battery_state);
+    IGameInputDevice_GetBatteryState(device, &battery_state);
+
+
+    /* Xbox doesn't let you obtain the power level, pretend we're always full */
+    SDL_SendJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL);
+#endif
 }
 
 static void GAMEINPUT_JoystickClose(SDL_Joystick* joystick)
@@ -561,7 +564,69 @@ static void GAMEINPUT_JoystickQuit(void)
 
 static SDL_bool GAMEINPUT_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
 {
-    return SDL_FALSE;
+    out->a.kind = EMappingKind_Button;
+    out->a.target = SDL_GAMEPAD_BUTTON_SOUTH;
+
+    out->b.kind = EMappingKind_Button;
+    out->b.target = SDL_GAMEPAD_BUTTON_EAST;
+
+    out->x.kind = EMappingKind_Button;
+    out->x.target = SDL_GAMEPAD_BUTTON_WEST;
+
+    out->y.kind = EMappingKind_Button;
+    out->y.target = SDL_GAMEPAD_BUTTON_NORTH;
+
+    out->back.kind = EMappingKind_Button;
+    out->back.target = SDL_GAMEPAD_BUTTON_BACK;
+
+    /* The guide button isn't available, so don't map it */
+
+    out->start.kind = EMappingKind_Button;
+    out->start.target = SDL_GAMEPAD_BUTTON_START;
+
+    out->leftstick.kind = EMappingKind_Button;
+    out->leftstick.target = SDL_GAMEPAD_BUTTON_LEFT_STICK;
+
+    out->rightstick.kind = EMappingKind_Button;
+    out->rightstick.target = SDL_GAMEPAD_BUTTON_RIGHT_STICK;
+
+    out->leftshoulder.kind = EMappingKind_Button;
+    out->leftshoulder.target = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER;
+
+    out->rightshoulder.kind = EMappingKind_Button;
+    out->rightshoulder.target = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER;
+
+    out->dpup.kind = EMappingKind_Hat;
+    out->dpup.target = SDL_HAT_UP;
+
+    out->dpdown.kind = EMappingKind_Hat;
+    out->dpdown.target = SDL_HAT_DOWN;
+
+    out->dpleft.kind = EMappingKind_Hat;
+    out->dpleft.target = SDL_HAT_LEFT;
+
+    out->dpright.kind = EMappingKind_Hat;
+    out->dpright.target = SDL_HAT_RIGHT;
+
+    out->leftx.kind = EMappingKind_Axis;
+    out->leftx.target = SDL_GAMEPAD_AXIS_LEFTX;
+
+    out->lefty.kind = EMappingKind_Axis;
+    out->lefty.target = SDL_GAMEPAD_AXIS_LEFTY;
+
+    out->rightx.kind = EMappingKind_Axis;
+    out->rightx.target = SDL_GAMEPAD_AXIS_RIGHTX;
+
+    out->righty.kind = EMappingKind_Axis;
+    out->righty.target = SDL_GAMEPAD_AXIS_RIGHTY;
+
+    out->lefttrigger.kind = EMappingKind_Axis;
+    out->lefttrigger.target = SDL_GAMEPAD_AXIS_LEFT_TRIGGER;
+
+    out->righttrigger.kind = EMappingKind_Axis;
+    out->righttrigger.target = SDL_GAMEPAD_AXIS_RIGHT_TRIGGER;
+
+    return SDL_TRUE;
 }