SInput: Version as a capabilities vehicle (#13667)

* SInput: version capabilities compression

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

Thanks to Antheas Kapenekakis (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 <git@antheas.dev>
This commit is contained in:
mitchellcairns
2025-09-13 07:21:39 -07:00
committed by GitHub
parent 2d1870d8b3
commit 78e0ec7e0d
5 changed files with 687 additions and 139 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
/*
Simple DirectMedia Layer
Copyright (C) 2025 Mitchell Cairns <mitch.cairns@handheldlegend.com>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
typedef enum
{
SINPUT_ANALOGSTYLE_NONE,
SINPUT_ANALOGSTYLE_LEFTONLY,
SINPUT_ANALOGSTYLE_RIGHTONLY,
SINPUT_ANALOGSTYLE_LEFTRIGHT,
SINPUT_ANALOGSTYLE_MAX,
} SInput_AnalogStyleType;
typedef enum
{
SINPUT_BUMPERSTYLE_NONE,
SINPUT_BUMPERSTYLE_ONE,
SINPUT_BUMPERSTYLE_TWO,
SINPUT_BUMPERSTYLE_MAX,
} SInput_BumperStyleType;
typedef enum
{
SINPUT_TRIGGERSTYLE_NONE,
SINPUT_TRIGGERSTYLE_ANALOG,
SINPUT_TRIGGERSTYLE_DIGITAL,
SINPUT_TRIGGERSTYLE_DUALSTAGE,
SINPUT_TRIGGERSTYLE_MAX,
} SInput_TriggerStyleType;
typedef enum
{
SINPUT_PADDLESTYLE_NONE,
SINPUT_PADDLESTYLE_TWO,
SINPUT_PADDLESTYLE_FOUR,
SINPUT_PADDLESTYLE_MAX,
} SInput_PaddleStyleType;
typedef enum
{
SINPUT_METASTYLE_NONE,
SINPUT_METASTYLE_BACK,
SINPUT_METASTYLE_BACKGUIDE,
SINPUT_METASTYLE_BACKGUIDESHARE,
SINPUT_METASTYLE_MAX,
} SInput_MetaStyleType;
typedef enum
{
SINPUT_TOUCHSTYLE_NONE,
SINPUT_TOUCHSTYLE_SINGLE,
SINPUT_TOUCHSTYLE_DOUBLE,
SINPUT_TOUCHSTYLE_MAX,
} SInput_TouchStyleType;
typedef enum
{
SINPUT_MISCSTYLE_NONE,
SINPUT_MISCSTYLE_1,
SINPUT_MISCSTYLE_2,
SINPUT_MISCSTYLE_3,
SINPUT_MISCSTYLE_4,
SINPUT_MISCSTYLE_MAX,
} SInput_MiscStyleType;
typedef struct
{
Uint16 analog_style;
Uint16 bumper_style;
Uint16 trigger_style;
Uint16 paddle_style;
Uint16 meta_style;
Uint16 touch_style;
Uint16 misc_style;
} SDL_SInputStyles_t;

View File

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