Browse Source

SInput: Version as a capabilities vehicle (#13667)

* SInput: version capabilities compression

This commit includes additions relating to SInput generic device reporting capabilities in a bit more detail, to automatically choose the best input map possible for the given device.

Thanks to Antheas Kapenekakis ([email protected]) for contributing the neat compression algorithm, this is pulled from the PR Draft here: https://github.com/libsdl-org/SDL/pull/13565

Co-authored-by: Antheas Kapenekakis <[email protected]>
mitchellcairns 1 day ago
parent
commit
78e0ec7e0d

+ 324 - 49
src/joystick/SDL_gamepad.c

@@ -31,6 +31,7 @@
 #include "usb_ids.h"
 #include "hidapi/SDL_hidapi_flydigi.h"
 #include "hidapi/SDL_hidapi_nintendo.h"
+#include "hidapi/SDL_hidapi_sinput.h"
 #include "../events/SDL_events_c.h"
 
 
@@ -55,6 +56,26 @@
 #define SDL_GAMEPAD_SDKLE_FIELD         "sdk<=:"
 #define SDL_GAMEPAD_SDKLE_FIELD_SIZE    SDL_strlen(SDL_GAMEPAD_SDKLE_FIELD)
 
+// Helper function to add button mapping
+#ifndef ADD_BUTTON_MAPPING
+#define SDL_ADD_BUTTON_MAPPING(sdl_name, button_id, maxlen)                     \
+    do {                                                                        \
+        char temp[32];                                                          \
+        (void)SDL_snprintf(temp, sizeof(temp), "%s:b%d,", sdl_name, button_id); \
+        SDL_strlcat(mapping_string, temp, maxlen);                              \
+    } while (0)
+#endif
+
+// Helper function to add axis mapping
+#ifndef ADD_AXIS_MAPPING
+#define SDL_ADD_AXIS_MAPPING(sdl_name, axis_id, maxlen)                       \
+    do {                                                                      \
+        char temp[32];                                                        \
+        (void)SDL_snprintf(temp, sizeof(temp), "%s:a%d,", sdl_name, axis_id); \
+        SDL_strlcat(mapping_string, temp, maxlen);                            \
+    } while (0)
+#endif
+
 static bool SDL_gamepads_initialized;
 static SDL_Gamepad *SDL_gamepads SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
 
@@ -689,6 +710,304 @@ static GamepadMapping_t *SDL_CreateMappingForAndroidGamepad(SDL_GUID guid)
 }
 #endif // SDL_PLATFORM_ANDROID
 
+/*
+* Helper function to apply SInput decoded styles to the mapping string
+*/
+static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, char* mapping_string, size_t mapping_string_len)
+{
+    int current_button = 0;
+    int current_axis = 0;
+    int misc_buttons = 0;
+    bool digital_triggers = false;
+    bool dualstage_triggers = false;
+    int bumpers = 0;
+    bool left_stick = false;
+    bool right_stick = false;
+    int paddle_pairs = 0;
+
+    // Determine how many misc buttons are used
+    switch (styles->misc_style) {
+    case SINPUT_MISCSTYLE_1:
+        misc_buttons = 1;
+        break;
+
+    case SINPUT_MISCSTYLE_2:
+        misc_buttons = 2;
+        break;
+
+    case SINPUT_MISCSTYLE_3:
+        misc_buttons = 3;
+        break;
+
+    case SINPUT_MISCSTYLE_4:
+        misc_buttons = 4;
+        break;
+
+    default:
+        break;
+    }
+
+    // Analog joysticks (always come first in axis mapping)
+    switch (styles->analog_style) {
+    case SINPUT_ANALOGSTYLE_LEFTONLY:
+        SDL_ADD_AXIS_MAPPING("leftx", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("lefty", current_axis++, mapping_string_len);
+        left_stick = true;
+        break;
+
+    case SINPUT_ANALOGSTYLE_LEFTRIGHT:
+        SDL_ADD_AXIS_MAPPING("leftx", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("lefty", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("rightx", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("righty", current_axis++, mapping_string_len);
+        left_stick = true;
+        right_stick = true;
+        break;
+
+    case SINPUT_ANALOGSTYLE_RIGHTONLY:
+        SDL_ADD_AXIS_MAPPING("rightx", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("righty", current_axis++, mapping_string_len);
+        right_stick = true;
+        break;
+
+    default:
+        break;
+    }
+
+    // Bumpers
+    switch (styles->bumper_style) {
+    case SINPUT_BUMPERSTYLE_ONE:
+        bumpers = 1;
+        break;
+
+    case SINPUT_BUMPERSTYLE_TWO:
+        bumpers = 2;
+        break;
+
+    default:
+        break;
+    }
+
+    // Analog triggers
+    switch (styles->trigger_style) {
+    // Analog triggers
+    case SINPUT_TRIGGERSTYLE_ANALOG:
+        SDL_ADD_AXIS_MAPPING("lefttrigger", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("righttrigger", current_axis++, mapping_string_len);
+        break;
+
+    // Digital triggers
+    case SINPUT_TRIGGERSTYLE_DIGITAL:
+        digital_triggers = true;
+        break;
+
+    // Analog triggers with digital press
+    case SINPUT_TRIGGERSTYLE_DUALSTAGE:
+        SDL_ADD_AXIS_MAPPING("lefttrigger", current_axis++, mapping_string_len);
+        SDL_ADD_AXIS_MAPPING("righttrigger", current_axis++, mapping_string_len);
+        dualstage_triggers = true;
+        break;
+
+    default:
+        break;
+    }
+
+    switch (styles->paddle_style) {
+    case SINPUT_PADDLESTYLE_TWO:
+        paddle_pairs = 1;
+        break;
+
+    case SINPUT_PADDLESTYLE_FOUR:
+        paddle_pairs = 2;
+        break;
+
+    default:
+        break;
+    }
+
+    // Digital button mappings
+    // ABXY buttons (always applied as South, East, West, North)
+    SDL_ADD_BUTTON_MAPPING("a", current_button++, mapping_string_len); // South (typically A on Xbox, X on PlayStation)
+    SDL_ADD_BUTTON_MAPPING("b", current_button++, mapping_string_len); // East  (typically B on Xbox, Circle on PlayStation)
+    SDL_ADD_BUTTON_MAPPING("x", current_button++, mapping_string_len); // West  (typically X on Xbox, Square on PlayStation)
+    SDL_ADD_BUTTON_MAPPING("y", current_button++, mapping_string_len); // North (typically Y on Xbox, Triangle on PlayStation)
+
+    // D-Pad (always applied)
+    SDL_strlcat(mapping_string, "dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,", mapping_string_len);
+
+    // Left and Right stick buttons
+    if (left_stick) {
+        SDL_ADD_BUTTON_MAPPING("leftstick", current_button++, mapping_string_len);
+    }
+    if (right_stick) {
+        SDL_ADD_BUTTON_MAPPING("rightstick", current_button++, mapping_string_len);
+    }
+
+    // Digital shoulder buttons (L/R Shoulder)
+    if (bumpers > 0) {
+        SDL_ADD_BUTTON_MAPPING("leftshoulder", current_button++, mapping_string_len);
+    }
+    if (bumpers > 1) {
+        SDL_ADD_BUTTON_MAPPING("rightshoulder", current_button++, mapping_string_len);
+    }
+
+    // Digital trigger buttons (capability overrides analog)
+    if (digital_triggers) {
+        SDL_ADD_BUTTON_MAPPING("lefttrigger", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("righttrigger", current_button++, mapping_string_len);
+    } else if (dualstage_triggers) {
+        // Dual-stage trigger buttons are appended as MISC buttons
+        // but only if we have the space to use them.
+        if (misc_buttons <= 2) {
+            switch (misc_buttons) {
+            case 0:
+                SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len);
+                SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len);
+                break;
+
+            case 1:
+                SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len);
+                SDL_ADD_BUTTON_MAPPING("misc5", current_button++, mapping_string_len);
+                break;
+
+            case 2:
+                SDL_ADD_BUTTON_MAPPING("misc5", current_button++, mapping_string_len);
+                SDL_ADD_BUTTON_MAPPING("misc6", current_button++, mapping_string_len);
+                break;
+
+            default:
+                // We do not overwrite other misc buttons if they are used.
+                break;
+            }
+        }
+    }
+
+    // Paddle 1/2
+    if (paddle_pairs > 0) {
+        SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len);
+    }
+
+    // Start/Plus
+    SDL_ADD_BUTTON_MAPPING("start", current_button++, mapping_string_len);
+
+    // Back/Minus, Guide/Home, Share/Capture
+    switch (styles->meta_style) {
+    case SINPUT_METASTYLE_BACK:
+        SDL_ADD_BUTTON_MAPPING("back", current_button++, mapping_string_len);
+        break;
+
+    case SINPUT_METASTYLE_BACKGUIDE:
+        SDL_ADD_BUTTON_MAPPING("back", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("guide", current_button++, mapping_string_len);
+        break;
+
+    case SINPUT_METASTYLE_BACKGUIDESHARE:
+        SDL_ADD_BUTTON_MAPPING("back", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("guide", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc1", current_button++, mapping_string_len);
+        break;
+
+    default:
+        break;
+    }
+
+    // Paddle 3/4
+    if (paddle_pairs > 1) {
+        SDL_ADD_BUTTON_MAPPING("paddle3", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("paddle4", current_button++, mapping_string_len);
+    }
+
+    // Touchpad buttons
+    switch (styles->touch_style) {
+    case SINPUT_TOUCHSTYLE_SINGLE:
+        SDL_ADD_BUTTON_MAPPING("touchpad", current_button++, mapping_string_len);
+        break;
+
+    case SINPUT_TOUCHSTYLE_DOUBLE:
+        SDL_ADD_BUTTON_MAPPING("touchpad", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc2", current_button++, mapping_string_len);
+        break;
+
+    default:
+        break;
+    }
+
+    switch (misc_buttons) {
+    case 1:
+        SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len);
+        break;
+
+    case 2:
+        SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len);
+        break;
+
+    case 3:
+        SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc5", current_button++, mapping_string_len);
+        break;
+
+    case 4:
+        SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc5", current_button++, mapping_string_len);
+        SDL_ADD_BUTTON_MAPPING("misc6", current_button++, mapping_string_len);
+        break;
+
+    default:
+        break;
+    }
+}
+
+/*
+* Helper function to decode SInput features information packed into version
+*/
+static void SDL_CreateMappingStringForSInputGamepad(Uint16 vendor, Uint16 product, Uint8 sub_product, Uint16 version, Uint8 face_style, char* mapping_string, size_t mapping_string_len)
+{
+    SDL_SInputStyles_t decoded = { 0 };
+
+    switch (face_style) {
+    default:
+        SDL_strlcat(mapping_string, "face:abxy,", mapping_string_len);
+        break;
+    case 2:
+        SDL_strlcat(mapping_string, "face:axby,", mapping_string_len);
+        break;
+    case 3:
+        SDL_strlcat(mapping_string, "face:bayx,", mapping_string_len);
+        break;
+    case 4:
+        SDL_strlcat(mapping_string, "face:sony,", mapping_string_len);
+        break;
+    }
+
+    // Interpret the mapping string
+    // dynamically based on the feature responses
+    decoded.misc_style = (SInput_MiscStyleType)(version % SINPUT_MISCSTYLE_MAX);
+    version /= SINPUT_MISCSTYLE_MAX;
+
+    decoded.touch_style = (SInput_TouchStyleType)(version % SINPUT_TOUCHSTYLE_MAX);
+    version /= SINPUT_TOUCHSTYLE_MAX;
+
+    decoded.meta_style = (SInput_MetaStyleType)(version % SINPUT_METASTYLE_MAX);
+    version /= SINPUT_METASTYLE_MAX;
+
+    decoded.paddle_style = (SInput_PaddleStyleType)(version % SINPUT_PADDLESTYLE_MAX);
+    version /= SINPUT_PADDLESTYLE_MAX;
+
+    decoded.trigger_style = (SInput_TriggerStyleType)(version % SINPUT_TRIGGERSTYLE_MAX);
+    version /= SINPUT_TRIGGERSTYLE_MAX;
+
+    decoded.bumper_style = (SInput_BumperStyleType)(version % SINPUT_BUMPERSTYLE_MAX);
+    version /= SINPUT_BUMPERSTYLE_MAX;
+
+    decoded.analog_style = (SInput_AnalogStyleType)(version % SINPUT_ANALOGSTYLE_MAX);
+
+    SDL_SInputStylesMapExtraction(&decoded, mapping_string, mapping_string_len);
+}
+
 /*
  * Helper function to guess at a mapping for HIDAPI gamepads
  */
@@ -698,10 +1017,11 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
     char mapping_string[1024];
     Uint16 vendor;
     Uint16 product;
+    Uint16 version;
 
     SDL_strlcpy(mapping_string, "none,*,", sizeof(mapping_string));
 
-    SDL_GetJoystickGUIDInfo(guid, &vendor, &product, NULL, NULL);
+    SDL_GetJoystickGUIDInfo(guid, &vendor, &product, &version, NULL);
 
     if (SDL_IsJoystickWheel(vendor, product)) {
         // We don't want to pick up Logitech FFB wheels here
@@ -827,56 +1147,11 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
             // This controller has no guide button
             SDL_strlcat(mapping_string, "a:b1,b:b0,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
     } else if (SDL_IsJoystickSInputController(vendor, product)) {
-        Uint8 face_style = (guid.data[15] & 0xE0) >> 5;
-        Uint8 sub_type  = guid.data[15] & 0x1F;
 
-        // Apply face style according to gamepad response
-        switch (face_style) {
-        default:
-            SDL_strlcat(mapping_string, "face:abxy,", sizeof(mapping_string));
-            break;
-        case 2:
-            SDL_strlcat(mapping_string, "face:axby,", sizeof(mapping_string));
-            break;
-        case 3:
-            SDL_strlcat(mapping_string, "face:bayx,", sizeof(mapping_string));
-            break;
-        case 4:
-            SDL_strlcat(mapping_string, "face:sony,", sizeof(mapping_string));
-            break;
-        }
-
-        switch (product) {
-        case USB_PRODUCT_HANDHELDLEGEND_PROGCC:
-            switch (sub_type) {
-            default:
-                // ProGCC Primary Mapping
-                SDL_strlcat(mapping_string, "a:b0,b:b1,x:b2,y:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b4,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b7,rightstick:b5,righttrigger:b9,rightx:a2,righty:a3,start:b10,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
-                break;
-            }
-            break;
-        case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE:
-            switch (sub_type) {
-            default:
-                // GC Ultimate Primary Map
-                SDL_strlcat(mapping_string, "a:b0,b:b1,x:b2,y:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b4,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b13,misc2:b14,rightshoulder:b7,rightstick:b5,righttrigger:a5,rightx:a2,righty:a3,start:b10,misc3:b8,misc4:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string));
-                break;
-            }
-            break;
-        case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC:
-            switch (sub_type) {
-            default:
-                // Default Fully Exposed Mapping (Development Purposes)
-                SDL_strlcat(mapping_string, "leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,a:b0,b:b1,x:b2,y:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftstick:b4,rightstick:b5,leftshoulder:b6,rightshoulder:b7,paddle1:b10,paddle2:b11,start:b12,back:b13,guide:b14,misc1:b15,paddle3:b16,paddle4:b17,touchpad:b18,misc2:b19,misc3:b20,misc4:b21,misc5:b22,misc6:b23", sizeof(mapping_string));
-                break;
-            }
-            break;
+        Uint8 face_style = (guid.data[15] & 0xE0) >> 5;
+        Uint8 sub_product  = guid.data[15] & 0x1F;
 
-        case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD:
-        default:
-            // Unmapped device
-            return NULL;
-        }
+        SDL_CreateMappingStringForSInputGamepad(vendor, product, sub_product, version, face_style, mapping_string, sizeof(mapping_string));
     } else {
         // All other gamepads have the standard set of 19 buttons and 6 axes
         if (SDL_IsJoystickGameCube(vendor, product)) {

+ 2 - 1
src/joystick/SDL_joystick.c

@@ -3221,7 +3221,8 @@ bool SDL_IsJoystickSInputController(Uint16 vendor_id, Uint16 product_id)
         if (product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC ||
             product_id == USB_PRODUCT_HANDHELDLEGEND_PROGCC ||
             product_id == USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE ||
-            product_id == USB_PRODUCT_BONZIRICHANNEL_FIREBIRD) {
+            product_id == USB_PRODUCT_BONZIRICHANNEL_FIREBIRD ||
+            product_id == USB_PRODUCT_VOIDGAMING_PS4FIREBIRD) {
             return true;
         }
     }

+ 264 - 85
src/joystick/hidapi/SDL_hidapi_sinput.c

@@ -27,12 +27,13 @@
 
 #include "SDL_hidapijoystick_c.h"
 #include "SDL_hidapi_rumble.h"
+#include "SDL_hidapi_sinput.h"
 
 #ifdef SDL_JOYSTICK_HIDAPI_SINPUT
 
 /*****************************************************************************************************/
 // This protocol is documented at:
-// https://docs.handheldlegend.com/s/sinput/doc/sinput-hid-protocol-TkPYWlDMAg
+// https://docs.handheldlegend.com/s/sinput
 /*****************************************************************************************************/
 
 // Define this if you want to log all packets from the controller
@@ -119,6 +120,39 @@
 #define SINPUT_BUTTON_IDX_MISC9             30
 #define SINPUT_BUTTON_IDX_MISC10            31
 
+#define SINPUT_BUTTONMASK_EAST          0x01
+#define SINPUT_BUTTONMASK_SOUTH         0x02
+#define SINPUT_BUTTONMASK_NORTH         0x04
+#define SINPUT_BUTTONMASK_WEST          0x08
+#define SINPUT_BUTTONMASK_DPAD_UP       0x10
+#define SINPUT_BUTTONMASK_DPAD_DOWN     0x20
+#define SINPUT_BUTTONMASK_DPAD_LEFT     0x40
+#define SINPUT_BUTTONMASK_DPAD_RIGHT    0x80
+#define SINPUT_BUTTONMASK_LEFT_STICK    0x01
+#define SINPUT_BUTTONMASK_RIGHT_STICK   0x02
+#define SINPUT_BUTTONMASK_LEFT_BUMPER   0x04
+#define SINPUT_BUTTONMASK_RIGHT_BUMPER  0x08
+#define SINPUT_BUTTONMASK_LEFT_TRIGGER  0x10
+#define SINPUT_BUTTONMASK_RIGHT_TRIGGER 0x20
+#define SINPUT_BUTTONMASK_LEFT_PADDLE1  0x40
+#define SINPUT_BUTTONMASK_RIGHT_PADDLE1 0x80
+#define SINPUT_BUTTONMASK_START         0x01
+#define SINPUT_BUTTONMASK_BACK          0x02
+#define SINPUT_BUTTONMASK_GUIDE         0x04
+#define SINPUT_BUTTONMASK_CAPTURE       0x08
+#define SINPUT_BUTTONMASK_LEFT_PADDLE2  0x10
+#define SINPUT_BUTTONMASK_RIGHT_PADDLE2 0x20
+#define SINPUT_BUTTONMASK_TOUCHPAD1     0x40
+#define SINPUT_BUTTONMASK_TOUCHPAD2     0x80
+#define SINPUT_BUTTONMASK_POWER         0x01
+#define SINPUT_BUTTONMASK_MISC4         0x02
+#define SINPUT_BUTTONMASK_MISC5         0x04
+#define SINPUT_BUTTONMASK_MISC6         0x08
+#define SINPUT_BUTTONMASK_MISC7         0x10
+#define SINPUT_BUTTONMASK_MISC8         0x20
+#define SINPUT_BUTTONMASK_MISC9         0x40
+#define SINPUT_BUTTONMASK_MISC10        0x80
+
 #define SINPUT_REPORT_IDX_COMMAND_RESPONSE_ID   1
 #define SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK 2
 
@@ -139,7 +173,6 @@
 #define EXTRACTUINT32(data, idx) ((Uint32)((data)[(idx)] | ((data)[(idx) + 1] << 8) | ((data)[(idx) + 2] << 16) | ((data)[(idx) + 3] << 24)))
 #endif
 
-
 typedef struct
 {
     uint8_t type;
@@ -183,6 +216,7 @@ typedef struct
 {
     SDL_HIDAPI_Device *device;
     Uint16 protocol_version;
+    Uint16 usb_device_version;
     bool sensors_enabled;
 
     Uint8 player_idx;
@@ -203,8 +237,8 @@ typedef struct
     Uint8 touchpad_count;        // 2 touchpads maximum
     Uint8 touchpad_finger_count; // 2 fingers for one touchpad, or 1 per touchpad (2 max)
 
-    Uint8  polling_rate_ms;
-    Uint8  sub_type;    // Subtype of the device, 0 in most cases
+    Uint16 polling_rate_us;
+    Uint8 sub_product;    // Subtype of the device, 0 in most cases
 
     Uint16 accelRange; // Example would be 2,4,8,16 +/- (g-force)
     Uint16 gyroRange;  // Example would be 1000,2000,4000 +/- (degrees per second)
@@ -213,6 +247,7 @@ typedef struct
     float gyroScale;  // Scale factor for gyroscope values
     Uint8 last_state[USB_PACKET_LENGTH];
 
+    Uint8 axes_count;
     Uint8 buttons_count;
     Uint8 usage_masks[4];
 
@@ -233,6 +268,172 @@ static inline float CalculateAccelScale(uint16_t g_range)
     return SDL_STANDARD_GRAVITY / (32768.0f / (float)g_range);
 }
 
+// This function uses base-n encoding to encode features into the version GUID bytes
+// that properly represents the supported device features
+// This also sets the driver context button mask correctly based on the features
+static void DeviceDynamicEncodingSetup(SDL_HIDAPI_Device *device)
+{
+    SDL_DriverSInput_Context *ctx = device->context;
+
+    // A new button mask is generated to provide
+    // SDL with a mapping string that is sane. In case of
+    // an unconventional gamepad setup, the closest sane
+    // mapping is provided to the driver.
+    Uint8 mask[4] = { 0 };
+
+    // For all gamepads, there is a minimum SInput expectation
+    // to have dpad, abxy, and start buttons
+
+    // ABXY + D-Pad
+    mask[0] = 0xFF;
+    ctx->dpad_supported = true;
+
+    // Start button
+    mask[2] |= SINPUT_BUTTONMASK_START;
+
+    // Bumpers 
+    bool left_bumper = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_BUMPER) != 0;
+    bool right_bumper = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_BUMPER) != 0;
+
+    int bumperStyle = SINPUT_BUMPERSTYLE_NONE;
+    if (left_bumper && right_bumper) {
+        bumperStyle = SINPUT_BUMPERSTYLE_TWO;
+        mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER);
+    } else if (left_bumper || right_bumper) {
+        bumperStyle = SINPUT_BUMPERSTYLE_ONE;
+
+        if (left_bumper) {
+            mask[1] |= SINPUT_BUTTONMASK_LEFT_BUMPER;
+        } else if (right_bumper) {
+            mask[1] |= SINPUT_BUTTONMASK_RIGHT_BUMPER;
+        }
+    }
+
+    // Trigger bits live in mask[1]
+    bool digital_triggers = (ctx->usage_masks[1] & (SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER)) != 0;
+
+    bool analog_triggers = ctx->left_analog_trigger_supported || ctx->right_analog_trigger_supported;
+
+    // Touchpads
+    bool t1 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_TOUCHPAD1) != 0;
+    bool t2 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_TOUCHPAD2) != 0;
+
+    int analogStyle = SINPUT_ANALOGSTYLE_NONE;
+    if (ctx->left_analog_stick_supported && ctx->right_analog_stick_supported) {
+        analogStyle = SINPUT_ANALOGSTYLE_LEFTRIGHT;
+        mask[1] |= (SINPUT_BUTTONMASK_LEFT_STICK | SINPUT_BUTTONMASK_RIGHT_STICK);
+    } else if (ctx->left_analog_stick_supported) {
+        analogStyle = SINPUT_ANALOGSTYLE_LEFTONLY;
+        mask[1] |= SINPUT_BUTTONMASK_LEFT_STICK;
+    } else if (ctx->right_analog_stick_supported) {
+        analogStyle = SINPUT_ANALOGSTYLE_RIGHTONLY;
+        mask[1] |= SINPUT_BUTTONMASK_RIGHT_STICK;
+    }
+
+    int triggerStyle = SINPUT_TRIGGERSTYLE_NONE;
+
+    if (analog_triggers && digital_triggers) {
+        // When we have both analog triggers and digital triggers
+        // this is interpreted as having dual-stage triggers
+        triggerStyle = SINPUT_TRIGGERSTYLE_DUALSTAGE;
+        mask[1] |= (SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER);
+    } else if (analog_triggers) {
+        triggerStyle = SINPUT_TRIGGERSTYLE_ANALOG;
+    } else if (digital_triggers) {
+        triggerStyle = SINPUT_TRIGGERSTYLE_DIGITAL;
+        mask[1] |= (SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER);
+    }
+
+    // Paddle bits may touch mask[1] and mask[2]
+    bool pg1 = (ctx->usage_masks[1] & (SINPUT_BUTTONMASK_LEFT_PADDLE1 | SINPUT_BUTTONMASK_RIGHT_PADDLE1)) != 0;
+    bool pg2 = (ctx->usage_masks[2] & (SINPUT_BUTTONMASK_LEFT_PADDLE2 | SINPUT_BUTTONMASK_RIGHT_PADDLE2)) != 0;
+
+    int paddleStyle = SINPUT_PADDLESTYLE_NONE;
+    if (pg1 && pg2) {
+        paddleStyle = SINPUT_PADDLESTYLE_FOUR;
+        mask[1] |= (SINPUT_BUTTONMASK_LEFT_PADDLE1 | SINPUT_BUTTONMASK_RIGHT_PADDLE1);
+        mask[2] |= (SINPUT_BUTTONMASK_LEFT_PADDLE2 | SINPUT_BUTTONMASK_RIGHT_PADDLE2);
+    } else if (pg1) {
+        paddleStyle = SINPUT_PADDLESTYLE_TWO;
+        mask[1] |= (SINPUT_BUTTONMASK_LEFT_PADDLE1 | SINPUT_BUTTONMASK_RIGHT_PADDLE1);
+    }
+
+
+    // Meta Buttons (Back, Guide, Share)
+    bool back = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_BACK) != 0;
+    bool guide = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_GUIDE) != 0;
+    bool share = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_CAPTURE) != 0;
+
+    int metaStyle = SINPUT_METASTYLE_NONE;
+    if (share) {
+        metaStyle = SINPUT_METASTYLE_BACKGUIDESHARE;
+        mask[2] |= (SINPUT_BUTTONMASK_BACK | SINPUT_BUTTONMASK_GUIDE | SINPUT_BUTTONMASK_CAPTURE);
+    } else if (guide) {
+        metaStyle = SINPUT_METASTYLE_BACKGUIDE;
+        mask[2] |= (SINPUT_BUTTONMASK_BACK | SINPUT_BUTTONMASK_GUIDE);
+    } else if (back) {
+        metaStyle = SINPUT_METASTYLE_BACK;
+        mask[2] |= (SINPUT_BUTTONMASK_BACK);
+    }
+
+    int touchStyle = SINPUT_TOUCHSTYLE_NONE;
+    if (t1 && t2) {
+        touchStyle = SINPUT_TOUCHSTYLE_DOUBLE;
+        mask[2] |= (SINPUT_BUTTONMASK_TOUCHPAD1 | SINPUT_BUTTONMASK_TOUCHPAD2);
+    } else if (t1) {
+        touchStyle = SINPUT_TOUCHSTYLE_SINGLE;
+        mask[2] |= SINPUT_BUTTONMASK_TOUCHPAD1;
+    }
+
+    // Misc Buttons
+    int miscStyle = SINPUT_MISCSTYLE_NONE;
+    Uint8 extra_misc = ctx->usage_masks[3] & 0x0F;
+    switch (extra_misc) {
+    case 0x0F:
+        miscStyle = SINPUT_MISCSTYLE_4;
+        mask[3] = 0x0F;
+        break;
+    case 0x07:
+        miscStyle = SINPUT_MISCSTYLE_3;
+        mask[3] = 0x07;
+        break;
+    case 0x03:
+        miscStyle = SINPUT_MISCSTYLE_2;
+        mask[3] = 0x03;
+        break;
+    case 0x01:
+        miscStyle = SINPUT_MISCSTYLE_1;
+        mask[3] = 0x01;
+        break;
+    default:
+        miscStyle = SINPUT_MISCSTYLE_NONE;
+        mask[3] = 0x00;
+        break;
+    }
+
+    int version = analogStyle;
+    version = (version * (int)SINPUT_BUMPERSTYLE_MAX) + bumperStyle;
+    version = (version * (int)SINPUT_TRIGGERSTYLE_MAX) + triggerStyle;
+    version = (version * (int)SINPUT_PADDLESTYLE_MAX) + paddleStyle;
+    version = (version * (int)SINPUT_METASTYLE_MAX) + metaStyle;
+    version = (version * (int)SINPUT_TOUCHSTYLE_MAX) + touchStyle;
+    version = (version * (int)SINPUT_MISCSTYLE_MAX) + miscStyle;
+
+    // Overwrite our button usage masks
+    // with our sanitized masks
+    ctx->usage_masks[0] = mask[0];
+    ctx->usage_masks[1] = mask[1];
+    ctx->usage_masks[2] = mask[2];
+    ctx->usage_masks[3] = mask[3];
+
+    version = SDL_clamp(version, 0, UINT16_MAX);
+
+    // Overwrite 'Version' field of the GUID data
+    device->guid.data[12] = (Uint8)(version & 0xFF);
+    device->guid.data[13] = (Uint8)(version >> 8);
+}
+
+
 static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data)
 {
     SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context;
@@ -266,46 +467,70 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data)
     // The 5 LSB represent a device sub-type
     device->guid.data[15] = data[5];
 
-    ctx->sub_type = (data[5] & 0x1F);
+    ctx->sub_product = (data[5] & 0x1F);
 
 #if defined(DEBUG_SINPUT_INIT)
     SDL_Log("SInput Face Style: %d", (data[5] & 0xE0) >> 5);
-    SDL_Log("SInput Sub-type: %d", (data[5] & 0x1F));
+    SDL_Log("SInput Sub-product: %d", (data[5] & 0x1F));
 #endif
 
-    ctx->polling_rate_ms = data[6];
+    ctx->polling_rate_us = EXTRACTUINT16(data, 6);
+
+#if defined(DEBUG_SINPUT_INIT)
+    SDL_Log("SInput polling interval (microseconds): %d", ctx->polling_rate_us);
+#endif
 
     ctx->accelRange = EXTRACTUINT16(data, 8);
     ctx->gyroRange = EXTRACTUINT16(data, 10);
 
+    ctx->usage_masks[0] = data[12];
+    ctx->usage_masks[1] = data[13];
+    ctx->usage_masks[2] = data[14];
+    ctx->usage_masks[3] = data[15];
 
-    if ((device->product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) && (device->vendor_id == USB_VENDOR_RASPBERRYPI)) {
-        switch (ctx->sub_type) {
-        // SInput generic device, exposes all buttons
-        default:
-        case 0:
-            ctx->usage_masks[0] = 0xFF;
-            ctx->usage_masks[1] = 0xFF;
-            ctx->usage_masks[2] = 0xFF;
-            ctx->usage_masks[3] = 0xFF;
-            break;
-        }
-    } else {
-        // Masks in LSB to MSB
-        // South, East, West, North, DUp, DDown, DLeft, DRight
-        ctx->usage_masks[0] = data[12];
+    // Get and validate touchpad parameters
+    ctx->touchpad_count = data[16];
+    ctx->touchpad_finger_count = data[17];
+
+    // Get device Serial - MAC address
+    char serial[18];
+    (void)SDL_snprintf(serial, sizeof(serial), "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x",
+                       data[18], data[19], data[20], data[21], data[22], data[23]);
 
-        // Stick Left, Stick Right, L Shoulder, R Shoulder,
-        // L Digital Trigger, R Digital Trigger, L Paddle 1, R Paddle 1
-        ctx->usage_masks[1] = data[13];
+#if defined(DEBUG_SINPUT_INIT)
+    SDL_Log("Serial num: %s", serial);
+#endif
+    HIDAPI_SetDeviceSerial(device, serial);
+
+#if defined(DEBUG_SINPUT_INIT)
+    SDL_Log("Accelerometer Range: %d", ctx->accelRange);
+#endif
 
-        // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R
-        ctx->usage_masks[2] = data[14];
+#if defined(DEBUG_SINPUT_INIT)
+    SDL_Log("Gyro Range: %d", ctx->gyroRange);
+#endif
+
+    ctx->accelScale = CalculateAccelScale(ctx->accelRange);
+    ctx->gyroScale = CalculateGyroScale(ctx->gyroRange);
 
-        // Power, Misc 4 to 10
-        ctx->usage_masks[3] = data[15];
+    Uint8 axes = 0;
+    if (ctx->left_analog_stick_supported) {
+        axes += 2;
+    }
+
+    if (ctx->right_analog_stick_supported) {
+        axes += 2;
+    }
+
+    if (ctx->left_analog_trigger_supported || ctx->right_analog_trigger_supported) {
+        // Always add both analog trigger axes if one is present
+        axes += 2;
     }
 
+    ctx->axes_count = axes;
+
+    DeviceDynamicEncodingSetup(device);
+
     // Derive button count from mask
     for (Uint8 byte = 0; byte < 4; ++byte) {
         for (Uint8 bit = 0; bit < 8; ++bit) {
@@ -329,31 +554,6 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data)
 #if defined(DEBUG_SINPUT_INIT)
     SDL_Log("Buttons count: %d", ctx->buttons_count);
 #endif
-
-    // Get and validate touchpad parameters
-    ctx->touchpad_count = data[16];
-    ctx->touchpad_finger_count = data[17];
-
-    // Get device Serial - MAC address
-    char serial[18];
-    (void)SDL_snprintf(serial, sizeof(serial), "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x",
-                       data[18], data[19], data[20], data[21], data[22], data[23]);
-
-#if defined(DEBUG_SINPUT_INIT)
-    SDL_Log("Serial num: %s", serial);
-#endif
-    HIDAPI_SetDeviceSerial(device, serial);
-
-#if defined(DEBUG_SINPUT_INIT)
-    SDL_Log("Accelerometer Range: %d", ctx->accelRange);
-#endif
-
-#if defined(DEBUG_SINPUT_INIT)
-    SDL_Log("Gyro Range: %d", ctx->gyroRange);
-#endif
-
-    ctx->accelScale = CalculateAccelScale(ctx->accelRange);
-    ctx->gyroScale = CalculateGyroScale(ctx->gyroRange);
 }
 
 static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device)
@@ -460,6 +660,9 @@ static bool HIDAPI_DriverSInput_InitDevice(SDL_HIDAPI_Device *device)
         return false;
     }
 
+    // Store the USB Device Version because we will overwrite this data
+    ctx->usb_device_version = device->version;
+
     switch (device->product_id) {
     case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE:
         HIDAPI_SetDeviceName(device, "HHL GC Ultimate");
@@ -467,8 +670,11 @@ static bool HIDAPI_DriverSInput_InitDevice(SDL_HIDAPI_Device *device)
     case USB_PRODUCT_HANDHELDLEGEND_PROGCC:
         HIDAPI_SetDeviceName(device, "HHL ProGCC");
         break;
+    case USB_PRODUCT_VOIDGAMING_PS4FIREBIRD:
+        HIDAPI_SetDeviceName(device, "Void Gaming PS4 FireBird");
+        break;
     case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD:
-        HIDAPI_SetDeviceName(device, "Bonziri Firebird");
+        HIDAPI_SetDeviceName(device, "Bonziri FireBird");
         break;
     case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC:
     default:
@@ -523,45 +729,18 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys
 
     SDL_zeroa(ctx->last_state);
 
-    int axes = 0;
-    if (ctx->left_analog_stick_supported) {
-        axes += 2;
-    }
-
-    if (ctx->right_analog_stick_supported) {
-        axes += 2;
-    }
-
-    if (ctx->left_analog_trigger_supported) {
-        ++axes;
-    }
-
-    if (ctx->right_analog_trigger_supported) {
-        ++axes;
-    }
-
-    if ((device->product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) && (device->vendor_id == USB_VENDOR_RASPBERRYPI)) {
-        switch (ctx->sub_type) {
-        // Default generic device, exposes all axes
-        default:
-        case 0:
-            axes = 6;
-            break;
-        }
-    } 
-
-    joystick->naxes = axes;
+    joystick->naxes = ctx->axes_count;
 
     if (ctx->dpad_supported) {
         joystick->nhats = 1;
     }
 
     if (ctx->accelerometer_supported) {
-        SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 1000.0f / ctx->polling_rate_ms);
+        SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 1000000.0f / ctx->polling_rate_us);
     }
 
     if (ctx->gyroscope_supported) {
-        SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 1000.0f / ctx->polling_rate_ms);
+        SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 1000000.0f / ctx->polling_rate_us);
     }
 
     if (ctx->touchpad_supported) {

+ 92 - 0
src/joystick/hidapi/SDL_hidapi_sinput.h

@@ -0,0 +1,92 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 2025 Mitchell Cairns <[email protected]>
+
+  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.
+*/
+
+typedef enum
+{
+    SINPUT_ANALOGSTYLE_NONE,
+    SINPUT_ANALOGSTYLE_LEFTONLY,
+    SINPUT_ANALOGSTYLE_RIGHTONLY,
+    SINPUT_ANALOGSTYLE_LEFTRIGHT,
+    SINPUT_ANALOGSTYLE_MAX,
+} SInput_AnalogStyleType;
+
+typedef enum
+{
+    SINPUT_BUMPERSTYLE_NONE,
+    SINPUT_BUMPERSTYLE_ONE,
+    SINPUT_BUMPERSTYLE_TWO,
+    SINPUT_BUMPERSTYLE_MAX,
+} SInput_BumperStyleType;
+
+typedef enum
+{
+    SINPUT_TRIGGERSTYLE_NONE,
+    SINPUT_TRIGGERSTYLE_ANALOG,
+    SINPUT_TRIGGERSTYLE_DIGITAL,
+    SINPUT_TRIGGERSTYLE_DUALSTAGE,
+    SINPUT_TRIGGERSTYLE_MAX,
+} SInput_TriggerStyleType;
+
+typedef enum
+{
+    SINPUT_PADDLESTYLE_NONE,
+    SINPUT_PADDLESTYLE_TWO,
+    SINPUT_PADDLESTYLE_FOUR,
+    SINPUT_PADDLESTYLE_MAX,
+} SInput_PaddleStyleType;
+
+typedef enum
+{
+    SINPUT_METASTYLE_NONE,
+    SINPUT_METASTYLE_BACK,
+    SINPUT_METASTYLE_BACKGUIDE,
+    SINPUT_METASTYLE_BACKGUIDESHARE,
+    SINPUT_METASTYLE_MAX,
+} SInput_MetaStyleType;
+
+typedef enum
+{
+    SINPUT_TOUCHSTYLE_NONE,
+    SINPUT_TOUCHSTYLE_SINGLE,
+    SINPUT_TOUCHSTYLE_DOUBLE,
+    SINPUT_TOUCHSTYLE_MAX,
+} SInput_TouchStyleType;
+
+typedef enum
+{
+    SINPUT_MISCSTYLE_NONE,
+    SINPUT_MISCSTYLE_1,
+    SINPUT_MISCSTYLE_2,
+    SINPUT_MISCSTYLE_3,
+    SINPUT_MISCSTYLE_4,
+    SINPUT_MISCSTYLE_MAX,
+} SInput_MiscStyleType;
+
+typedef struct
+{
+    Uint16 analog_style;
+    Uint16 bumper_style;
+    Uint16 trigger_style;
+    Uint16 paddle_style;
+    Uint16 meta_style;
+    Uint16 touch_style;
+    Uint16 misc_style;
+} SDL_SInputStyles_t;

+ 1 - 0
src/joystick/usb_ids.h

@@ -171,6 +171,7 @@
 #define USB_PRODUCT_HANDHELDLEGEND_PROGCC                 0x10df
 #define USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE             0x10dd
 #define USB_PRODUCT_BONZIRICHANNEL_FIREBIRD               0x10e0
+#define USB_PRODUCT_VOIDGAMING_PS4FIREBIRD                0x10e5
 
 // USB usage pages
 #define USB_USAGEPAGE_GENERIC_DESKTOP 0x0001