SDL: GameInput backend for SDL (Gamepad-only for now)

From 534f753e2025040f25b56524bbe96ae26cabe177 Mon Sep 17 00:00:00 2001
From: Nikita Krapivin <[EMAIL REDACTED]>
Date: Thu, 25 Jan 2024 01:26:01 +0500
Subject: [PATCH] GameInput backend for SDL (Gamepad-only for now)

---
 VisualC-GDK/SDL/SDL.vcxproj                   |   7 +-
 .../build_config/SDL_build_config_wingdk.h    |  10 +
 include/build_config/SDL_build_config_xbox.h  |  12 +-
 src/core/windows/SDL_xinput.c                 |   4 +
 src/joystick/SDL_joystick.c                   |   3 +
 src/joystick/SDL_sysjoystick.h                |   1 +
 src/joystick/gdk/SDL_gameinputjoystick.cpp    | 582 ++++++++++++++++++
 src/joystick/gdk/SDL_gameinputjoystick_c.h    |  58 ++
 8 files changed, 674 insertions(+), 3 deletions(-)
 create mode 100644 src/joystick/gdk/SDL_gameinputjoystick.cpp
 create mode 100644 src/joystick/gdk/SDL_gameinputjoystick_c.h

diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index e070c31ef76e..cec3972f0f9c 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -438,6 +438,7 @@
     <ClInclude Include="..\..\src\joystick\windows\SDL_rawinputjoystick_c.h" />
     <ClInclude Include="..\..\src\joystick\windows\SDL_windowsjoystick_c.h" />
     <ClInclude Include="..\..\src\joystick\windows\SDL_xinputjoystick_c.h" />
+    <ClInclude Include="..\..\src\joystick\gdk\SDL_gameinputjoystick_c.h" />
     <ClInclude Include="..\..\src\libm\math_libm.h" />
     <ClInclude Include="..\..\src\libm\math_private.h" />
     <ClInclude Include="..\..\src\locale\SDL_syslocale.h" />
@@ -576,9 +577,7 @@
     <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_lsx.h" />
     <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_lsx_func.h" />
     <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_sse.h" />
-    <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_sse_func.h" />
     <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_std.h" />
-    <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_std_func.h" />
     <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_sse_func.h" />
     <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_std_func.h" />
     <ClCompile Include="..\..\src\atomic\SDL_atomic.c" />
@@ -691,6 +690,10 @@
       <CompileAs Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Xbox.Scarlett.x64'">CompileAsCpp</CompileAs>
       <CompileAs Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Xbox.XboxOne.x64'">CompileAsCpp</CompileAs>
     </ClCompile>
+    <ClCompile Include="..\..\src\joystick\gdk\SDL_gameinputjoystick.cpp">
+      <PrecompiledHeaderOutputFile Condition="'$(Configuration)|$(Platform)'=='Debug|Gaming.Desktop.x64'">$(IntDir)$(TargetName)_cpp.pch</PrecompiledHeaderOutputFile>
+      <PrecompiledHeaderOutputFile Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Desktop.x64'">$(IntDir)$(TargetName)_cpp.pch</PrecompiledHeaderOutputFile>
+    </ClCompile>
     <ClCompile Include="..\..\src\libm\e_atan2.c" />
     <ClCompile Include="..\..\src\libm\e_exp.c" />
     <ClCompile Include="..\..\src\libm\e_fmod.c" />
diff --git a/include/build_config/SDL_build_config_wingdk.h b/include/build_config/SDL_build_config_wingdk.h
index 2f25b597e549..d856d2323e08 100644
--- a/include/build_config/SDL_build_config_wingdk.h
+++ b/include/build_config/SDL_build_config_wingdk.h
@@ -176,6 +176,16 @@
 #define SDL_JOYSTICK_XINPUT 1
 #define SDL_HAPTIC_DINPUT   1
 
+/* Native GameInput: */
+/*#define SDL_JOYSTICK_GAMEINPUT 1*/
+#if defined(SDL_JOYSTICK_GAMEINPUT) && (defined(SDL_JOYSTICK_XINPUT) || defined(SDL_JOYSTICK_DINPUT))
+#error "GameInput cannot co-exist, choose one."
+#endif /* defined(SDL_JOYSTICK_GAMEINPUT) && (defined(SDL_JOYSTICK_XINPUT) || defined(SDL_JOYSTICK_DINPUT)) */
+#if defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT
+/* TODO: Implement proper haptics for GameInput! */
+#define SDL_HAPTIC_DUMMY 1
+#endif /* defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT */
+
 /* Enable the sensor driver */
 #ifdef HAVE_SENSORSAPI_H
 #define SDL_SENSOR_WINDOWS  1
diff --git a/include/build_config/SDL_build_config_xbox.h b/include/build_config/SDL_build_config_xbox.h
index 2f0f090fe772..c5198aeec763 100644
--- a/include/build_config/SDL_build_config_xbox.h
+++ b/include/build_config/SDL_build_config_xbox.h
@@ -173,7 +173,17 @@
 #ifdef HAVE_WINDOWS_GAMING_INPUT_H
 #define SDL_JOYSTICK_WGI    1
 #endif
-#define SDL_JOYSTICK_XINPUT 1
+/* This is XInputOnGameInput for GDK platforms: */
+/*#define SDL_JOYSTICK_XINPUT 1*/
+/* Native GameInput: */
+#define SDL_JOYSTICK_GAMEINPUT 1
+#if defined(SDL_JOYSTICK_GAMEINPUT) && (defined(SDL_JOYSTICK_XINPUT) || defined(SDL_JOYSTICK_DINPUT))
+#error "GameInput cannot co-exist, choose one."
+#endif /* defined(SDL_JOYSTICK_GAMEINPUT) && (defined(SDL_JOYSTICK_XINPUT) || defined(SDL_JOYSTICK_DINPUT)) */
+#if defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT
+/* TODO: Implement proper haptics for GameInput! */
+#define SDL_HAPTIC_DUMMY 1
+#endif /* defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT */
 /*#define SDL_HAPTIC_DINPUT   1*/
 
 /* Enable the sensor driver */
diff --git a/src/core/windows/SDL_xinput.c b/src/core/windows/SDL_xinput.c
index 13c06bde4dbc..fdeed7085449 100644
--- a/src/core/windows/SDL_xinput.c
+++ b/src/core/windows/SDL_xinput.c
@@ -20,6 +20,8 @@
 */
 #include "SDL_internal.h"
 
+#ifndef SDL_JOYSTICK_GAMEINPUT
+
 #include "SDL_xinput.h"
 
 /* Set up for C function definitions, even when using C++ */
@@ -142,3 +144,5 @@ void WIN_UnloadXInputDLL(void)
 #ifdef __cplusplus
 }
 #endif
+
+#endif /* !SDL_JOYSTICK_GAMEINPUT */
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index bbc553b58b4c..c1c1a3843cae 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -55,6 +55,9 @@ static SDL_JoystickDriver *SDL_joystick_drivers[] = {
 #ifdef SDL_JOYSTICK_RAWINPUT /* Before WINDOWS_ driver, as WINDOWS wants to check if this driver is handling things */
     &SDL_RAWINPUT_JoystickDriver,
 #endif
+#ifdef SDL_JOYSTICK_GAMEINPUT /* Before WINDOWS_ driver, as GameInput takes priority over XInputOnGameInput for GDK platforms */
+    &SDL_GAMEINPUT_JoystickDriver,
+#endif
 #if defined(SDL_JOYSTICK_DINPUT) || defined(SDL_JOYSTICK_XINPUT) /* Before WGI driver, as WGI wants to check if this driver is handling things */
     &SDL_WINDOWS_JoystickDriver,
 #endif
diff --git a/src/joystick/SDL_sysjoystick.h b/src/joystick/SDL_sysjoystick.h
index 95544118da18..739d6973d132 100644
--- a/src/joystick/SDL_sysjoystick.h
+++ b/src/joystick/SDL_sysjoystick.h
@@ -245,6 +245,7 @@ extern SDL_JoystickDriver SDL_PS2_JoystickDriver;
 extern SDL_JoystickDriver SDL_PSP_JoystickDriver;
 extern SDL_JoystickDriver SDL_VITA_JoystickDriver;
 extern SDL_JoystickDriver SDL_N3DS_JoystickDriver;
+extern SDL_JoystickDriver SDL_GAMEINPUT_JoystickDriver;
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
diff --git a/src/joystick/gdk/SDL_gameinputjoystick.cpp b/src/joystick/gdk/SDL_gameinputjoystick.cpp
new file mode 100644
index 000000000000..1b4223d6ea72
--- /dev/null
+++ b/src/joystick/gdk/SDL_gameinputjoystick.cpp
@@ -0,0 +1,582 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_gameinputjoystick_c.h"
+
+#if defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Public APIs: GAMEINPUT_Joystick... */
+/* Private APIs: GAMEINPUT_InternalJoystick... */
+
+#include "../usb_ids.h"
+
+typedef struct GAMEINPUT_InternalDevice
+{
+    IGameInputDevice *device;
+    const char *deviceName;        /* this is a constant string literal */
+    SDL_JoystickGUID joystickGuid; /* generated by SDL. */
+    SDL_JoystickID instanceId;     /* generated by SDL. */
+    int playerIndex;
+    Uint32 caps;
+    char devicePath[(APP_LOCAL_DEVICE_ID_SIZE * 2) + 1];
+    bool isAdded, isDeleteRequested;
+} GAMEINPUT_InternalDevice;
+
+typedef struct GAMEINPUT_InternalList
+{
+    GAMEINPUT_InternalDevice **devices;
+    int count;
+} GAMEINPUT_InternalList;
+
+typedef struct joystick_hwdata
+{
+    GAMEINPUT_InternalDevice *devref;
+    GameInputRumbleParams rumbleParams;
+    Uint64 lastTimestamp;
+} GAMEINPUT_InternalJoystickHwdata;
+
+
+static GAMEINPUT_InternalList g_GameInputList = { NULL };
+static IGameInput *g_pGameInput = NULL;
+static GameInputCallbackToken g_GameInputCallbackToken = GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE;
+
+
+static int GAMEINPUT_InternalAddOrFind(IGameInputDevice *pDevice)
+{
+    GAMEINPUT_InternalDevice **devicelist = NULL;
+    GAMEINPUT_InternalDevice *elem = NULL;
+    const GameInputDeviceInfo *devinfo = NULL;
+    char tmpbuff[4];
+    int idx = 0;
+
+    if (!pDevice) {
+        return SDL_SetError("GAMEINPUT_InternalAddOrFind argument pDevice cannot be NULL");
+    }
+
+    devinfo = pDevice->GetDeviceInfo();
+    if (!devinfo) {
+        return SDL_SetError("GAMEINPUT_InternalAddOrFind GetDeviceInfo returned NULL");
+    }
+
+    for (idx = 0; idx < g_GameInputList.count; ++idx) {
+        elem = g_GameInputList.devices[idx];
+        if (elem && elem->device == pDevice) {
+            /* we're already added */
+            return idx;
+        }
+    }
+
+    elem = (GAMEINPUT_InternalDevice *)SDL_calloc(1, sizeof(*elem));
+    if (!elem) {
+        return SDL_OutOfMemory();
+    }
+
+    /* generate a device name */
+    for (idx = 0; idx < APP_LOCAL_DEVICE_ID_SIZE; ++idx) {
+        (void)SDL_snprintf(tmpbuff, SDL_arraysize(tmpbuff), "%02hhX", devinfo->deviceId.value[idx]);
+        (void)strncat_s(elem->devicePath, tmpbuff, SDL_arraysize(tmpbuff));
+    }
+
+    devicelist = (GAMEINPUT_InternalDevice **)SDL_realloc(g_GameInputList.devices, sizeof(elem) * (g_GameInputList.count + 1LL));
+    if (!devicelist) {
+        SDL_free(elem);
+        return SDL_OutOfMemory();
+    }
+
+    g_GameInputList.devices = devicelist;
+    pDevice->AddRef();
+    elem->device = pDevice;
+    elem->deviceName = "GameInput Gamepad";
+    elem->caps = 0;
+    if (devinfo->supportedRumbleMotors & (GameInputRumbleLowFrequency | GameInputRumbleHighFrequency)) {
+        elem->caps |= SDL_JOYSTICK_CAP_RUMBLE;
+    }
+    if (devinfo->supportedRumbleMotors & (GameInputRumbleLeftTrigger | GameInputRumbleRightTrigger)) {
+        elem->caps |= SDL_JOYSTICK_CAP_TRIGGER_RUMBLE;
+    }
+    elem->joystickGuid = SDL_CreateJoystickGUID(
+        SDL_HARDWARE_BUS_BLUETOOTH,
+        USB_VENDOR_MICROSOFT,
+        USB_PRODUCT_XBOX_SERIES_X_BLE,
+        1,
+        "GameInput",
+        "Gamepad",
+        'g',
+        0
+    );
+    elem->instanceId = SDL_GetNextObjectID();
+    g_GameInputList.devices[g_GameInputList.count] = elem;
+
+    /* finally increment the count and return */
+    return g_GameInputList.count++;
+}
+
+static int GAMEINPUT_InternalRemoveByIndex(int idx)
+{
+    GAMEINPUT_InternalDevice **devicelist = NULL;
+    int bytes = 0;
+
+    if (idx < 0 || idx >= g_GameInputList.count) {
+        return SDL_SetError("GAMEINPUT_InternalRemoveByIndex argument idx %d is out of range", idx);
+    }
+
+    g_GameInputList.devices[idx]->device->Release();
+
+    if (g_GameInputList.devices[idx]) {
+        SDL_free(g_GameInputList.devices[idx]);
+        g_GameInputList.devices[idx] = NULL;
+    }
+
+    if (g_GameInputList.count == 1) {
+        /* last element in the list, free the entire list then */
+        SDL_free(g_GameInputList.devices);
+        g_GameInputList.devices = NULL;
+    } else {
+        if (idx != g_GameInputList.count - 1) {
+            bytes = sizeof(*devicelist) * (g_GameInputList.count - idx);
+            SDL_memmove(&g_GameInputList.devices[idx], &g_GameInputList.devices[idx + 1], bytes);
+        }
+
+        devicelist = (GAMEINPUT_InternalDevice **)SDL_realloc(g_GameInputList.devices, sizeof(*devicelist) * (g_GameInputList.count - 1LL));
+        if (!devicelist) {
+            return SDL_OutOfMemory();
+        }
+
+        g_GameInputList.devices = devicelist;
+    }
+
+    /* decrement the count and return */
+    return g_GameInputList.count--;
+}
+
+static GAMEINPUT_InternalDevice *GAMEINPUT_InternalFindByIndex(int idx)
+{
+    if (idx < 0 || idx >= g_GameInputList.count) {
+        SDL_SetError("GAMEINPUT_InternalFindByIndex argument idx %d out of range", idx);
+        return NULL;
+    }
+
+    return g_GameInputList.devices[idx];
+}
+
+static void CALLBACK GAMEINPUT_InternalJoystickDeviceCallback(
+    _In_ GameInputCallbackToken callbackToken,
+    _In_ void* context,
+    _In_ IGameInputDevice* device,
+    _In_ uint64_t timestamp,
+    _In_ GameInputDeviceStatus currentStatus,
+    _In_ GameInputDeviceStatus previousStatus)
+{
+    int idx = 0;
+    GAMEINPUT_InternalDevice *elem = NULL;
+
+    if (currentStatus & GameInputDeviceConnected) {
+        GAMEINPUT_InternalAddOrFind(device);
+    } else {
+        for (idx = 0; idx < g_GameInputList.count; ++idx) {
+            elem = g_GameInputList.devices[idx];
+            if (elem && elem->device == device) {
+                /* will be deleted on the next Detect call */
+                elem->isDeleteRequested = true;
+                break;
+            }
+        }
+    }
+}
+
+static void GAMEINPUT_JoystickDetect(void);
+
+static int GAMEINPUT_JoystickInit(void)
+{
+    HRESULT hR;
+
+    if (!g_pGameInput) {
+        hR = GameInputCreate(&g_pGameInput);
+        if (FAILED(hR)) {
+            return SDL_SetError("GameInputCreate failure with HRESULT of %08X", hR);
+        }
+    }
+
+    hR = g_pGameInput->RegisterDeviceCallback(
+        nullptr,
+        GameInputKindGamepad,
+        GameInputDeviceConnected,
+        GameInputBlockingEnumeration,
+        nullptr,
+        GAMEINPUT_InternalJoystickDeviceCallback,
+        &g_GameInputCallbackToken
+    );
+    if (FAILED(hR)) {
+        return SDL_SetError("IGameInput::RegisterDeviceCallback failure with HRESULT of %08X", hR);
+    }
+
+    GAMEINPUT_JoystickDetect();
+
+    /* no need to free IGameInput on failure. */
+    return 0;
+}
+
+static int GAMEINPUT_JoystickGetCount(void)
+{
+    return g_GameInputList.count;
+}
+
+static void GAMEINPUT_JoystickDetect(void)
+{
+    int idx = 0;
+    GAMEINPUT_InternalDevice *elem = NULL;
+
+    for (idx = 0; idx < g_GameInputList.count; ++idx) {
+        elem = g_GameInputList.devices[idx];
+        if (!elem) {
+            continue;
+        }
+
+        if (!elem->isAdded) {
+            SDL_PrivateJoystickAdded(elem->instanceId);
+            elem->isAdded = true;
+        }
+
+        if (elem->isDeleteRequested || !(elem->device->GetDeviceStatus() & GameInputDeviceConnected)) {
+            SDL_PrivateJoystickRemoved(elem->instanceId);
+            GAMEINPUT_InternalRemoveByIndex(idx--);
+        }
+    }
+}
+
+static const char *GAMEINPUT_JoystickGetDeviceName(int device_index)
+{
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+
+    if (!elem) {
+        return NULL;
+    }
+
+    return elem->deviceName;
+}
+
+static const char *GAMEINPUT_JoystickGetDevicePath(int device_index)
+{
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+
+    if (!elem) {
+        return NULL;
+    }
+
+    /* APP_LOCAL_DEVICE_ID as a hex string, since it's required for some association callbacks */
+    return elem->devicePath;
+}
+
+static int GAMEINPUT_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index)
+{
+    /* Steamworks API is not available in GDK */
+    return -1;
+}
+
+static int GAMEINPUT_JoystickGetDevicePlayerIndex(int device_index)
+{
+    /*
+     * Okay, so, while XInput technically has player indicies,
+     * GameInput does not. It just dispatches a callback whenever a device is found.
+     * So if you're using true native GameInput (which this backend IS)
+     * you're meant to assign some index to a player yourself.
+     *
+     * GameMaker, for example, seems to do this in the order of plugging in.
+     * 
+     * Sorry for the trouble!
+     */
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+
+    if (!elem) {
+        return -1;
+    }
+
+    return elem->playerIndex;
+}
+
+static void GAMEINPUT_JoystickSetDevicePlayerIndex(int device_index, int player_index)
+{
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+
+    if (!elem) {
+        return;
+    }
+
+    elem->playerIndex = player_index;
+}
+
+static SDL_JoystickGUID GAMEINPUT_JoystickGetDeviceGUID(int device_index)
+{
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+
+    if (!elem) {
+        /* empty guid */
+        return { { 0 } };
+    }
+
+    return elem->joystickGuid;
+}
+
+static SDL_JoystickID GAMEINPUT_JoystickGetDeviceInstanceID(int device_index)
+{
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+
+    if (!elem) {
+        return 0;
+    }
+
+    return elem->instanceId;
+}
+
+static int GAMEINPUT_JoystickOpen(SDL_Joystick *joystick, int device_index)
+{
+    GAMEINPUT_InternalDevice *elem = GAMEINPUT_InternalFindByIndex(device_index);
+    GAMEINPUT_InternalJoystickHwdata *hwdata = NULL;
+
+    if (!elem) {
+        return -1;
+    }
+
+    hwdata = (GAMEINPUT_InternalJoystickHwdata *)SDL_calloc(1, sizeof(*hwdata));
+    if (!hwdata) {
+        return SDL_OutOfMemory();
+    }
+
+    hwdata->devref = elem;
+
+    joystick->hwdata = hwdata;
+    joystick->naxes = 6;
+    joystick->nbuttons = 11;
+    joystick->nhats = 1;
+
+    return 0;
+}
+
+static int GAMEINPUT_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
+{
+    /* don't check for caps here, since SetRumbleState doesn't return any result - we don't need to check it */
+    GAMEINPUT_InternalJoystickHwdata *hwdata = joystick->hwdata;
+    GameInputRumbleParams *params = &hwdata->rumbleParams;
+    params->lowFrequency = (float)low_frequency_rumble / (float)SDL_MAX_UINT16;
+    params->highFrequency = (float)high_frequency_rumble / (float)SDL_MAX_UINT16;
+    hwdata->devref->device->SetRumbleState(params);
+    return 0;
+}
+
+static int GAMEINPUT_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
+{
+    /* don't check for caps here, since SetRumbleState doesn't return any result - we don't need to check it */
+    GAMEINPUT_InternalJoystickHwdata *hwdata = joystick->hwdata;
+    GameInputRumbleParams *params = &hwdata->rumbleParams;
+    params->leftTrigger = (float)left_rumble / (float)SDL_MAX_UINT16;
+    params->rightTrigger = (float)right_rumble / (float)SDL_MAX_UINT16;
+    hwdata->devref->device->SetRumbleState(params);
+    return 0;
+}
+
+static Uint32 GAMEINPUT_JoystickGetCapabilities(SDL_Joystick *joystick)
+{
+    return joystick->hwdata->devref->caps;
+}
+
+static int GAMEINPUT_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
+{
+    return SDL_Unsupported();
+}
+
+static int GAMEINPUT_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)
+{
+    HRESULT hR = S_OK;
+    const GAMEINPUT_JoystickEffectData *effect = NULL;
+    GAMEINPUT_InternalJoystickHwdata *hwdata = joystick->hwdata;
+
+    if (!data || size != sizeof(GAMEINPUT_JoystickEffectData)) {
+        return SDL_SetError("GAMEINPUT_JoystickSendEffect invalid data or size");
+    }
+
+    effect = (const GAMEINPUT_JoystickEffectData *)data;
+    if (effect->type == GAMEINPUT_JoystickEffectDataType_HapticFeedback) {
+        hR = hwdata->devref->device->SetHapticMotorState(
+            effect->hapticFeedbackMotorIndex,
+            &effect->hapticFeedbackParams
+        );
+        if (FAILED(hR)) {
+            return SDL_SetError("IGameInputDevice::SetHapticMotorState failure with HRESULT of %08X", hR);
+        }
+    }
+
+    return 0;
+}
+
+static int GAMEINPUT_JoystickSetSensorsEnabled(SDL_Joystick *joystick, SDL_bool enabled)
+{
+    /* I am not sure what is this even supposed to do in case of GameInput... */
+    return 0;
+}
+
+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... */
+    };
+    Uint8 btnidx = 0, btnstate = 0, hat = 0;
+    GAMEINPUT_InternalJoystickHwdata *hwdata = joystick->hwdata;
+    IGameInputDevice *device = hwdata->devref->device;
+    IGameInputReading *reading = NULL;
+    uint64_t ts = 0;
+    GameInputGamepadState state;
+    HRESULT hR = g_pGameInput->GetCurrentReading(
+        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(reading->GetTimestamp());
+
+    if (((!hwdata->lastTimestamp) || (ts != hwdata->lastTimestamp)) && reading->GetGamepadState(&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
+
+        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 (state.buttons & GameInputGamepadDPadUp) {
+            hat |= SDL_HAT_UP;
+        }
+        if (state.buttons & GameInputGamepadDPadDown) {
+            hat |= SDL_HAT_DOWN;
+        }
+        if (state.buttons & GameInputGamepadDPadLeft) {
+            hat |= SDL_HAT_LEFT;
+        }
+        if (state.buttons & GameInputGamepadDPadRight) {
+            hat |= SDL_HAT_RIGHT;
+        }
+        SDL_SendJoystickHat(ts, 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;
+    }
+
+    reading->Release();
+}
+
+static void GAMEINPUT_JoystickClose(SDL_Joystick* joystick)
+{
+    SDL_free(joystick->hwdata);
+    joystick->hwdata = NULL;
+}
+
+static void GAMEINPUT_JoystickQuit(void)
+{
+    int idx;
+
+    if (!g_pGameInput) {
+        return;
+    }
+
+    /* free the callback */
+    g_pGameInput->UnregisterCallback(g_GameInputCallbackToken, /*timeoutInUs:*/ 10000);
+    g_GameInputCallbackToken = GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE;
+
+    /* free the list */
+    for (idx = 0; idx < g_GameInputList.count; ++idx) {
+        g_GameInputList.devices[idx]->device->Release();
+        SDL_free(g_GameInputList.devices[idx]);
+        g_GameInputList.devices[idx] = NULL;
+    }
+    SDL_free(g_GameInputList.devices);
+    g_GameInputList.devices = NULL;
+    g_GameInputList.count = 0;
+
+    g_pGameInput->Release();
+    g_pGameInput = NULL;
+}
+
+static SDL_bool GAMEINPUT_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
+{
+    return SDL_FALSE;
+}
+
+
+SDL_JoystickDriver SDL_GAMEINPUT_JoystickDriver =
+{
+    GAMEINPUT_JoystickInit,
+    GAMEINPUT_JoystickGetCount,
+    GAMEINPUT_JoystickDetect,
+    GAMEINPUT_JoystickGetDeviceName,
+    GAMEINPUT_JoystickGetDevicePath,
+    GAMEINPUT_JoystickGetDeviceSteamVirtualGamepadSlot,
+    GAMEINPUT_JoystickGetDevicePlayerIndex,
+    GAMEINPUT_JoystickSetDevicePlayerIndex,
+    GAMEINPUT_JoystickGetDeviceGUID,
+    GAMEINPUT_JoystickGetDeviceInstanceID,
+    GAMEINPUT_JoystickOpen,
+    GAMEINPUT_JoystickRumble,
+    GAMEINPUT_JoystickRumbleTriggers,
+    GAMEINPUT_JoystickGetCapabilities,
+    GAMEINPUT_JoystickSetLED,
+    GAMEINPUT_JoystickSendEffect,
+    GAMEINPUT_JoystickSetSensorsEnabled,
+    GAMEINPUT_JoystickUpdate,
+    GAMEINPUT_JoystickClose,
+    GAMEINPUT_JoystickQuit,
+    GAMEINPUT_JoystickGetGamepadMapping
+};
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT */
diff --git a/src/joystick/gdk/SDL_gameinputjoystick_c.h b/src/joystick/gdk/SDL_gameinputjoystick_c.h
new file mode 100644
index 000000000000..03bc1c059950
--- /dev/null
+++ b/src/joystick/gdk/SDL_gameinputjoystick_c.h
@@ -0,0 +1,58 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+#include "../SDL_sysjoystick.h"
+
+#if defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT
+
+/* include this file in C++ */
+#include <GameInput.h>
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum GAMEINPUT_JoystickEffectDataType
+{
+    GAMEINPUT_JoystickEffectDataType_HapticFeedback
+} GAMEINPUT_JoystickEffectDataType;
+
+typedef struct GAMEINPUT_JoystickEffectData
+{
+    GAMEINPUT_JoystickEffectDataType type;
+
+    union
+    {
+        struct /* type == GAMEINPUT_JoystickEffectDataType_HapticFeedback */
+        {
+            uint32_t hapticFeedbackMotorIndex;
+            GameInputHapticFeedbackParams hapticFeedbackParams;
+        };
+    };
+} GAMEINPUT_JoystickEffectData;
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* defined(SDL_JOYSTICK_GAMEINPUT) && SDL_JOYSTICK_GAMEINPUT */