From 0f50b57dc5eec40cbe0dd5ad6e0d52b119610fe1 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Mon, 4 Aug 2025 21:33:38 -0700 Subject: [PATCH 01/13] 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 (git@antheas.dev) 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 --- src/joystick/SDL_gamepad.c | 335 ++++++++++++++++++++---- src/joystick/hidapi/SDL_hidapi_sinput.c | 247 ++++++++++++++--- src/joystick/hidapi/SDL_hidapi_sinput.h | 91 +++++++ 3 files changed, 593 insertions(+), 80 deletions(-) create mode 100644 src/joystick/hidapi/SDL_hidapi_sinput.h diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 123b534b2ab32..4f2326df35c43 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -30,6 +30,7 @@ #include "controller_type.h" #include "usb_ids.h" #include "hidapi/SDL_hidapi_nintendo.h" +#include "hidapi/SDL_hidapi_sinput.h" #include "../events/SDL_events_c.h" @@ -54,6 +55,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; @@ -688,6 +709,268 @@ 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; + bool digital_triggers = false; + bool bumpers = false; + bool left_stick = false; + bool right_stick = false; + bool paddle_second_pair = false; + + // 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; + } + + // Analog triggers + switch (styles->trigger_style) { + // Analog triggers + bumpers + 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 + bumpers + case SINPUT_TRIGGERSTYLE_DIGITAL: + digital_triggers = true; + bumpers = true; + break; + + // Only bumpers + case SINPUT_TRIGGERSTYLE_BUMPERS: + bumpers = true; + break; + + default: + break; + } + + // BAYX buttons (East, South, North, West) + SDL_ADD_BUTTON_MAPPING("b", current_button++, mapping_string_len); // East (typically B on Xbox, Circle on PlayStation) + SDL_ADD_BUTTON_MAPPING("a", current_button++, mapping_string_len); // South (typically A on Xbox, X on PlayStation) + SDL_ADD_BUTTON_MAPPING("y", current_button++, mapping_string_len); // North (typically Y on Xbox, Triangle on PlayStation) + SDL_ADD_BUTTON_MAPPING("x", current_button++, mapping_string_len); // West (typically X on Xbox, Square on PlayStation) + + // DPad + 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) { + SDL_ADD_BUTTON_MAPPING("leftshoulder", current_button++, mapping_string_len); + 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); + } + + // Paddle 1/2 + switch (styles->paddle_style) { + case SINPUT_PADDLESTYLE_TWO: + SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); + break; + + case SINPUT_PADDLESTYLE_FOUR: + SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); + paddle_second_pair = true; + break; + + default: + break; + } + + // Start/Plus & Select/Back + SDL_ADD_BUTTON_MAPPING("start", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("back", current_button++, mapping_string_len); + + switch (styles->meta_style) { + case SINPUT_METASTYLE_GUIDE: + SDL_ADD_BUTTON_MAPPING("guide", current_button++, mapping_string_len); + break; + + case SINPUT_METASTYLE_GUIDESHARE: + 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_second_pair) { + 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 (styles->misc_style) { + case SINPUT_MISCSTYLE_1: + SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len); + break; + + case SINPUT_MISCSTYLE_2: + SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len); + break; + + case SINPUT_MISCSTYLE_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 SINPUT_MISCSTYLE_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; + } + + // Remove trailing comma + size_t len = SDL_strlen(mapping_string); + if (len > 0 && mapping_string[len - 1] == ',') { + mapping_string[len - 1] = '\0'; + } +} + +/* +* Helper function to decode SInput features information packed into version +*/ +static bool 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; + } + + switch (product) { + case USB_PRODUCT_HANDHELDLEGEND_PROGCC: + switch (sub_product) { + default: + // ProGCC Primary Mapping + SDL_strlcat(mapping_string, "a:b1,b:b0,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,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", mapping_string_len); + break; + } + return true; + + case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: + switch (sub_product) { + default: + // GC Ultimate Primary Map + SDL_strlcat(mapping_string, "a:b0,b:b2,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,x:b1,y:b3,misc3:b8,misc4:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", mapping_string_len); + break; + } + return true; + + case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: + switch (sub_product) { + default: + case SINPUT_GENERIC_DEVMAP: + // Default Fully Exposed Mapping (Development Purposes) + SDL_strlcat(mapping_string, "leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,b:b0,a:b1,y:b2,x: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", mapping_string_len); + break; + + case SINPUT_GENERIC_DYNMAP: + // Decode styles for correct dynamic features + decoded.misc_style = (SINPUT_MISC_STYLE_E)(version % SINPUT_MISCSTYLE_MAX); + version /= SINPUT_MISCSTYLE_MAX; + + decoded.touch_style = (SINPUT_TOUCH_STYLE_E)(version % SINPUT_TOUCHSTYLE_MAX); + version /= SINPUT_TOUCHSTYLE_MAX; + + decoded.meta_style = (SINPUT_META_STYLE_E)(version % SINPUT_METASTYLE_MAX); + version /= SINPUT_METASTYLE_MAX; + + decoded.paddle_style = (SINPUT_PADDLE_STYLE_E)(version % SINPUT_PADDLESTYLE_MAX); + version /= SINPUT_PADDLESTYLE_MAX; + + decoded.trigger_style = (SINPUT_TRIGGER_STYLE_E)(version % SINPUT_TRIGGERSTYLE_MAX); + version /= SINPUT_TRIGGERSTYLE_MAX; + + decoded.analog_style = (SINPUT_ANALOG_STYLE_E)(version % SINPUT_ANALOGSTYLE_MAX); + + SDL_SInputStylesMapExtraction(&decoded, mapping_string, mapping_string_len); + break; + } + return true; + + case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: + default: + // Unmapped device + return false; + } +} + /* * Helper function to guess at a mapping for HIDAPI gamepads */ @@ -697,10 +980,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 @@ -799,54 +1083,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; - } + Uint8 face_style = (guid.data[15] & 0xE0) >> 5; + Uint8 sub_product = guid.data[15] & 0x1F; - switch (product) { - case USB_PRODUCT_HANDHELDLEGEND_PROGCC: - switch (sub_type) { - default: - // ProGCC Primary Mapping - SDL_strlcat(mapping_string, "a:b1,b:b0,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,x:b3,y:b2,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:b2,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,x:b1,y:b3,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,b:b0,a:b1,y:b2,x: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; - - case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: - default: - // Unmapped device + if (!SDL_CreateMappingStringForSInputGamepad(vendor, product, sub_product, version, face_style, mapping_string, sizeof(mapping_string))) { return NULL; } } else { diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 5bb708d1e973d..4c4a0eeb8fb35 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -2,6 +2,9 @@ Simple DirectMedia Layer Copyright (C) 2025 Mitchell Cairns + Contributors: + Antheas Kapenekakis + 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. @@ -27,6 +30,7 @@ #include "SDL_hidapijoystick_c.h" #include "SDL_hidapi_rumble.h" +#include "SDL_hidapi_sinput.h" #ifdef SDL_JOYSTICK_HIDAPI_SINPUT @@ -119,6 +123,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 +176,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 +219,7 @@ typedef struct { SDL_HIDAPI_Device *device; Uint16 protocol_version; + Uint16 usb_device_version; bool sensors_enabled; Uint8 player_idx; @@ -233,6 +270,140 @@ 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; + Uint8 mask[4] = { 0 }; + + // ABXY + D-Pad + mask[0] = 0xFF; + ctx->dpad_supported = true; + + // Start + Back + mask[2] |= (SINPUT_BUTTONMASK_BACK | SINPUT_BUTTONMASK_START); + + // Trigger & bumper bits live in mask[1] + bool digital_triggers = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_TRIGGER) || + (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_TRIGGER); + bool bumpers = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_BUMPER) || + (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_BUMPER); + bool analog_trigs = ctx->left_analog_trigger_supported || + ctx->right_analog_trigger_supported; + + // Paddle bits may touch mask[1] and mask[2] + bool pg1 = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_PADDLE1) || + (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_PADDLE1); + bool pg2 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_LEFT_PADDLE2) || + (ctx->usage_masks[2] & SINPUT_BUTTONMASK_RIGHT_PADDLE2); + + // Guide/Share + bool guide = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_GUIDE) != 0; + bool share = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_CAPTURE) != 0; + + // Touchpads + bool t1 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_TOUCHPAD1) != 0; + bool t2 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_TOUCHPAD2) != 0; + + int analogIndex = SINPUT_ANALOGSTYLE_NONE; + if (ctx->left_analog_stick_supported && ctx->right_analog_stick_supported) { + analogIndex = SINPUT_ANALOGSTYLE_LEFTRIGHT; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_STICK | SINPUT_BUTTONMASK_RIGHT_STICK); + } else if (ctx->left_analog_stick_supported) { + analogIndex = SINPUT_ANALOGSTYLE_LEFTONLY; + mask[1] |= SINPUT_BUTTONMASK_LEFT_STICK; + } else if (ctx->right_analog_stick_supported) { + analogIndex = SINPUT_ANALOGSTYLE_RIGHTONLY; + mask[1] |= SINPUT_BUTTONMASK_RIGHT_STICK; + } + + int triggerIndex = SINPUT_TRIGGERSTYLE_NONE; + if (analog_trigs) { + triggerIndex = SINPUT_TRIGGERSTYLE_ANALOG; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER); + } else if (digital_triggers) { + triggerIndex = SINPUT_TRIGGERSTYLE_DIGITAL; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER | + SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER); + } else if (bumpers) { + triggerIndex = SINPUT_TRIGGERSTYLE_BUMPERS; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER); + } + + int paddleIndex = SINPUT_PADDLESTYLE_NONE; + if (pg1 && pg2) { + paddleIndex = 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) { + paddleIndex = SINPUT_PADDLESTYLE_TWO; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_PADDLE1 | SINPUT_BUTTONMASK_RIGHT_PADDLE1); + } + + int metaIndex = SINPUT_METASTYLE_NONE; + if (guide && share) { + metaIndex = SINPUT_METASTYLE_GUIDESHARE; + mask[2] |= (SINPUT_BUTTONMASK_GUIDE | SINPUT_BUTTONMASK_CAPTURE); + } else if (guide) { + metaIndex = SINPUT_METASTYLE_GUIDE; + mask[2] |= SINPUT_BUTTONMASK_GUIDE; + } + + int touchIndex = SINPUT_TOUCHSTYLE_NONE; + if (t1 && t2) { + touchIndex = SINPUT_TOUCHSTYLE_DOUBLE; + mask[2] |= (SINPUT_BUTTONMASK_TOUCHPAD1 | SINPUT_BUTTONMASK_TOUCHPAD2); + } else if (t1) { + touchIndex = SINPUT_TOUCHSTYLE_SINGLE; + mask[2] |= SINPUT_BUTTONMASK_TOUCHPAD1; + } + + // Extra misc + int miscIndex = SINPUT_MISCSTYLE_NONE; + Uint8 extra_misc = ctx->usage_masks[3] & 0x0F; + switch (extra_misc) { + case 0x0F: + miscIndex = SINPUT_MISCSTYLE_4; + mask[3] = 0x0F; + break; + case 0x07: + miscIndex = SINPUT_MISCSTYLE_3; + mask[3] = 0x07; + break; + case 0x03: + miscIndex = SINPUT_MISCSTYLE_2; + mask[3] = 0x03; + break; + case 0x01: + miscIndex = SINPUT_MISCSTYLE_1; + mask[3] = 0x01; + break; + default: + miscIndex = SINPUT_MISCSTYLE_NONE; + mask[3] = 0x00; + break; + } + + Uint16 version = analogIndex; + version = version * SINPUT_TRIGGERSTYLE_MAX + triggerIndex; + version = version * SINPUT_PADDLESTYLE_MAX + paddleIndex; + version = version * SINPUT_METASTYLE_MAX + metaIndex; + version = version * SINPUT_TOUCHSTYLE_MAX + touchIndex; + version = version * SINPUT_MISCSTYLE_MAX + miscIndex; + + ctx->usage_masks[0] = mask[0]; + ctx->usage_masks[1] = mask[1]; + ctx->usage_masks[2] = mask[2]; + ctx->usage_masks[3] = mask[3]; + + 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; @@ -281,55 +452,36 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) 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: + // SInput Generic DevMode, exposes all buttons/inputs + case SINPUT_GENERIC_DEVMAP: ctx->usage_masks[0] = 0xFF; ctx->usage_masks[1] = 0xFF; ctx->usage_masks[2] = 0xFF; ctx->usage_masks[3] = 0xFF; break; + + // SInput Generic Device Dynamic, applies an appropriate mapping + case SINPUT_GENERIC_DYNMAP: + ctx->usage_masks[0] = data[12]; + ctx->usage_masks[1] = data[13]; + ctx->usage_masks[2] = data[14]; + ctx->usage_masks[3] = data[15]; + break; } } else { // Masks in LSB to MSB // South, East, West, North, DUp, DDown, DLeft, DRight ctx->usage_masks[0] = data[12]; - // 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]; - // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R ctx->usage_masks[2] = data[14]; - // Power, Misc 4 to 10 ctx->usage_masks[3] = data[15]; } - // Derive button count from mask - for (Uint8 byte = 0; byte < 4; ++byte) { - for (Uint8 bit = 0; bit < 8; ++bit) { - if ((ctx->usage_masks[byte] & (1 << bit)) != 0) { - ++ctx->buttons_count; - } - } - } - - // Convert DPAD to hat - const int DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | - (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | - (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | - (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); - if ((ctx->usage_masks[0] & DPAD_MASK) == DPAD_MASK) { - ctx->dpad_supported = true; - ctx->usage_masks[0] &= ~DPAD_MASK; - ctx->buttons_count -= 4; - } - -#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]; @@ -354,6 +506,33 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelScale = CalculateAccelScale(ctx->accelRange); ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); + + // Process dynamic controller info + DeviceDynamicEncodingSetup(device); + + // Derive button count from mask + for (Uint8 byte = 0; byte < 4; ++byte) { + for (Uint8 bit = 0; bit < 8; ++bit) { + if ((ctx->usage_masks[byte] & (1 << bit)) != 0) { + ++ctx->buttons_count; + } + } + } + + // Convert DPAD to hat + const int DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | + (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | + (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | + (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); + if ((ctx->usage_masks[0] & DPAD_MASK) == DPAD_MASK) { + ctx->dpad_supported = true; + ctx->usage_masks[0] &= ~DPAD_MASK; + ctx->buttons_count -= 4; + } + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Buttons count: %d", ctx->buttons_count); +#endif } static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device) @@ -460,6 +639,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"); @@ -543,12 +725,11 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys 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: + case SINPUT_GENERIC_DEVMAP: axes = 6; break; } - } + } joystick->naxes = axes; diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.h b/src/joystick/hidapi/SDL_hidapi_sinput.h new file mode 100644 index 0000000000000..877c5037289d6 --- /dev/null +++ b/src/joystick/hidapi/SDL_hidapi_sinput.h @@ -0,0 +1,91 @@ +/* + Simple DirectMedia Layer + Copyright (C) 2025 Mitchell Cairns + + Contributors: + Antheas Kapenekakis + + 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_ANALOG_STYLE_E; + +typedef enum +{ + SINPUT_TRIGGERSTYLE_NONE, + SINPUT_TRIGGERSTYLE_BUMPERS, + SINPUT_TRIGGERSTYLE_ANALOG, + SINPUT_TRIGGERSTYLE_DIGITAL, + SINPUT_TRIGGERSTYLE_MAX, +} SINPUT_TRIGGER_STYLE_E; + +typedef enum +{ + SINPUT_PADDLESTYLE_NONE, + SINPUT_PADDLESTYLE_TWO, + SINPUT_PADDLESTYLE_FOUR, + SINPUT_PADDLESTYLE_MAX, +} SINPUT_PADDLE_STYLE_E; + +typedef enum +{ + SINPUT_METASTYLE_NONE, + SINPUT_METASTYLE_GUIDE, + SINPUT_METASTYLE_GUIDESHARE, + SINPUT_METASTYLE_MAX, +} SINPUT_META_STYLE_E; + +typedef enum +{ + SINPUT_TOUCHSTYLE_NONE, + SINPUT_TOUCHSTYLE_SINGLE, + SINPUT_TOUCHSTYLE_DOUBLE, + SINPUT_TOUCHSTYLE_MAX, +} SINPUT_TOUCH_STYLE_E; + +typedef enum +{ + SINPUT_MISCSTYLE_NONE, + SINPUT_MISCSTYLE_1, + SINPUT_MISCSTYLE_2, + SINPUT_MISCSTYLE_3, + SINPUT_MISCSTYLE_4, + SINPUT_MISCSTYLE_MAX, +} SINPUT_MISC_STYLE_E; + +typedef enum +{ + SINPUT_GENERIC_DEVMAP = 0x00, + SINPUT_GENERIC_DYNMAP = 0x1F, +} SDL_SInputGenericDevices_t; + +typedef struct +{ + Uint16 analog_style; + Uint16 trigger_style; + Uint16 paddle_style; + Uint16 meta_style; + Uint16 touch_style; + Uint16 misc_style; +} SDL_SInputStyles_t; From f0907a18417d127594faf3afd4073538553608ba Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Mon, 4 Aug 2025 21:49:23 -0700 Subject: [PATCH 02/13] Dynamic mapping boolean check --- src/joystick/hidapi/SDL_hidapi_sinput.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 4c4a0eeb8fb35..d9b388fc8d687 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -387,18 +387,20 @@ DeviceDynamicEncodingSetup(SDL_HIDAPI_Device *device) break; } - Uint16 version = analogIndex; - version = version * SINPUT_TRIGGERSTYLE_MAX + triggerIndex; - version = version * SINPUT_PADDLESTYLE_MAX + paddleIndex; - version = version * SINPUT_METASTYLE_MAX + metaIndex; - version = version * SINPUT_TOUCHSTYLE_MAX + touchIndex; - version = version * SINPUT_MISCSTYLE_MAX + miscIndex; + int version = analogIndex; + version = version * (int) SINPUT_TRIGGERSTYLE_MAX + triggerIndex; + version = version * (int)SINPUT_PADDLESTYLE_MAX + paddleIndex; + version = version * (int)SINPUT_METASTYLE_MAX + metaIndex; + version = version * (int)SINPUT_TOUCHSTYLE_MAX + touchIndex; + version = version * (int)SINPUT_MISCSTYLE_MAX + miscIndex; 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); + device->guid.data[12] = (Uint8)(version & 0xFF); device->guid.data[13] = (Uint8)(version >> 8); } @@ -449,6 +451,7 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelRange = EXTRACTUINT16(data, 8); ctx->gyroRange = EXTRACTUINT16(data, 10); + bool use_dynamic_mapping = false; if ((device->product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) && (device->vendor_id == USB_VENDOR_RASPBERRYPI)) { switch (ctx->sub_type) { @@ -467,6 +470,7 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->usage_masks[1] = data[13]; ctx->usage_masks[2] = data[14]; ctx->usage_masks[3] = data[15]; + use_dynamic_mapping = true; break; } } else { @@ -508,7 +512,9 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); // Process dynamic controller info - DeviceDynamicEncodingSetup(device); + if (use_dynamic_mapping) { + DeviceDynamicEncodingSetup(device); + } // Derive button count from mask for (Uint8 byte = 0; byte < 4; ++byte) { From 40a79d3dbc17494df428bdba9d76aa0ded7b36d3 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Mon, 4 Aug 2025 21:49:45 -0700 Subject: [PATCH 03/13] Int casting --- src/joystick/hidapi/SDL_hidapi_sinput.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index d9b388fc8d687..b0cb745c30662 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -389,10 +389,10 @@ DeviceDynamicEncodingSetup(SDL_HIDAPI_Device *device) int version = analogIndex; version = version * (int) SINPUT_TRIGGERSTYLE_MAX + triggerIndex; - version = version * (int)SINPUT_PADDLESTYLE_MAX + paddleIndex; - version = version * (int)SINPUT_METASTYLE_MAX + metaIndex; - version = version * (int)SINPUT_TOUCHSTYLE_MAX + touchIndex; - version = version * (int)SINPUT_MISCSTYLE_MAX + miscIndex; + version = version * (int) SINPUT_PADDLESTYLE_MAX + paddleIndex; + version = version * (int) SINPUT_METASTYLE_MAX + metaIndex; + version = version * (int) SINPUT_TOUCHSTYLE_MAX + touchIndex; + version = version * (int) SINPUT_MISCSTYLE_MAX + miscIndex; ctx->usage_masks[0] = mask[0]; ctx->usage_masks[1] = mask[1]; From 3f1d1037ed25a8c8b53f3397bcafbe124442ea36 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Mon, 4 Aug 2025 21:33:38 -0700 Subject: [PATCH 04/13] 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 (git@antheas.dev) 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 --- src/joystick/SDL_gamepad.c | 335 ++++++++++++++++++++---- src/joystick/hidapi/SDL_hidapi_sinput.c | 247 ++++++++++++++--- src/joystick/hidapi/SDL_hidapi_sinput.h | 91 +++++++ 3 files changed, 593 insertions(+), 80 deletions(-) create mode 100644 src/joystick/hidapi/SDL_hidapi_sinput.h diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index dca3cfcb91e32..4f2326df35c43 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -30,6 +30,7 @@ #include "controller_type.h" #include "usb_ids.h" #include "hidapi/SDL_hidapi_nintendo.h" +#include "hidapi/SDL_hidapi_sinput.h" #include "../events/SDL_events_c.h" @@ -54,6 +55,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; @@ -688,6 +709,268 @@ 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; + bool digital_triggers = false; + bool bumpers = false; + bool left_stick = false; + bool right_stick = false; + bool paddle_second_pair = false; + + // 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; + } + + // Analog triggers + switch (styles->trigger_style) { + // Analog triggers + bumpers + 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 + bumpers + case SINPUT_TRIGGERSTYLE_DIGITAL: + digital_triggers = true; + bumpers = true; + break; + + // Only bumpers + case SINPUT_TRIGGERSTYLE_BUMPERS: + bumpers = true; + break; + + default: + break; + } + + // BAYX buttons (East, South, North, West) + SDL_ADD_BUTTON_MAPPING("b", current_button++, mapping_string_len); // East (typically B on Xbox, Circle on PlayStation) + SDL_ADD_BUTTON_MAPPING("a", current_button++, mapping_string_len); // South (typically A on Xbox, X on PlayStation) + SDL_ADD_BUTTON_MAPPING("y", current_button++, mapping_string_len); // North (typically Y on Xbox, Triangle on PlayStation) + SDL_ADD_BUTTON_MAPPING("x", current_button++, mapping_string_len); // West (typically X on Xbox, Square on PlayStation) + + // DPad + 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) { + SDL_ADD_BUTTON_MAPPING("leftshoulder", current_button++, mapping_string_len); + 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); + } + + // Paddle 1/2 + switch (styles->paddle_style) { + case SINPUT_PADDLESTYLE_TWO: + SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); + break; + + case SINPUT_PADDLESTYLE_FOUR: + SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); + paddle_second_pair = true; + break; + + default: + break; + } + + // Start/Plus & Select/Back + SDL_ADD_BUTTON_MAPPING("start", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("back", current_button++, mapping_string_len); + + switch (styles->meta_style) { + case SINPUT_METASTYLE_GUIDE: + SDL_ADD_BUTTON_MAPPING("guide", current_button++, mapping_string_len); + break; + + case SINPUT_METASTYLE_GUIDESHARE: + 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_second_pair) { + 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 (styles->misc_style) { + case SINPUT_MISCSTYLE_1: + SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len); + break; + + case SINPUT_MISCSTYLE_2: + SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len); + break; + + case SINPUT_MISCSTYLE_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 SINPUT_MISCSTYLE_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; + } + + // Remove trailing comma + size_t len = SDL_strlen(mapping_string); + if (len > 0 && mapping_string[len - 1] == ',') { + mapping_string[len - 1] = '\0'; + } +} + +/* +* Helper function to decode SInput features information packed into version +*/ +static bool 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; + } + + switch (product) { + case USB_PRODUCT_HANDHELDLEGEND_PROGCC: + switch (sub_product) { + default: + // ProGCC Primary Mapping + SDL_strlcat(mapping_string, "a:b1,b:b0,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,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", mapping_string_len); + break; + } + return true; + + case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: + switch (sub_product) { + default: + // GC Ultimate Primary Map + SDL_strlcat(mapping_string, "a:b0,b:b2,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,x:b1,y:b3,misc3:b8,misc4:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", mapping_string_len); + break; + } + return true; + + case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: + switch (sub_product) { + default: + case SINPUT_GENERIC_DEVMAP: + // Default Fully Exposed Mapping (Development Purposes) + SDL_strlcat(mapping_string, "leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,b:b0,a:b1,y:b2,x: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", mapping_string_len); + break; + + case SINPUT_GENERIC_DYNMAP: + // Decode styles for correct dynamic features + decoded.misc_style = (SINPUT_MISC_STYLE_E)(version % SINPUT_MISCSTYLE_MAX); + version /= SINPUT_MISCSTYLE_MAX; + + decoded.touch_style = (SINPUT_TOUCH_STYLE_E)(version % SINPUT_TOUCHSTYLE_MAX); + version /= SINPUT_TOUCHSTYLE_MAX; + + decoded.meta_style = (SINPUT_META_STYLE_E)(version % SINPUT_METASTYLE_MAX); + version /= SINPUT_METASTYLE_MAX; + + decoded.paddle_style = (SINPUT_PADDLE_STYLE_E)(version % SINPUT_PADDLESTYLE_MAX); + version /= SINPUT_PADDLESTYLE_MAX; + + decoded.trigger_style = (SINPUT_TRIGGER_STYLE_E)(version % SINPUT_TRIGGERSTYLE_MAX); + version /= SINPUT_TRIGGERSTYLE_MAX; + + decoded.analog_style = (SINPUT_ANALOG_STYLE_E)(version % SINPUT_ANALOGSTYLE_MAX); + + SDL_SInputStylesMapExtraction(&decoded, mapping_string, mapping_string_len); + break; + } + return true; + + case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: + default: + // Unmapped device + return false; + } +} + /* * Helper function to guess at a mapping for HIDAPI gamepads */ @@ -697,10 +980,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 @@ -799,54 +1083,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; - } + Uint8 face_style = (guid.data[15] & 0xE0) >> 5; + Uint8 sub_product = guid.data[15] & 0x1F; - 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; - - case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: - default: - // Unmapped device + if (!SDL_CreateMappingStringForSInputGamepad(vendor, product, sub_product, version, face_style, mapping_string, sizeof(mapping_string))) { return NULL; } } else { diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 5bb708d1e973d..4c4a0eeb8fb35 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -2,6 +2,9 @@ Simple DirectMedia Layer Copyright (C) 2025 Mitchell Cairns + Contributors: + Antheas Kapenekakis + 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. @@ -27,6 +30,7 @@ #include "SDL_hidapijoystick_c.h" #include "SDL_hidapi_rumble.h" +#include "SDL_hidapi_sinput.h" #ifdef SDL_JOYSTICK_HIDAPI_SINPUT @@ -119,6 +123,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 +176,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 +219,7 @@ typedef struct { SDL_HIDAPI_Device *device; Uint16 protocol_version; + Uint16 usb_device_version; bool sensors_enabled; Uint8 player_idx; @@ -233,6 +270,140 @@ 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; + Uint8 mask[4] = { 0 }; + + // ABXY + D-Pad + mask[0] = 0xFF; + ctx->dpad_supported = true; + + // Start + Back + mask[2] |= (SINPUT_BUTTONMASK_BACK | SINPUT_BUTTONMASK_START); + + // Trigger & bumper bits live in mask[1] + bool digital_triggers = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_TRIGGER) || + (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_TRIGGER); + bool bumpers = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_BUMPER) || + (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_BUMPER); + bool analog_trigs = ctx->left_analog_trigger_supported || + ctx->right_analog_trigger_supported; + + // Paddle bits may touch mask[1] and mask[2] + bool pg1 = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_PADDLE1) || + (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_PADDLE1); + bool pg2 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_LEFT_PADDLE2) || + (ctx->usage_masks[2] & SINPUT_BUTTONMASK_RIGHT_PADDLE2); + + // Guide/Share + bool guide = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_GUIDE) != 0; + bool share = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_CAPTURE) != 0; + + // Touchpads + bool t1 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_TOUCHPAD1) != 0; + bool t2 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_TOUCHPAD2) != 0; + + int analogIndex = SINPUT_ANALOGSTYLE_NONE; + if (ctx->left_analog_stick_supported && ctx->right_analog_stick_supported) { + analogIndex = SINPUT_ANALOGSTYLE_LEFTRIGHT; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_STICK | SINPUT_BUTTONMASK_RIGHT_STICK); + } else if (ctx->left_analog_stick_supported) { + analogIndex = SINPUT_ANALOGSTYLE_LEFTONLY; + mask[1] |= SINPUT_BUTTONMASK_LEFT_STICK; + } else if (ctx->right_analog_stick_supported) { + analogIndex = SINPUT_ANALOGSTYLE_RIGHTONLY; + mask[1] |= SINPUT_BUTTONMASK_RIGHT_STICK; + } + + int triggerIndex = SINPUT_TRIGGERSTYLE_NONE; + if (analog_trigs) { + triggerIndex = SINPUT_TRIGGERSTYLE_ANALOG; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER); + } else if (digital_triggers) { + triggerIndex = SINPUT_TRIGGERSTYLE_DIGITAL; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER | + SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER); + } else if (bumpers) { + triggerIndex = SINPUT_TRIGGERSTYLE_BUMPERS; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER); + } + + int paddleIndex = SINPUT_PADDLESTYLE_NONE; + if (pg1 && pg2) { + paddleIndex = 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) { + paddleIndex = SINPUT_PADDLESTYLE_TWO; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_PADDLE1 | SINPUT_BUTTONMASK_RIGHT_PADDLE1); + } + + int metaIndex = SINPUT_METASTYLE_NONE; + if (guide && share) { + metaIndex = SINPUT_METASTYLE_GUIDESHARE; + mask[2] |= (SINPUT_BUTTONMASK_GUIDE | SINPUT_BUTTONMASK_CAPTURE); + } else if (guide) { + metaIndex = SINPUT_METASTYLE_GUIDE; + mask[2] |= SINPUT_BUTTONMASK_GUIDE; + } + + int touchIndex = SINPUT_TOUCHSTYLE_NONE; + if (t1 && t2) { + touchIndex = SINPUT_TOUCHSTYLE_DOUBLE; + mask[2] |= (SINPUT_BUTTONMASK_TOUCHPAD1 | SINPUT_BUTTONMASK_TOUCHPAD2); + } else if (t1) { + touchIndex = SINPUT_TOUCHSTYLE_SINGLE; + mask[2] |= SINPUT_BUTTONMASK_TOUCHPAD1; + } + + // Extra misc + int miscIndex = SINPUT_MISCSTYLE_NONE; + Uint8 extra_misc = ctx->usage_masks[3] & 0x0F; + switch (extra_misc) { + case 0x0F: + miscIndex = SINPUT_MISCSTYLE_4; + mask[3] = 0x0F; + break; + case 0x07: + miscIndex = SINPUT_MISCSTYLE_3; + mask[3] = 0x07; + break; + case 0x03: + miscIndex = SINPUT_MISCSTYLE_2; + mask[3] = 0x03; + break; + case 0x01: + miscIndex = SINPUT_MISCSTYLE_1; + mask[3] = 0x01; + break; + default: + miscIndex = SINPUT_MISCSTYLE_NONE; + mask[3] = 0x00; + break; + } + + Uint16 version = analogIndex; + version = version * SINPUT_TRIGGERSTYLE_MAX + triggerIndex; + version = version * SINPUT_PADDLESTYLE_MAX + paddleIndex; + version = version * SINPUT_METASTYLE_MAX + metaIndex; + version = version * SINPUT_TOUCHSTYLE_MAX + touchIndex; + version = version * SINPUT_MISCSTYLE_MAX + miscIndex; + + ctx->usage_masks[0] = mask[0]; + ctx->usage_masks[1] = mask[1]; + ctx->usage_masks[2] = mask[2]; + ctx->usage_masks[3] = mask[3]; + + 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; @@ -281,55 +452,36 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) 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: + // SInput Generic DevMode, exposes all buttons/inputs + case SINPUT_GENERIC_DEVMAP: ctx->usage_masks[0] = 0xFF; ctx->usage_masks[1] = 0xFF; ctx->usage_masks[2] = 0xFF; ctx->usage_masks[3] = 0xFF; break; + + // SInput Generic Device Dynamic, applies an appropriate mapping + case SINPUT_GENERIC_DYNMAP: + ctx->usage_masks[0] = data[12]; + ctx->usage_masks[1] = data[13]; + ctx->usage_masks[2] = data[14]; + ctx->usage_masks[3] = data[15]; + break; } } else { // Masks in LSB to MSB // South, East, West, North, DUp, DDown, DLeft, DRight ctx->usage_masks[0] = data[12]; - // 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]; - // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R ctx->usage_masks[2] = data[14]; - // Power, Misc 4 to 10 ctx->usage_masks[3] = data[15]; } - // Derive button count from mask - for (Uint8 byte = 0; byte < 4; ++byte) { - for (Uint8 bit = 0; bit < 8; ++bit) { - if ((ctx->usage_masks[byte] & (1 << bit)) != 0) { - ++ctx->buttons_count; - } - } - } - - // Convert DPAD to hat - const int DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | - (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | - (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | - (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); - if ((ctx->usage_masks[0] & DPAD_MASK) == DPAD_MASK) { - ctx->dpad_supported = true; - ctx->usage_masks[0] &= ~DPAD_MASK; - ctx->buttons_count -= 4; - } - -#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]; @@ -354,6 +506,33 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelScale = CalculateAccelScale(ctx->accelRange); ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); + + // Process dynamic controller info + DeviceDynamicEncodingSetup(device); + + // Derive button count from mask + for (Uint8 byte = 0; byte < 4; ++byte) { + for (Uint8 bit = 0; bit < 8; ++bit) { + if ((ctx->usage_masks[byte] & (1 << bit)) != 0) { + ++ctx->buttons_count; + } + } + } + + // Convert DPAD to hat + const int DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | + (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | + (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | + (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); + if ((ctx->usage_masks[0] & DPAD_MASK) == DPAD_MASK) { + ctx->dpad_supported = true; + ctx->usage_masks[0] &= ~DPAD_MASK; + ctx->buttons_count -= 4; + } + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Buttons count: %d", ctx->buttons_count); +#endif } static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device) @@ -460,6 +639,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"); @@ -543,12 +725,11 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys 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: + case SINPUT_GENERIC_DEVMAP: axes = 6; break; } - } + } joystick->naxes = axes; diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.h b/src/joystick/hidapi/SDL_hidapi_sinput.h new file mode 100644 index 0000000000000..877c5037289d6 --- /dev/null +++ b/src/joystick/hidapi/SDL_hidapi_sinput.h @@ -0,0 +1,91 @@ +/* + Simple DirectMedia Layer + Copyright (C) 2025 Mitchell Cairns + + Contributors: + Antheas Kapenekakis + + 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_ANALOG_STYLE_E; + +typedef enum +{ + SINPUT_TRIGGERSTYLE_NONE, + SINPUT_TRIGGERSTYLE_BUMPERS, + SINPUT_TRIGGERSTYLE_ANALOG, + SINPUT_TRIGGERSTYLE_DIGITAL, + SINPUT_TRIGGERSTYLE_MAX, +} SINPUT_TRIGGER_STYLE_E; + +typedef enum +{ + SINPUT_PADDLESTYLE_NONE, + SINPUT_PADDLESTYLE_TWO, + SINPUT_PADDLESTYLE_FOUR, + SINPUT_PADDLESTYLE_MAX, +} SINPUT_PADDLE_STYLE_E; + +typedef enum +{ + SINPUT_METASTYLE_NONE, + SINPUT_METASTYLE_GUIDE, + SINPUT_METASTYLE_GUIDESHARE, + SINPUT_METASTYLE_MAX, +} SINPUT_META_STYLE_E; + +typedef enum +{ + SINPUT_TOUCHSTYLE_NONE, + SINPUT_TOUCHSTYLE_SINGLE, + SINPUT_TOUCHSTYLE_DOUBLE, + SINPUT_TOUCHSTYLE_MAX, +} SINPUT_TOUCH_STYLE_E; + +typedef enum +{ + SINPUT_MISCSTYLE_NONE, + SINPUT_MISCSTYLE_1, + SINPUT_MISCSTYLE_2, + SINPUT_MISCSTYLE_3, + SINPUT_MISCSTYLE_4, + SINPUT_MISCSTYLE_MAX, +} SINPUT_MISC_STYLE_E; + +typedef enum +{ + SINPUT_GENERIC_DEVMAP = 0x00, + SINPUT_GENERIC_DYNMAP = 0x1F, +} SDL_SInputGenericDevices_t; + +typedef struct +{ + Uint16 analog_style; + Uint16 trigger_style; + Uint16 paddle_style; + Uint16 meta_style; + Uint16 touch_style; + Uint16 misc_style; +} SDL_SInputStyles_t; From f73e74d2d1107f19106dd12c3d988ef79da7c23e Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Mon, 4 Aug 2025 21:49:23 -0700 Subject: [PATCH 05/13] Dynamic mapping boolean check --- src/joystick/hidapi/SDL_hidapi_sinput.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 4c4a0eeb8fb35..d9b388fc8d687 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -387,18 +387,20 @@ DeviceDynamicEncodingSetup(SDL_HIDAPI_Device *device) break; } - Uint16 version = analogIndex; - version = version * SINPUT_TRIGGERSTYLE_MAX + triggerIndex; - version = version * SINPUT_PADDLESTYLE_MAX + paddleIndex; - version = version * SINPUT_METASTYLE_MAX + metaIndex; - version = version * SINPUT_TOUCHSTYLE_MAX + touchIndex; - version = version * SINPUT_MISCSTYLE_MAX + miscIndex; + int version = analogIndex; + version = version * (int) SINPUT_TRIGGERSTYLE_MAX + triggerIndex; + version = version * (int)SINPUT_PADDLESTYLE_MAX + paddleIndex; + version = version * (int)SINPUT_METASTYLE_MAX + metaIndex; + version = version * (int)SINPUT_TOUCHSTYLE_MAX + touchIndex; + version = version * (int)SINPUT_MISCSTYLE_MAX + miscIndex; 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); + device->guid.data[12] = (Uint8)(version & 0xFF); device->guid.data[13] = (Uint8)(version >> 8); } @@ -449,6 +451,7 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelRange = EXTRACTUINT16(data, 8); ctx->gyroRange = EXTRACTUINT16(data, 10); + bool use_dynamic_mapping = false; if ((device->product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) && (device->vendor_id == USB_VENDOR_RASPBERRYPI)) { switch (ctx->sub_type) { @@ -467,6 +470,7 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->usage_masks[1] = data[13]; ctx->usage_masks[2] = data[14]; ctx->usage_masks[3] = data[15]; + use_dynamic_mapping = true; break; } } else { @@ -508,7 +512,9 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); // Process dynamic controller info - DeviceDynamicEncodingSetup(device); + if (use_dynamic_mapping) { + DeviceDynamicEncodingSetup(device); + } // Derive button count from mask for (Uint8 byte = 0; byte < 4; ++byte) { From 93db8b57c8fd1899eac7e7beae0d2f2fd56029b7 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Mon, 4 Aug 2025 21:49:45 -0700 Subject: [PATCH 06/13] Int casting --- src/joystick/hidapi/SDL_hidapi_sinput.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index d9b388fc8d687..b0cb745c30662 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -389,10 +389,10 @@ DeviceDynamicEncodingSetup(SDL_HIDAPI_Device *device) int version = analogIndex; version = version * (int) SINPUT_TRIGGERSTYLE_MAX + triggerIndex; - version = version * (int)SINPUT_PADDLESTYLE_MAX + paddleIndex; - version = version * (int)SINPUT_METASTYLE_MAX + metaIndex; - version = version * (int)SINPUT_TOUCHSTYLE_MAX + touchIndex; - version = version * (int)SINPUT_MISCSTYLE_MAX + miscIndex; + version = version * (int) SINPUT_PADDLESTYLE_MAX + paddleIndex; + version = version * (int) SINPUT_METASTYLE_MAX + metaIndex; + version = version * (int) SINPUT_TOUCHSTYLE_MAX + touchIndex; + version = version * (int) SINPUT_MISCSTYLE_MAX + miscIndex; ctx->usage_masks[0] = mask[0]; ctx->usage_masks[1] = mask[1]; From 5c78e169ff8c9181fc48dcc6da7b432831625135 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Tue, 12 Aug 2025 21:23:39 -0700 Subject: [PATCH 07/13] Dynamic Mapping Update - Sub-Product 0 now utilizes the dynamic mapping fallback - New style types have been implemented to represent more gamepad types --- src/joystick/SDL_gamepad.c | 217 ++++++++++++++++-------- src/joystick/hidapi/SDL_hidapi_sinput.c | 214 +++++++++++------------ src/joystick/hidapi/SDL_hidapi_sinput.h | 37 ++-- 3 files changed, 266 insertions(+), 202 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 4f2326df35c43..65834b936862a 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -716,11 +716,35 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha { int current_button = 0; int current_axis = 0; + int misc_buttons = 0; bool digital_triggers = false; - bool bumpers = false; + bool dualstage_triggers = false; + int bumpers = 0; bool left_stick = false; bool right_stick = false; - bool paddle_second_pair = 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) { @@ -749,36 +773,69 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha 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 + bumpers + // 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 + bumpers + // Digital triggers case SINPUT_TRIGGERSTYLE_DIGITAL: digital_triggers = true; - bumpers = true; break; - // Only bumpers - case SINPUT_TRIGGERSTYLE_BUMPERS: - bumpers = true; + // 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; } - // BAYX buttons (East, South, North, West) - SDL_ADD_BUTTON_MAPPING("b", current_button++, mapping_string_len); // East (typically B on Xbox, Circle on PlayStation) + switch (styles->paddle_style) { + case SINPUT_PADDLESTYLE_TWO: + paddle_pairs = 1; + SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); + break; + + case SINPUT_PADDLESTYLE_FOUR: + SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); + SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); + 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("y", current_button++, mapping_string_len); // North (typically Y on Xbox, Triangle 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) - // DPad + // DPad (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 @@ -790,8 +847,10 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha } // Digital shoulder buttons (L/R Shoulder) - if (bumpers) { + 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); } @@ -799,35 +858,54 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha 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 - switch (styles->paddle_style) { - case SINPUT_PADDLESTYLE_TWO: + if(paddle_pairs > 0) { SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); - break; - - case SINPUT_PADDLESTYLE_FOUR: - SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); - SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); - paddle_second_pair = true; - break; - - default: - break; } // Start/Plus & Select/Back SDL_ADD_BUTTON_MAPPING("start", current_button++, mapping_string_len); - SDL_ADD_BUTTON_MAPPING("back", current_button++, mapping_string_len); switch (styles->meta_style) { - case SINPUT_METASTYLE_GUIDE: + 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_GUIDESHARE: + 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; @@ -837,7 +915,7 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha } // Paddle 3/4 - if (paddle_second_pair) { + if (paddle_pairs > 1) { SDL_ADD_BUTTON_MAPPING("paddle3", current_button++, mapping_string_len); SDL_ADD_BUTTON_MAPPING("paddle4", current_button++, mapping_string_len); } @@ -857,23 +935,23 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha break; } - switch (styles->misc_style) { - case SINPUT_MISCSTYLE_1: + switch (misc_buttons) { + case 1: SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len); break; - case SINPUT_MISCSTYLE_2: + case 2: SDL_ADD_BUTTON_MAPPING("misc3", current_button++, mapping_string_len); SDL_ADD_BUTTON_MAPPING("misc4", current_button++, mapping_string_len); break; - case SINPUT_MISCSTYLE_3: + 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 SINPUT_MISCSTYLE_4: + 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); @@ -883,12 +961,6 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha default: break; } - - // Remove trailing comma - size_t len = SDL_strlen(mapping_string); - if (len > 0 && mapping_string[len - 1] == ',') { - mapping_string[len - 1] = '\0'; - } } /* @@ -913,12 +985,41 @@ static bool SDL_CreateMappingStringForSInputGamepad(Uint16 vendor, Uint16 produc break; } + // For a sub-product value of 0, we interpret the + // mapping string dynamically based on the feature responses + if (sub_product == 0) { + // Decode styles for correct dynamic features + 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); + return true; + } + + // For non-zero sub-product IDs switch (product) { case USB_PRODUCT_HANDHELDLEGEND_PROGCC: switch (sub_product) { default: - // ProGCC Primary Mapping - SDL_strlcat(mapping_string, "a:b1,b:b0,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,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", mapping_string_len); + // ProGCC Default 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,", mapping_string_len); break; } return true; @@ -926,8 +1027,8 @@ static bool SDL_CreateMappingStringForSInputGamepad(Uint16 vendor, Uint16 produc case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: switch (sub_product) { default: - // GC Ultimate Primary Map - SDL_strlcat(mapping_string, "a:b0,b:b2,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,x:b1,y:b3,misc3:b8,misc4:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", mapping_string_len); + // GC Ultimate Default 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:a4,leftx:a0,lefty:a1,misc1:b13,misc3:b14,rightshoulder:b7,rightstick:b5,righttrigger:a5,rightx:a2,righty:a3,start:b10,misc4:b8,misc5:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", mapping_string_len); break; } return true; @@ -935,38 +1036,14 @@ static bool SDL_CreateMappingStringForSInputGamepad(Uint16 vendor, Uint16 produc case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: switch (sub_product) { default: - case SINPUT_GENERIC_DEVMAP: - // Default Fully Exposed Mapping (Development Purposes) - SDL_strlcat(mapping_string, "leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,b:b0,a:b1,y:b2,x: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", mapping_string_len); - break; - - case SINPUT_GENERIC_DYNMAP: - // Decode styles for correct dynamic features - decoded.misc_style = (SINPUT_MISC_STYLE_E)(version % SINPUT_MISCSTYLE_MAX); - version /= SINPUT_MISCSTYLE_MAX; - - decoded.touch_style = (SINPUT_TOUCH_STYLE_E)(version % SINPUT_TOUCHSTYLE_MAX); - version /= SINPUT_TOUCHSTYLE_MAX; - - decoded.meta_style = (SINPUT_META_STYLE_E)(version % SINPUT_METASTYLE_MAX); - version /= SINPUT_METASTYLE_MAX; - - decoded.paddle_style = (SINPUT_PADDLE_STYLE_E)(version % SINPUT_PADDLESTYLE_MAX); - version /= SINPUT_PADDLESTYLE_MAX; - - decoded.trigger_style = (SINPUT_TRIGGER_STYLE_E)(version % SINPUT_TRIGGERSTYLE_MAX); - version /= SINPUT_TRIGGERSTYLE_MAX; - - decoded.analog_style = (SINPUT_ANALOG_STYLE_E)(version % SINPUT_ANALOGSTYLE_MAX); - - SDL_SInputStylesMapExtraction(&decoded, mapping_string, mapping_string_len); + // 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", mapping_string_len); break; } return true; case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: default: - // Unmapped device return false; } } diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index b0cb745c30662..c3a36c09dba77 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -2,9 +2,6 @@ Simple DirectMedia Layer Copyright (C) 2025 Mitchell Cairns - Contributors: - Antheas Kapenekakis - 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. @@ -241,7 +238,7 @@ typedef struct 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 + 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) @@ -273,127 +270,156 @@ static inline float CalculateAccelScale(uint16_t 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) +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 + Back - mask[2] |= (SINPUT_BUTTONMASK_BACK | SINPUT_BUTTONMASK_START); + // Start button + mask[2] |= SINPUT_BUTTONMASK_START; - // Trigger & bumper bits live in mask[1] - bool digital_triggers = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_TRIGGER) || - (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_TRIGGER); - bool bumpers = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_BUMPER) || - (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_BUMPER); - bool analog_trigs = ctx->left_analog_trigger_supported || - ctx->right_analog_trigger_supported; + // 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; - // Paddle bits may touch mask[1] and mask[2] - bool pg1 = (ctx->usage_masks[1] & SINPUT_BUTTONMASK_LEFT_PADDLE1) || - (ctx->usage_masks[1] & SINPUT_BUTTONMASK_RIGHT_PADDLE1); - bool pg2 = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_LEFT_PADDLE2) || - (ctx->usage_masks[2] & SINPUT_BUTTONMASK_RIGHT_PADDLE2); + 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; - // Guide/Share - bool guide = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_GUIDE) != 0; - bool share = (ctx->usage_masks[2] & SINPUT_BUTTONMASK_CAPTURE) != 0; + 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 analogIndex = SINPUT_ANALOGSTYLE_NONE; + int analogStyle = SINPUT_ANALOGSTYLE_NONE; if (ctx->left_analog_stick_supported && ctx->right_analog_stick_supported) { - analogIndex = SINPUT_ANALOGSTYLE_LEFTRIGHT; + analogStyle = SINPUT_ANALOGSTYLE_LEFTRIGHT; mask[1] |= (SINPUT_BUTTONMASK_LEFT_STICK | SINPUT_BUTTONMASK_RIGHT_STICK); } else if (ctx->left_analog_stick_supported) { - analogIndex = SINPUT_ANALOGSTYLE_LEFTONLY; + analogStyle = SINPUT_ANALOGSTYLE_LEFTONLY; mask[1] |= SINPUT_BUTTONMASK_LEFT_STICK; } else if (ctx->right_analog_stick_supported) { - analogIndex = SINPUT_ANALOGSTYLE_RIGHTONLY; + analogStyle = SINPUT_ANALOGSTYLE_RIGHTONLY; mask[1] |= SINPUT_BUTTONMASK_RIGHT_STICK; } - int triggerIndex = SINPUT_TRIGGERSTYLE_NONE; - if (analog_trigs) { - triggerIndex = SINPUT_TRIGGERSTYLE_ANALOG; - mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER); + 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) { - triggerIndex = SINPUT_TRIGGERSTYLE_DIGITAL; - mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER | - SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER); - } else if (bumpers) { - triggerIndex = SINPUT_TRIGGERSTYLE_BUMPERS; - mask[1] |= (SINPUT_BUTTONMASK_LEFT_BUMPER | SINPUT_BUTTONMASK_RIGHT_BUMPER); + triggerStyle = SINPUT_TRIGGERSTYLE_DIGITAL; + mask[1] |= (SINPUT_BUTTONMASK_LEFT_TRIGGER | SINPUT_BUTTONMASK_RIGHT_TRIGGER); } - int paddleIndex = SINPUT_PADDLESTYLE_NONE; + // 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) { - paddleIndex = SINPUT_PADDLESTYLE_FOUR; + 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) { - paddleIndex = SINPUT_PADDLESTYLE_TWO; + paddleStyle = SINPUT_PADDLESTYLE_TWO; mask[1] |= (SINPUT_BUTTONMASK_LEFT_PADDLE1 | SINPUT_BUTTONMASK_RIGHT_PADDLE1); } - int metaIndex = SINPUT_METASTYLE_NONE; - if (guide && share) { - metaIndex = SINPUT_METASTYLE_GUIDESHARE; - mask[2] |= (SINPUT_BUTTONMASK_GUIDE | SINPUT_BUTTONMASK_CAPTURE); + + // 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) { - metaIndex = SINPUT_METASTYLE_GUIDE; - mask[2] |= SINPUT_BUTTONMASK_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 touchIndex = SINPUT_TOUCHSTYLE_NONE; + int touchStyle = SINPUT_TOUCHSTYLE_NONE; if (t1 && t2) { - touchIndex = SINPUT_TOUCHSTYLE_DOUBLE; + touchStyle = SINPUT_TOUCHSTYLE_DOUBLE; mask[2] |= (SINPUT_BUTTONMASK_TOUCHPAD1 | SINPUT_BUTTONMASK_TOUCHPAD2); } else if (t1) { - touchIndex = SINPUT_TOUCHSTYLE_SINGLE; + touchStyle = SINPUT_TOUCHSTYLE_SINGLE; mask[2] |= SINPUT_BUTTONMASK_TOUCHPAD1; } - // Extra misc - int miscIndex = SINPUT_MISCSTYLE_NONE; + // Misc Buttons + int miscStyle = SINPUT_MISCSTYLE_NONE; Uint8 extra_misc = ctx->usage_masks[3] & 0x0F; switch (extra_misc) { case 0x0F: - miscIndex = SINPUT_MISCSTYLE_4; + miscStyle = SINPUT_MISCSTYLE_4; mask[3] = 0x0F; break; case 0x07: - miscIndex = SINPUT_MISCSTYLE_3; + miscStyle = SINPUT_MISCSTYLE_3; mask[3] = 0x07; break; case 0x03: - miscIndex = SINPUT_MISCSTYLE_2; + miscStyle = SINPUT_MISCSTYLE_2; mask[3] = 0x03; break; case 0x01: - miscIndex = SINPUT_MISCSTYLE_1; + miscStyle = SINPUT_MISCSTYLE_1; mask[3] = 0x01; break; default: - miscIndex = SINPUT_MISCSTYLE_NONE; + miscStyle = SINPUT_MISCSTYLE_NONE; mask[3] = 0x00; break; } - int version = analogIndex; - version = version * (int) SINPUT_TRIGGERSTYLE_MAX + triggerIndex; - version = version * (int) SINPUT_PADDLESTYLE_MAX + paddleIndex; - version = version * (int) SINPUT_METASTYLE_MAX + metaIndex; - version = version * (int) SINPUT_TOUCHSTYLE_MAX + touchIndex; - version = version * (int) SINPUT_MISCSTYLE_MAX + miscIndex; + 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]; @@ -401,6 +427,7 @@ DeviceDynamicEncodingSetup(SDL_HIDAPI_Device *device) 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); } @@ -439,7 +466,7 @@ 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); @@ -451,40 +478,10 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelRange = EXTRACTUINT16(data, 8); ctx->gyroRange = EXTRACTUINT16(data, 10); - bool use_dynamic_mapping = false; - - if ((device->product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) && (device->vendor_id == USB_VENDOR_RASPBERRYPI)) { - switch (ctx->sub_type) { - default: - // SInput Generic DevMode, exposes all buttons/inputs - case SINPUT_GENERIC_DEVMAP: - ctx->usage_masks[0] = 0xFF; - ctx->usage_masks[1] = 0xFF; - ctx->usage_masks[2] = 0xFF; - ctx->usage_masks[3] = 0xFF; - break; - - // SInput Generic Device Dynamic, applies an appropriate mapping - case SINPUT_GENERIC_DYNMAP: - ctx->usage_masks[0] = data[12]; - ctx->usage_masks[1] = data[13]; - ctx->usage_masks[2] = data[14]; - ctx->usage_masks[3] = data[15]; - use_dynamic_mapping = true; - break; - } - } else { - // Masks in LSB to MSB - // South, East, West, North, DUp, DDown, DLeft, DRight - ctx->usage_masks[0] = data[12]; - // 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]; - // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R - ctx->usage_masks[2] = data[14]; - // Power, Misc 4 to 10 - ctx->usage_masks[3] = data[15]; - } + ctx->usage_masks[0] = data[12]; + ctx->usage_masks[1] = data[13]; + ctx->usage_masks[2] = data[14]; + ctx->usage_masks[3] = data[15]; // Get and validate touchpad parameters ctx->touchpad_count = data[16]; @@ -511,8 +508,9 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelScale = CalculateAccelScale(ctx->accelRange); ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); - // Process dynamic controller info - if (use_dynamic_mapping) { + // Sub Product 0 is a fallback to + // utilize a dynamic mapping + if (ctx->sub_product == 0) { DeviceDynamicEncodingSetup(device); } @@ -720,21 +718,9 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys 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 - case SINPUT_GENERIC_DEVMAP: - axes = 6; - break; - } + if (ctx->left_analog_trigger_supported || ctx->right_analog_trigger_supported) { + // Always add both analog trigger axes if one is present + axes += 2; } joystick->naxes = axes; diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.h b/src/joystick/hidapi/SDL_hidapi_sinput.h index 877c5037289d6..8bcc70d7fcbc9 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.h +++ b/src/joystick/hidapi/SDL_hidapi_sinput.h @@ -2,9 +2,6 @@ Simple DirectMedia Layer Copyright (C) 2025 Mitchell Cairns - Contributors: - Antheas Kapenekakis - 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. @@ -29,16 +26,24 @@ typedef enum SINPUT_ANALOGSTYLE_RIGHTONLY, SINPUT_ANALOGSTYLE_LEFTRIGHT, SINPUT_ANALOGSTYLE_MAX, -} SINPUT_ANALOG_STYLE_E; +} 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_BUMPERS, SINPUT_TRIGGERSTYLE_ANALOG, SINPUT_TRIGGERSTYLE_DIGITAL, + SINPUT_TRIGGERSTYLE_DUALSTAGE, SINPUT_TRIGGERSTYLE_MAX, -} SINPUT_TRIGGER_STYLE_E; +} SInput_TriggerStyleType; typedef enum { @@ -46,15 +51,16 @@ typedef enum SINPUT_PADDLESTYLE_TWO, SINPUT_PADDLESTYLE_FOUR, SINPUT_PADDLESTYLE_MAX, -} SINPUT_PADDLE_STYLE_E; +} SInput_PaddleStyleType; typedef enum { SINPUT_METASTYLE_NONE, - SINPUT_METASTYLE_GUIDE, - SINPUT_METASTYLE_GUIDESHARE, + SINPUT_METASTYLE_BACK, + SINPUT_METASTYLE_BACKGUIDE, + SINPUT_METASTYLE_BACKGUIDESHARE, SINPUT_METASTYLE_MAX, -} SINPUT_META_STYLE_E; +} SInput_MetaStyleType; typedef enum { @@ -62,7 +68,7 @@ typedef enum SINPUT_TOUCHSTYLE_SINGLE, SINPUT_TOUCHSTYLE_DOUBLE, SINPUT_TOUCHSTYLE_MAX, -} SINPUT_TOUCH_STYLE_E; +} SInput_TouchStyleType; typedef enum { @@ -72,17 +78,12 @@ typedef enum SINPUT_MISCSTYLE_3, SINPUT_MISCSTYLE_4, SINPUT_MISCSTYLE_MAX, -} SINPUT_MISC_STYLE_E; - -typedef enum -{ - SINPUT_GENERIC_DEVMAP = 0x00, - SINPUT_GENERIC_DYNMAP = 0x1F, -} SDL_SInputGenericDevices_t; +} SInput_MiscStyleType; typedef struct { Uint16 analog_style; + Uint16 bumper_style; Uint16 trigger_style; Uint16 paddle_style; Uint16 meta_style; From 34627baa6c79749b243b6e1955d25b55f6d8d0e6 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Tue, 12 Aug 2025 21:37:19 -0700 Subject: [PATCH 08/13] Code Cleanup - Move axes info to device init - Use shortened documentation URL - Generic device exposed mapping default for non-zero sub-product --- src/joystick/hidapi/SDL_hidapi_sinput.c | 43 ++++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index c3a36c09dba77..a53e46eee522a 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -33,7 +33,7 @@ /*****************************************************************************************************/ // 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 @@ -247,6 +247,8 @@ 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]; @@ -508,10 +510,33 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelScale = CalculateAccelScale(ctx->accelRange); ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); + 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 || ctx->right_analog_trigger_supported) { + // Always add both analog trigger axes if one is present + axes += 2; + } + + ctx->axes_count = axes; + // Sub Product 0 is a fallback to // utilize a dynamic mapping if (ctx->sub_product == 0) { DeviceDynamicEncodingSetup(device); + } else if (device->product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC && device->vendor_id == USB_VENDOR_RASPBERRYPI) { + ctx->usage_masks[0] = 0xFF; + ctx->usage_masks[1] = 0xFF; + ctx->usage_masks[2] = 0xFF; + ctx->usage_masks[3] = 0xFF; + + ctx->axes_count = 6; } // Derive button count from mask @@ -709,21 +734,7 @@ 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 || ctx->right_analog_trigger_supported) { - // Always add both analog trigger axes if one is present - axes += 2; - } - - joystick->naxes = axes; + joystick->naxes = ctx->axes_count; if (ctx->dpad_supported) { joystick->nhats = 1; From a8a87425c2708ffbae2773df043587589b3e9127 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Tue, 12 Aug 2025 22:05:51 -0700 Subject: [PATCH 09/13] Resolve type issue --- src/joystick/hidapi/SDL_hidapi_sinput.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index a53e46eee522a..6f3d3f1549190 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -510,7 +510,7 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->accelScale = CalculateAccelScale(ctx->accelRange); ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); - int axes = 0; + Uint8 axes = 0; if (ctx->left_analog_stick_supported) { axes += 2; } From d7a86626a429e48d2997559a2ca4982915315084 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Wed, 13 Aug 2025 01:08:32 -0700 Subject: [PATCH 10/13] Fix Paddle Bug --- src/joystick/SDL_gamepad.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 65834b936862a..cfd2f11b8aa50 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -814,13 +814,9 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha switch (styles->paddle_style) { case SINPUT_PADDLESTYLE_TWO: paddle_pairs = 1; - SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); - SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); break; case SINPUT_PADDLESTYLE_FOUR: - SDL_ADD_BUTTON_MAPPING("paddle1", current_button++, mapping_string_len); - SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); paddle_pairs = 2; break; From 59d9cf9a36386735b1a8786db5b69043676db625 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Wed, 13 Aug 2025 01:09:30 -0700 Subject: [PATCH 11/13] FireBird PS4 PID Added --- src/joystick/SDL_gamepad.c | 17 +++++++++++++++++ src/joystick/SDL_joystick.c | 3 ++- src/joystick/hidapi/SDL_hidapi_sinput.c | 5 ++++- src/joystick/usb_ids.h | 3 ++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index cfd2f11b8aa50..b56f45465ae1f 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -1038,7 +1038,24 @@ static bool SDL_CreateMappingStringForSInputGamepad(Uint16 vendor, Uint16 produc } return true; + case USB_PRODUCT_VOIDGAMING_PS4FIREBIRD: + switch (sub_product) { + default: + // PS4 FireBird Default Mapping + SDL_strlcat(mapping_string, "a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b4,lefttrigger:a4,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,paddle3:b17,paddle4:b16,rightshoulder:b7,rightstick:b5,righttrigger:a5,rightx:a2,righty:a3,start:b12,touchpad:b13,x:b2,y:b3,", mapping_string_len); + break; + } + return true; + case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: + switch (sub_product) { + default: + // FireBird Default Mapping + SDL_strlcat(mapping_string, "a:b0,b:b1,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b4,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b15,paddle1:b11,paddle2:b10,paddle3:b17,paddle4:b16,rightshoulder:b7,rightstick:b5,righttrigger:b9,rightx:a2,righty:a3,start:b12,x:b2,y:b3,", mapping_string_len); + break; + } + return true; + default: return false; } diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index a8bc76f1bf263..971eb697ccf68 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -3212,7 +3212,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; } } diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 6f3d3f1549190..1f838394d54e9 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -678,8 +678,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: diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index b98c3ebb74e58..3b80f878d0302 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -165,7 +165,8 @@ #define USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC 0x10c6 #define USB_PRODUCT_HANDHELDLEGEND_PROGCC 0x10df #define USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE 0x10dd -#define USB_PRODUCT_BONZIRICHANNEL_FIREBIRD 0x10e0 +#define USB_PRODUCT_BONZIRICHANNEL_FIREBIRD 0x10e0 +#define USB_PRODUCT_VOIDGAMING_PS4FIREBIRD 0x10e5 // USB usage pages #define USB_USAGEPAGE_GENERIC_DESKTOP 0x0001 From f0f32c135b801c497f1b468752cbaae232441fc1 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Wed, 13 Aug 2025 01:16:45 -0700 Subject: [PATCH 12/13] Comments Update --- src/joystick/SDL_gamepad.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index b56f45465ae1f..633f1d8d998a9 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -887,9 +887,10 @@ static inline void SDL_SInputStylesMapExtraction(SDL_SInputStyles_t* styles, cha SDL_ADD_BUTTON_MAPPING("paddle2", current_button++, mapping_string_len); } - // Start/Plus & Select/Back + // 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); From a2d065f77465222ec73bca4705f052327f973719 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Wed, 13 Aug 2025 01:41:01 -0700 Subject: [PATCH 13/13] Polling Rate Expansion Polling rate is now represented as a uint16 microseconds value. --- src/joystick/hidapi/SDL_hidapi_sinput.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 1f838394d54e9..c162bbaa1672c 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -237,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_product; // 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) @@ -475,7 +475,11 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) SDL_Log("SInput Sub-type: %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); @@ -744,11 +748,11 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys } 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) {