diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index 58194e72467cd..a9024fc683255 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -723,6 +723,7 @@ + diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 8a988ace966c8..9a0ce21537866 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -74,6 +74,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 843f8e61ce613..f59c31f9e2c9e 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -613,6 +613,7 @@ + @@ -774,4 +775,4 @@ - + \ No newline at end of file diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 375c175c0e95a..4a1b31c8848c9 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -1215,6 +1215,9 @@ joystick\hidapi + + joystick\hidapi + joystick\hidapi @@ -1615,4 +1618,4 @@ - + \ No newline at end of file diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index ffe8fa75a60bf..a6434e435a2cf 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 89E580242D03606400DAF6D3 /* SDL_hidapihaptic_lg4ff.c in Sources */ = {isa = PBXBuildFile; fileRef = 89E580212D03606400DAF6D3 /* SDL_hidapihaptic_lg4ff.c */; }; 89E580252D03606400DAF6D3 /* SDL_hidapihaptic_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 89E580202D03606400DAF6D3 /* SDL_hidapihaptic_c.h */; }; 9846B07C287A9020000C35C8 /* SDL_hidapi_shield.c in Sources */ = {isa = PBXBuildFile; fileRef = 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */; }; + 02D6A1C228A84B8F00A7F002 /* SDL_hidapi_sinput.c in Sources */ = {isa = PBXBuildFile; fileRef = 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */; }; A1626A3E2617006A003F1973 /* SDL_triangle.c in Sources */ = {isa = PBXBuildFile; fileRef = A1626A3D2617006A003F1973 /* SDL_triangle.c */; }; A1626A522617008D003F1973 /* SDL_triangle.h in Headers */ = {isa = PBXBuildFile; fileRef = A1626A512617008C003F1973 /* SDL_triangle.h */; }; A1BB8B6327F6CF330057CFA8 /* SDL_list.c in Sources */ = {isa = PBXBuildFile; fileRef = A1BB8B6127F6CF320057CFA8 /* SDL_list.c */; }; @@ -620,6 +621,7 @@ 89E580202D03606400DAF6D3 /* SDL_hidapihaptic_c.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapihaptic_c.h; sourceTree = ""; }; 89E580212D03606400DAF6D3 /* SDL_hidapihaptic_lg4ff.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapihaptic_lg4ff.c; sourceTree = ""; }; 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_shield.c; sourceTree = ""; }; + 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = ""; }; A1626A3D2617006A003F1973 /* SDL_triangle.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_triangle.c; sourceTree = ""; }; A1626A512617008C003F1973 /* SDL_triangle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_triangle.h; sourceTree = ""; }; A1BB8B6127F6CF320057CFA8 /* SDL_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_list.c; sourceTree = ""; }; @@ -1943,6 +1945,7 @@ A75FDBC323EA380300529352 /* SDL_hidapi_rumble.h */, A75FDBC423EA380300529352 /* SDL_hidapi_rumble.c */, 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */, + 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */, F3984CCF25BCC92800374F43 /* SDL_hidapi_stadia.c */, A75FDAAC23E2795C00529352 /* SDL_hidapi_steam.c */, F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */, @@ -2877,6 +2880,7 @@ A7D8B62F23E2514300DCD162 /* SDL_sysfilesystem.m in Sources */, A7D8B41C23E2514300DCD162 /* SDL_systls.c in Sources */, 9846B07C287A9020000C35C8 /* SDL_hidapi_shield.c in Sources */, + 02D6A1C228A84B8F00A7F002 /* SDL_hidapi_sinput.c in Sources */, F31013C72C24E98200FBE946 /* SDL_keymap.c in Sources */, F3A9AE992C8A13C100AAC390 /* SDL_render_gpu.c in Sources */, A7D8BBD923E2574800DCD162 /* SDL_uikitmessagebox.m in Sources */, diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 0900b36eb4c23..878dd2d486f38 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -1746,6 +1746,18 @@ extern "C" { */ #define SDL_HINT_JOYSTICK_HIDAPI_8BITDO "SDL_JOYSTICK_HIDAPI_8BITDO" +/** + * A variable controlling whether the HIDAPI driver for SInput controllers + * should be used. More info - https://github.com/HandHeldLegend/SInput-HID + * + * This variable can be set to the following values: + * + * "0" - HIDAPI driver is not used. "1" - HIDAPI driver is used. + * + * The default is the value of SDL_HINT_JOYSTICK_HIDAPI + */ +#define SDL_HINT_JOYSTICK_HIDAPI_SINPUT "SDL_JOYSTICK_HIDAPI_SINPUT" + /** * A variable controlling whether the HIDAPI driver for Flydigi controllers * should be used. diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 28aaf6a23da58..59d50bffa2bb8 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -798,6 +798,54 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) product == USB_PRODUCT_8BITDO_SF30_PRO_BT)) { // 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] & 0xF0) >> 4; + Uint8 u_id = guid.data[15] & 0x0F; + + switch (product) { + case USB_PRODUCT_HANDHELDLEGEND_PROGCC: + // ProGCC Mapping + SDL_strlcat(mapping_string, "a:b1,b:b0,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:b12,leftx:a0,lefty:a1,misc1:b17,rightshoulder:b11,rightstick:b9,righttrigger:b13,rightx:a2,righty:a3,start:b14,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + break; + + case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: + // GC Ultimate Map + SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc3:b18,paddle1:b13,paddle2:b12,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); + break; + + case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: + if (u_id != 1) { + return NULL; + } + + // SuperGamepad+ Map + if (u_id == 1) { + SDL_strlcat(mapping_string, "a:b1,b:b0,back:b11,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,leftshoulder:b8,rightshoulder:b9,start:b10,x:b3,y:b2,", sizeof(mapping_string)); + } + + // Apply face style + switch (face_style) { + default: + case 1: + 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; + } + break; + + default: + case USB_PRODUCT_BONJIRICHANNEL_FIREBIRD: + // Unmapped devices + return NULL; + } } else { // All other gamepads have the standard set of 19 buttons and 6 axes if (SDL_IsJoystickGameCube(vendor, product)) { @@ -1235,6 +1283,7 @@ static bool SDL_PrivateParseGamepadElement(SDL_Gamepad *gamepad, const char *szG if (SDL_strstr(gamepad->mapping->mapping, ",hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1") != NULL) { baxy_mapping = true; } + // FIXME: We fix these up when loading the mapping, does this ever get hit? //SDL_assert(!axby_mapping && !baxy_mapping); diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 0e9fcab3b55c4..3d9b9aea25c9e 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -3201,6 +3201,17 @@ bool SDL_IsJoystickHoriSteamController(Uint16 vendor_id, Uint16 product_id) return vendor_id == USB_VENDOR_HORI && (product_id == USB_PRODUCT_HORI_STEAM_CONTROLLER || product_id == USB_PRODUCT_HORI_STEAM_CONTROLLER_BT); } +bool SDL_IsJoystickSInputController(Uint16 vendor_id, Uint16 product_id) +{ + bool vendor_match = (vendor_id == USB_VENDOR_RASPBERRYPI); + bool product_match = + (product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) | + (product_id == USB_PRODUCT_HANDHELDLEGEND_PROGCC) | + (product_id == USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE) | + (product_id == USB_PRODUCT_BONJIRICHANNEL_FIREBIRD); + return (vendor_match && product_match); +} + bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id) { return vendor_id == USB_VENDOR_FLYDIGI && product_id == USB_PRODUCT_FLYDIGI_GAMEPAD; diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h index cbc33608c4478..c6e1a7b792746 100644 --- a/src/joystick/SDL_joystick_c.h +++ b/src/joystick/SDL_joystick_c.h @@ -135,6 +135,9 @@ extern bool SDL_IsJoystickSteamController(Uint16 vendor_id, Uint16 product_id); // Function to return whether a joystick is a HORI Steam controller extern bool SDL_IsJoystickHoriSteamController(Uint16 vendor_id, Uint16 product_id); +// Function to return whether a joystick is an SInput (Open Format) controller +extern bool SDL_IsJoystickSInputController(Uint16 vendor_id, Uint16 product_id); + // Function to return whether a joystick is a Flydigi controller extern bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id); diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c new file mode 100644 index 0000000000000..142e45c1efed1 --- /dev/null +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -0,0 +1,809 @@ +/* + Simple DirectMedia Layer + Copyright (C) 2025 Mitchell Cairns + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_JOYSTICK_HIDAPI + +#include "../../SDL_hints_c.h" +#include "../SDL_sysjoystick.h" + +#include "SDL_hidapijoystick_c.h" +#include "SDL_hidapi_rumble.h" + +#ifdef SDL_JOYSTICK_HIDAPI_SINPUT + +/*****************************************************************************************************/ + +// Define this if you want to log all packets from the controller +#if 0 +#define DEBUG_SINPUT_PROTOCOL +#endif + +#if 0 +#define DEBUG_SINPUT_INIT +#endif + +#define SINPUT_DEVICE_REPORT_SIZE 64 // Size of input reports (And CMD Input reports) +#define SINPUT_DEVICE_REPORT_COMMAND_SIZE 48 // Size of command OUTPUT reports + +#define SINPUT_DEVICE_REPORT_ID_JOYSTICK_INPUT 0x01 +#define SINPUT_DEVICE_REPORT_ID_INPUT_CMDDAT 0x02 +#define SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT 0x03 + +#define SINPUT_DEVICE_COMMAND_HAPTIC 0x01 +#define SINPUT_DEVICE_COMMAND_FEATURES 0x02 +#define SINPUT_DEVICE_COMMAND_PLAYERLED 0x03 +#define SINPUT_DEVICE_COMMAND_JOYSTICKRGB 0x04 + +#define SINPUT_HAPTIC_TYPE_PRECISE 0x01 +#define SINPUT_HAPTIC_TYPE_ERMSIMULATION 0x02 + +#define SINPUT_DEFAULT_GYRO_SENS 2000 +#define SINPUT_DEFAULT_ACCEL_SENS 8 + +#define SINPUT_REPORT_IDX_BUTTONS_0 3 +#define SINPUT_REPORT_IDX_BUTTONS_1 4 +#define SINPUT_REPORT_IDX_BUTTONS_2 5 +#define SINPUT_REPORT_IDX_BUTTONS_3 6 +#define SINPUT_REPORT_IDX_LEFT_X 7 +#define SINPUT_REPORT_IDX_LEFT_Y 9 +#define SINPUT_REPORT_IDX_RIGHT_X 11 +#define SINPUT_REPORT_IDX_RIGHT_Y 13 +#define SINPUT_REPORT_IDX_LEFT_TRIGGER 15 +#define SINPUT_REPORT_IDX_RIGHT_TRIGGER 17 +#define SINPUT_REPORT_IDX_IMU_TIMESTAMP 19 +#define SINPUT_REPORT_IDX_IMU_ACCEL_X 21 +#define SINPUT_REPORT_IDX_IMU_ACCEL_Y 23 +#define SINPUT_REPORT_IDX_IMU_ACCEL_Z 25 +#define SINPUT_REPORT_IDX_IMU_GYRO_X 27 +#define SINPUT_REPORT_IDX_IMU_GYRO_Y 29 +#define SINPUT_REPORT_IDX_IMU_GYRO_Z 31 +#define SINPUT_REPORT_IDX_TOUCH1_X 33 +#define SINPUT_REPORT_IDX_TOUCH1_Y 35 +#define SINPUT_REPORT_IDX_TOUCH1_P 37 +#define SINPUT_REPORT_IDX_TOUCH2_X 39 +#define SINPUT_REPORT_IDX_TOUCH2_Y 41 +#define SINPUT_REPORT_IDX_TOUCH2_P 43 + +#define SINPUT_REPORT_IDX_COMMAND_RESPONSE_ID 1 +#define SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK 2 + +#define SINPUT_REPORT_IDX_PLUG_STATUS 1 +#define SINPUT_REPORT_IDX_CHARGE_LEVEL 2 + +#define SINPUT_MAX_ALLOWED_TOUCHPADS 2 + +#ifndef EXTRACTSINT16 +#define EXTRACTSINT16(data, idx) ((Sint16)((data)[(idx)] | ((data)[(idx) + 1] << 8))) +#endif + +#ifndef EXTRACTUINT16 +#define EXTRACTUINT16(data, idx) ((Uint16)((data)[(idx)] | ((data)[(idx) + 1] << 8))) +#endif + + +typedef struct +{ + uint8_t type; + + union { + // Frequency Amplitude pairs + struct { + struct { + uint16_t frequency_1; + uint16_t amplitude_1; + uint16_t frequency_2; + uint16_t amplitude_2; + } left; + + struct { + uint16_t frequency_1; + uint16_t amplitude_1; + uint16_t frequency_2; + uint16_t amplitude_2; + } right; + + } type_1; + + // Basic ERM simulation model + struct { + struct { + uint8_t amplitude; + bool brake; + } left; + + struct { + uint8_t amplitude; + bool brake; + } right; + + } type_2; + }; +} SINPUT_HAPTIC_S; + +typedef struct +{ + SDL_HIDAPI_Device *device; + bool sensors_enabled; + + Uint8 player_idx; + + bool player_leds_supported; + bool joystick_rgb_supported; + bool rumble_supported; + bool accelerometer_supported; + bool gyroscope_supported; + bool left_analog_stick_supported; + bool right_analog_stick_supported; + bool left_analog_trigger_supported; + bool right_analog_trigger_supported; + bool touchpad_supported; + + 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 accelRange; // Example would be 2,4,8,16 +/- (g-force) + Uint16 gyroRange; // Example would be 1000,2000,4000 +/- (degrees per second) + + float accelScale; // Scale factor for accelerometer values + float gyroScale; // Scale factor for gyroscope values + Uint8 last_state[USB_PACKET_LENGTH]; + + Uint8 buttons_count; + Uint8 usage_masks[4]; + + Uint64 imu_timestamp; // Nanoseconds. We accumulate with received deltas +} SDL_DriverSInput_Context; + +// Converts raw int16_t gyro scale setting +static inline float CalculateGyroScale(uint16_t dps_range) +{ + return SDL_PI_F / 180.0f / (32768.0f / (float)dps_range); +} + +// Converts raw int16_t accel scale setting +static inline float CalculateAccelScale(uint16_t g_range) +{ + return SDL_STANDARD_GRAVITY / (32768.0f / (float)g_range); +} + +static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + // Bitfields are not portable, so we unpack them into a struct value + ctx->rumble_supported = (data[0] & 0x01) != 0; + ctx->player_leds_supported = (data[0] & 0x02) != 0; + ctx->accelerometer_supported = (data[0] & 0x04) != 0; + ctx->gyroscope_supported = (data[0] & 0x08) != 0; + + ctx->left_analog_stick_supported = (data[0] & 0x10) != 0; + ctx->right_analog_stick_supported = (data[0] & 0x20) != 0; + ctx->left_analog_trigger_supported = (data[0] & 0x40) != 0; + ctx->right_analog_trigger_supported = (data[0] & 0x80) != 0; + + ctx->touchpad_supported = (data[1] & 0x01) != 0; + ctx->joystick_rgb_supported = (data[1] & 0x02) != 0; + + SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN; + type = (SDL_GamepadType)SDL_clamp(data[2], SDL_GAMEPAD_TYPE_UNKNOWN, SDL_GAMEPAD_TYPE_COUNT); + device->type = type; + + // The 4 MSB represent a button layout style SDL_GamepadFaceStyle + // The 4 LSB represent a device sub-type + device->guid.data[15] = data[3]; + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput Face Style: %d", (data[3] & 0xF0) >> 4); + SDL_Log("SInput Sub-type: %d", (data[3] & 0xF)); +#endif + + ctx->polling_rate_ms = data[4]; + + ctx->accelRange = EXTRACTUINT16(data, 6); + ctx->gyroRange = EXTRACTUINT16(data, 8); + + // Masks in LSB to MSB + // South, East, West, North, DUp, DDown, DLeft, DRight + ctx->usage_masks[0] = data[10]; + + // Stick Left, Stick Right, L Shoulder, R Shoulder, + // L Trigger, R Trigger, L Paddle 1, R Paddle 1 + ctx->usage_masks[1] = data[11]; + + // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R + ctx->usage_masks[2] = data[12]; + + // Power, Misc 4 to 10 + ctx->usage_masks[3] = data[13]; + + // 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; + } + } + } + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Buttons count: %d", ctx->buttons_count); +#endif + + // Get and validate touchpad parameters + ctx->touchpad_count = data[14]; + ctx->touchpad_finger_count = data[15]; + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Accelerometer Range: %d", ctx->accelRange); +#endif + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Gyro Range: %d", ctx->gyroRange); +#endif + + ctx->accelScale = CalculateAccelScale(ctx->accelRange); + ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); +} + +static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device) +{ + int written = 0; + + // Attempt to send the SDL features get command. + for (int attempt = 0; attempt < 8; ++attempt) { + const Uint8 featuresGetCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_FEATURES }; + // This write will occasionally return -1, so ignore failure here and try again + written = SDL_hid_write(device->dev, featuresGetCommand, sizeof(featuresGetCommand)); + + if (written == SINPUT_DEVICE_REPORT_COMMAND_SIZE) { + break; + } + } + + if (written < SINPUT_DEVICE_REPORT_COMMAND_SIZE) { + SDL_SetError("SInput device SDL Features GET command could not write"); + return false; + } + + int read = 0; + + // Read the reply + for (int i = 0; i < 100; ++i) { + SDL_Delay(1); + + Uint8 data[USB_PACKET_LENGTH]; + read = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0); + if (read < 0) { + SDL_SetError("SInput device SDL Features GET command could not read"); + return false; + } + if (read == 0) { + continue; + } + +#ifdef DEBUG_SINPUT_PROTOCOL + HIDAPI_DumpPacket("SInput packet: size = %d", data, size); +#endif + + if ((read == USB_PACKET_LENGTH) && (data[0] == SINPUT_DEVICE_REPORT_ID_INPUT_CMDDAT) && (data[1] == SINPUT_DEVICE_COMMAND_FEATURES)) { + ProcessSDLFeaturesResponse(device, &(data[SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK])); +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Received SInput SDL Features command response"); +#endif + return true; + } + } + + return false; +} + +// Type 2 haptics are for more traditional rumble such as +// ERM motors or simulated ERM motors +static inline void HapticsType2Pack(SINPUT_HAPTIC_S *in, Uint8 *out) +{ + // Type of haptics + out[0] = 2; + + out[1] = in->type_2.left.amplitude; + out[2] = in->type_2.left.brake; + + out[3] = in->type_2.right.amplitude; + out[4] = in->type_2.right.brake; +} + +static void HIDAPI_DriverSInput_RegisterHints(SDL_HintCallback callback, void *userdata) +{ + SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SINPUT, callback, userdata); +} + +static void HIDAPI_DriverSInput_UnregisterHints(SDL_HintCallback callback, void *userdata) +{ + SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SINPUT, callback, userdata); +} + +static bool HIDAPI_DriverSInput_IsEnabled(void) +{ + return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_SINPUT, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT)); +} + +static bool HIDAPI_DriverSInput_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GamepadType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol) +{ + return SDL_IsJoystickSInputController(vendor_id, product_id); +} + +static bool HIDAPI_DriverSInput_InitDevice(SDL_HIDAPI_Device *device) +{ +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput device Init"); +#endif + + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)SDL_calloc(1, sizeof(*ctx)); + if (!ctx) { + return false; + } + + ctx->device = device; + device->context = ctx; + + if (!RetrieveSDLFeatures(device)) { + return false; + } + + return HIDAPI_JoystickConnected(device, NULL); +} + +static int HIDAPI_DriverSInput_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id) +{ + return -1; +} + +static void HIDAPI_DriverSInput_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->player_leds_supported) { + player_index = SDL_clamp(player_index + 1, 0, 255); + Uint8 player_num = (Uint8)player_index; + + ctx->player_idx = player_num; + + // Set player number, finalizing the setup + Uint8 playerLedCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_PLAYERLED, ctx->player_idx }; + int playerNumBytesWritten = SDL_hid_write(device->dev, playerLedCommand, SINPUT_DEVICE_REPORT_COMMAND_SIZE); + + if (playerNumBytesWritten < 0) { + SDL_SetError("SInput device player led command could not write"); + } + } +} + +#ifndef DEG2RAD +#define DEG2RAD(x) ((float)(x) * (float)(SDL_PI_F / 180.f)) +#endif + + +static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput device Open"); +#endif + + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + SDL_AssertJoysticksLocked(); + + joystick->nbuttons = ctx->buttons_count; + + 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; + } + + joystick->naxes = axes; + + if (ctx->accelerometer_supported) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, (float)1000.0f/ctx->polling_rate_ms); + } + + if (ctx->gyroscope_supported) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, (float)1000.0f / ctx->polling_rate_ms); + } + + if (ctx->touchpad_supported) { + // If touchpad is supported, minimum 1, max is capped + ctx->touchpad_count = SDL_clamp(ctx->touchpad_count, 1, SINPUT_MAX_ALLOWED_TOUCHPADS); + + if (ctx->touchpad_count > 1) { + // Support two separate touchpads with 1 finger each + // or support one touchpad with 2 fingers max + ctx->touchpad_finger_count = 1; + } + + if (ctx->touchpad_count > 0) { + SDL_PrivateJoystickAddTouchpad(joystick, ctx->touchpad_finger_count); + } + + if (ctx->touchpad_count > 1) { + SDL_PrivateJoystickAddTouchpad(joystick, ctx->touchpad_finger_count); + } + } + + return true; +} + +static bool HIDAPI_DriverSInput_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) +{ + + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->rumble_supported) { + SINPUT_HAPTIC_S hapticData = { 0 }; + Uint8 hapticReport[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_HAPTIC }; + + // Low Frequency = Left + // High Frequency = Right + hapticData.type_2.left.amplitude = (Uint8) (low_frequency_rumble >> 8); + hapticData.type_2.right.amplitude = (Uint8)(high_frequency_rumble >> 8); + + HapticsType2Pack(&hapticData, &(hapticReport[2])); + + SDL_HIDAPI_SendRumble(device, hapticReport, SINPUT_DEVICE_REPORT_COMMAND_SIZE); + + return true; + } + + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSInput_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) +{ + return SDL_Unsupported(); +} + +static Uint32 HIDAPI_DriverSInput_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + Uint32 caps = 0; + if (ctx->rumble_supported) { + caps |= SDL_JOYSTICK_CAP_RUMBLE; + } + + if (ctx->player_leds_supported) { + caps |= SDL_JOYSTICK_CAP_PLAYER_LED; + } + + if (ctx->joystick_rgb_supported) { + caps |= SDL_JOYSTICK_CAP_RGB_LED; + } + + return caps; +} + +static bool HIDAPI_DriverSInput_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->player_leds_supported) { + + // Set player number, finalizing the setup + Uint8 joystickRGBCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_JOYSTICKRGB, red, green, blue }; + int joystickRGBBytesWritten = SDL_hid_write(device->dev, joystickRGBCommand, SINPUT_DEVICE_REPORT_COMMAND_SIZE); + + if (joystickRGBBytesWritten < 0) { + SDL_SetError("SInput device joystick rgb command could not write"); + return false; + } + + return true; + } + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSInput_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size) +{ + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSInput_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->accelerometer_supported || ctx->gyroscope_supported) { + ctx->sensors_enabled = enabled; + return true; + } + return SDL_Unsupported(); +} + +static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverSInput_Context *ctx, Uint8 *data, int size) +{ + Sint16 axis = 0; + Sint16 accel = 0; + Sint16 gyro = 0; + Uint64 timestamp = SDL_GetTicksNS(); + float imu_values[3] = { 0 }; + Uint8 output_idx = 0; + + // Process digital buttons according to the supplied + // button mask to create a contiguous button input set + for (Uint8 processes = 0; processes < 4; ++processes) { + + Uint8 button_idx = SINPUT_REPORT_IDX_BUTTONS_0 + processes; + + for (Uint8 buttons = 0; buttons < 8; ++buttons) { + + // If a button is enabled by our usage mask + const Uint8 mask = (0x01 << buttons); + if ((ctx->usage_masks[processes] & mask) != 0) { + + bool down = (data[button_idx] & mask) != 0; + + if ( (output_idx < SDL_GAMEPAD_BUTTON_COUNT) && (ctx->last_state[button_idx] != data[button_idx]) ) { + SDL_SendJoystickButton(timestamp, joystick, output_idx, down); + } + + ++output_idx; + } + } + } + + // Analog inputs map to a signed Sint16 range of -32768 to 32767 from the device. + // Use an axis index because not all gamepads will have the same axis inputs. + Uint8 axis_idx = 0; + + // Left Analog Stick + axis = 0; // Reset axis value for joystick + if (ctx->left_analog_stick_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_LEFT_X); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_LEFT_Y); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + } + + // Right Analog Stick + axis = 0; // Reset axis value for joystick + if (ctx->right_analog_stick_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_RIGHT_X); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_RIGHT_Y); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + } + + // Left Analog Trigger + axis = SDL_MIN_SINT16; // Reset axis value for trigger + if (ctx->left_analog_trigger_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_LEFT_TRIGGER); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + } + + // Right Analog Trigger + axis = SDL_MIN_SINT16; // Reset axis value for trigger + if (ctx->right_analog_trigger_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_RIGHT_TRIGGER); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + } + + // Battery/Power state handling + if (ctx->last_state[SINPUT_REPORT_IDX_PLUG_STATUS] != data[SINPUT_REPORT_IDX_PLUG_STATUS] || + ctx->last_state[SINPUT_REPORT_IDX_CHARGE_LEVEL] != data[SINPUT_REPORT_IDX_CHARGE_LEVEL]) { + + SDL_PowerState state = SDL_POWERSTATE_NO_BATTERY; + Uint8 status = data[SINPUT_REPORT_IDX_PLUG_STATUS]; + int percent = data[SINPUT_REPORT_IDX_CHARGE_LEVEL]; + + percent = SDL_clamp(percent, 0, 100); // Ensure percent is within valid range + + switch (status) { + case 1: + state = SDL_POWERSTATE_NO_BATTERY; + percent = 0; + break; + case 2: + state = SDL_POWERSTATE_CHARGING; + break; + case 3: + state = SDL_POWERSTATE_CHARGED; + percent = 100; + break; + case 4: + state = SDL_POWERSTATE_ON_BATTERY; + break; + default: // Wired/No Battery Supported + state = SDL_POWERSTATE_UNKNOWN; + percent = 0; + break; + } + + if (state > 0) { + SDL_SendJoystickPowerInfo(joystick, state, percent); + } + } + + // Extract the IMU timestamp delta (in microseconds) + Uint16 imu_timestamp_delta = EXTRACTUINT16(data, SINPUT_REPORT_IDX_IMU_TIMESTAMP); + + // Check if we should process IMU data and if sensors are enabled + if ((imu_timestamp_delta > 0) && (ctx->sensors_enabled)) { + + // Process IMU timestamp by adding the delta to the accumulated timestamp and converting to nanoseconds + ctx->imu_timestamp += ((Uint64) imu_timestamp_delta * 1000); + + // Process Accelerometer + if (ctx->accelerometer_supported) { + + accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_Y); + imu_values[2] = -(float)accel * ctx->accelScale; // Y-axis acceleration + + accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_Z); + imu_values[1] = (float)accel * ctx->accelScale; // Z-axis acceleration + + accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_X); + imu_values[0] = -(float)accel * ctx->accelScale; // X-axis acceleration + + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, ctx->imu_timestamp, imu_values, 3); + } + + // Process Gyroscope + if (ctx->gyroscope_supported) { + + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_Y); + imu_values[2] = -(float)gyro * ctx->gyroScale; // Y-axis rotation + + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_Z); + imu_values[1] = (float)gyro * ctx->gyroScale; // Z-axis rotation + + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_X); + imu_values[0] = -(float)gyro * ctx->gyroScale; // X-axis rotation + + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, ctx->imu_timestamp, imu_values, 3); + } + } + + // Check if we should process touchpad + if (ctx->touchpad_supported && ctx->touchpad_count > 0) { + Uint8 touchpad = 0; + Uint8 finger = 0; + + Sint16 touch1X = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH1_X); + Sint16 touch1Y = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH1_Y); + Uint16 touch1P = EXTRACTUINT16(data, SINPUT_REPORT_IDX_TOUCH1_P); + + Sint16 touch2X = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH2_X); + Sint16 touch2Y = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH2_Y); + Uint16 touch2P = EXTRACTUINT16(data, SINPUT_REPORT_IDX_TOUCH2_P); + + SDL_SendJoystickTouchpad(timestamp, joystick, touchpad, finger, + touch1P > 0, + touch1X / 65536.0f + 0.5f, + touch1Y / 65536.0f + 0.5f, + touch1P / 32768.0f); + + if (ctx->touchpad_count > 1) { + ++touchpad; + } else if (ctx->touchpad_finger_count > 1) { + ++finger; + } + + if ((touchpad > 0) || (finger > 0)) { + SDL_SendJoystickTouchpad(timestamp, joystick, touchpad, finger, + touch2P > 0, + touch2X / 65536.0f + 0.5f, + touch2Y / 65536.0f + 0.5f, + touch2P / 32768.0f); + } + } + + SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state))); +} + +static bool HIDAPI_DriverSInput_UpdateDevice(SDL_HIDAPI_Device *device) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + SDL_Joystick *joystick = NULL; + Uint8 data[USB_PACKET_LENGTH]; + int size = 0; + + if (device->num_joysticks > 0) { + joystick = SDL_GetJoystickFromID(device->joysticks[0]); + } else { + return false; + } + + while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) { +#ifdef DEBUG_SINPUT_PROTOCOL + HIDAPI_DumpPacket("SInput packet: size = %d", data, size); +#endif + if (!joystick) { + continue; + } + + // Handle command response information + if (data[0] == SINPUT_DEVICE_REPORT_ID_JOYSTICK_INPUT) { + HIDAPI_DriverSInput_HandleStatePacket(joystick, ctx, data, size); + } + } + + if (size < 0) { + // Read error, device is disconnected + HIDAPI_JoystickDisconnected(device, device->joysticks[0]); + } + return (size >= 0); +} + +static void HIDAPI_DriverSInput_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ +} + +static void HIDAPI_DriverSInput_FreeDevice(SDL_HIDAPI_Device *device) +{ +} + +SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSInput = { + SDL_HINT_JOYSTICK_HIDAPI_SINPUT, + true, + HIDAPI_DriverSInput_RegisterHints, + HIDAPI_DriverSInput_UnregisterHints, + HIDAPI_DriverSInput_IsEnabled, + HIDAPI_DriverSInput_IsSupportedDevice, + HIDAPI_DriverSInput_InitDevice, + HIDAPI_DriverSInput_GetDevicePlayerIndex, + HIDAPI_DriverSInput_SetDevicePlayerIndex, + HIDAPI_DriverSInput_UpdateDevice, + HIDAPI_DriverSInput_OpenJoystick, + HIDAPI_DriverSInput_RumbleJoystick, + HIDAPI_DriverSInput_RumbleJoystickTriggers, + HIDAPI_DriverSInput_GetJoystickCapabilities, + HIDAPI_DriverSInput_SetJoystickLED, + HIDAPI_DriverSInput_SendJoystickEffect, + HIDAPI_DriverSInput_SetJoystickSensorsEnabled, + HIDAPI_DriverSInput_CloseJoystick, + HIDAPI_DriverSInput_FreeDevice, +}; + +#endif // SDL_JOYSTICK_HIDAPI_SINPUT + +#endif // SDL_JOYSTICK_HIDAPI diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index 5d26deafe8e3c..5124d97a91db2 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -97,6 +97,9 @@ static SDL_HIDAPI_DeviceDriver *SDL_HIDAPI_drivers[] = { #ifdef SDL_JOYSTICK_HIDAPI_FLYDIGI &SDL_HIDAPI_DriverFlydigi, #endif +#ifdef SDL_JOYSTICK_HIDAPI_SINPUT + &SDL_HIDAPI_DriverSInput, +#endif }; static int SDL_HIDAPI_numdrivers = 0; static SDL_AtomicInt SDL_HIDAPI_updating_devices; diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h index f6b8ebfae4053..e280c86aa70cf 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick_c.h +++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h @@ -44,6 +44,7 @@ #define SDL_JOYSTICK_HIDAPI_8BITDO #define SDL_JOYSTICK_HIDAPI_FLYDIGI #define SDL_JOYSTICK_HIDAPI_GIP +#define SDL_JOYSTICK_HIDAPI_SINPUT // Joystick capability definitions #define SDL_JOYSTICK_CAP_MONO_LED 0x00000001 @@ -165,6 +166,7 @@ extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteamHori; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverLg4ff; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_Driver8BitDo; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverFlydigi; +extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSInput; // Return true if a HID device is present and supported as a joystick of the given type extern bool HIDAPI_IsDeviceTypePresent(SDL_GamepadType type); diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index 35c92a5882e0f..a6187f0723348 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -59,6 +59,7 @@ #define USB_VENDOR_SWITCH 0x2563 #define USB_VENDOR_VALVE 0x28de #define USB_VENDOR_ZEROPLUS 0x0c12 +#define USB_VENDOR_RASPBERRYPI 0x2e8a // Commercial hardware from various companies are registered under this VID #define USB_PRODUCT_8BITDO_SF30_PRO 0x6000 // B + START #define USB_PRODUCT_8BITDO_SF30_PRO_BT 0x6100 // B + START @@ -160,6 +161,10 @@ #define USB_PRODUCT_XBOX_SERIES_X_BLE 0x0b13 #define USB_PRODUCT_XBOX_ONE_XBOXGIP_CONTROLLER 0x02ff // XBOXGIP driver software PID #define USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD 0x11ff +#define USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC 0x10c6 +#define USB_PRODUCT_HANDHELDLEGEND_PROGCC 0x10df +#define USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE 0x10dd +#define USB_PRODUCT_BONJIRICHANNEL_FIREBIRD 0x10e0 // USB usage pages #define USB_USAGEPAGE_GENERIC_DESKTOP 0x0001