From 5bee85408c66638a26f91150f919d159b8e37cba Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 7 May 2025 11:53:55 -0700
Subject: [PATCH] Cleanup 8BitDo HIDAPI support for SF30 Pro and SN30 Pro
This sets the correct number of buttons for older controllers, and adds parsing for older firmware USB reports
---
src/joystick/SDL_gamepad.c | 26 +++--
src/joystick/SDL_joystick.c | 18 ----
src/joystick/SDL_joystick_c.h | 3 -
src/joystick/hidapi/SDL_hidapi_8bitdo.c | 137 +++++++++++++++++++++---
src/joystick/usb_ids.h | 3 +-
5 files changed, 146 insertions(+), 41 deletions(-)
diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c
index 5f08f94752fd2..32d76df01158d 100644
--- a/src/joystick/SDL_gamepad.c
+++ b/src/joystick/SDL_gamepad.c
@@ -779,6 +779,20 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
}
break;
}
+ } else if (vendor == USB_VENDOR_8BITDO &&
+ (product == USB_PRODUCT_8BITDO_SN30_PRO ||
+ product == USB_PRODUCT_8BITDO_SN30_PRO_BT ||
+ product == USB_PRODUCT_8BITDO_PRO_2 ||
+ product == USB_PRODUCT_8BITDO_PRO_2_BT)) {
+ SDL_strlcat(mapping_string, "a:b1,b:b0,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,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));
+ if (product == USB_PRODUCT_8BITDO_PRO_2 || product == USB_PRODUCT_8BITDO_PRO_2_BT) {
+ SDL_strlcat(mapping_string, "paddle1:b14,paddle2:b13,", sizeof(mapping_string));
+ }
+ } else if (vendor == USB_VENDOR_8BITDO &&
+ (product == USB_PRODUCT_8BITDO_SF30_PRO ||
+ 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 {
// All other gamepads have the standard set of 19 buttons and 6 axes
if (SDL_IsJoystickGameCube(vendor, product)) {
@@ -802,20 +816,20 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
SDL_strlcat(mapping_string, "misc1:b11,", sizeof(mapping_string));
} else if (SDL_IsJoystickGoogleStadiaController(vendor, product)) {
// The Google Stadia controller has a share button and a Google Assistant button
- SDL_strlcat(mapping_string, "misc1:b11,misc2:b12", sizeof(mapping_string));
+ SDL_strlcat(mapping_string, "misc1:b11,misc2:b12,", sizeof(mapping_string));
} else if (SDL_IsJoystickNVIDIASHIELDController(vendor, product)) {
// The NVIDIA SHIELD controller has a share button between back and start buttons
SDL_strlcat(mapping_string, "misc1:b11,", sizeof(mapping_string));
if (product == USB_PRODUCT_NVIDIA_SHIELD_CONTROLLER_V103) {
// The original SHIELD controller has a touchpad and plus/minus buttons as well
- SDL_strlcat(mapping_string, "touchpad:b12,misc2:b13,misc3:b14", sizeof(mapping_string));
+ SDL_strlcat(mapping_string, "touchpad:b12,misc2:b13,misc3:b14,", sizeof(mapping_string));
}
} else if (SDL_IsJoystickHoriSteamController(vendor, product)) {
/* The Wireless HORIPad for Steam has QAM, Steam, Capsense L/R Sticks, 2 rear buttons, and 2 misc buttons */
- SDL_strlcat(mapping_string, "paddle1:b13,paddle2:b12,paddle3:b15,paddle4:b14,misc2:b11,misc3:b16,misc4:b17", sizeof(mapping_string));
- } else if (SDL_IsJoystick8BitDoController(vendor, product)) {
- SDL_strlcat(mapping_string, "paddle1:b12,paddle2:b11,paddle3:b14,paddle4:b13", sizeof(mapping_string));
+ SDL_strlcat(mapping_string, "paddle1:b13,paddle2:b12,paddle3:b15,paddle4:b14,misc2:b11,misc3:b16,misc4:b17,", sizeof(mapping_string));
+ } else if (vendor == USB_VENDOR_8BITDO && product == USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS) {
+ SDL_strlcat(mapping_string, "paddle1:b12,paddle2:b11,paddle3:b14,paddle4:b13,", sizeof(mapping_string));
} else {
switch (SDL_GetGamepadTypeFromGUID(guid, NULL)) {
case SDL_GAMEPAD_TYPE_PS4:
@@ -1295,7 +1309,7 @@ static bool SDL_PrivateParseGamepadElement(SDL_Gamepad *gamepad, const char *szG
static bool SDL_PrivateParseGamepadConfigString(SDL_Gamepad *gamepad, const char *pchString)
{
char szGameButton[20];
- char szJoystickButton[20];
+ char szJoystickButton[128];
bool bGameButton = true;
int i = 0;
const char *pchPos = pchString;
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index c88b15cbe3030..3caf227f015e4 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -3175,24 +3175,6 @@ 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_IsJoystick8BitDoController(Uint16 vendor_id, Uint16 product_id)
-{
- if (vendor_id == USB_VENDOR_8BITDO) {
- switch (product_id) {
- case USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS:
- case USB_PRODUCT_8BITDO_SN30_PRO:
- case USB_PRODUCT_8BITDO_SN30_PRO_BT:
- case USB_PRODUCT_8BITDO_SF30_PRO:
- case USB_PRODUCT_8BITDO_PRO_2:
- case USB_PRODUCT_8BITDO_PRO_2_BT:
- return true;
- default:
- break;
- }
- }
- return false;
-}
-
bool SDL_IsJoystickSteamDeck(Uint16 vendor_id, Uint16 product_id)
{
EControllerType eType = GuessControllerType(vendor_id, product_id);
diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h
index 61c7b32bbee91..6b82365c5865b 100644
--- a/src/joystick/SDL_joystick_c.h
+++ b/src/joystick/SDL_joystick_c.h
@@ -135,9 +135,6 @@ 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 a 8BitDo controller
-extern bool SDL_IsJoystick8BitDoController(Uint16 vendor_id, Uint16 product_id);
-
// Function to return whether a joystick is a Steam Deck
extern bool SDL_IsJoystickSteamDeck(Uint16 vendor_id, Uint16 product_id);
diff --git a/src/joystick/hidapi/SDL_hidapi_8bitdo.c b/src/joystick/hidapi/SDL_hidapi_8bitdo.c
index 1f12f85957cbb..82efaf701966f 100644
--- a/src/joystick/hidapi/SDL_hidapi_8bitdo.c
+++ b/src/joystick/hidapi/SDL_hidapi_8bitdo.c
@@ -126,7 +126,21 @@ static int ReadFeatureReport(SDL_hid_device *dev, Uint8 report_id, Uint8 *report
static bool HIDAPI_Driver8BitDo_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_IsJoystick8BitDoController(vendor_id, product_id);
+ if (vendor_id == USB_VENDOR_8BITDO) {
+ switch (product_id) {
+ case USB_PRODUCT_8BITDO_SF30_PRO:
+ case USB_PRODUCT_8BITDO_SF30_PRO_BT:
+ case USB_PRODUCT_8BITDO_SN30_PRO:
+ case USB_PRODUCT_8BITDO_SN30_PRO_BT:
+ case USB_PRODUCT_8BITDO_PRO_2:
+ case USB_PRODUCT_8BITDO_PRO_2_BT:
+ case USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS:
+ return true;
+ default:
+ break;
+ }
+ }
+ return false;
}
static bool HIDAPI_Driver8BitDo_InitDevice(SDL_HIDAPI_Device *device)
@@ -147,21 +161,24 @@ static bool HIDAPI_Driver8BitDo_InitDevice(SDL_HIDAPI_Device *device)
ctx->rumble_supported = true;
ctx->powerstate_supported = true;
}
- } else if (device->product_id == USB_PRODUCT_8BITDO_SN30_PRO || device->product_id == USB_PRODUCT_8BITDO_SN30_PRO_BT ||
- device->product_id == USB_PRODUCT_8BITDO_SF30_PRO || device->product_id == USB_PRODUCT_8BITDO_PRO_2 ||
- device->product_id == USB_PRODUCT_8BITDO_PRO_2_BT) {
+ } else {
Uint8 data[USB_PACKET_LENGTH];
int size = ReadFeatureReport(device->dev, SDL_8BITDO_FEATURE_REPORTID_ENABLE_SDL_REPORTID, data, sizeof(data));
if (size > 0) {
ctx->sensors_supported = true;
ctx->rumble_supported = true;
ctx->powerstate_supported = true;
- } else {
- SDL_LogDebug(SDL_LOG_CATEGORY_INPUT,
- "HIDAPI_Driver8BitDo_InitDevice(): Couldn't read feature report 0x06");
}
}
+ if (device->product_id == USB_PRODUCT_8BITDO_SF30_PRO || device->product_id == USB_PRODUCT_8BITDO_SF30_PRO_BT) {
+ HIDAPI_SetDeviceName(device, "8BitDo SF30 Pro");
+ } else if (device->product_id == USB_PRODUCT_8BITDO_SN30_PRO || device->product_id == USB_PRODUCT_8BITDO_SN30_PRO_BT) {
+ HIDAPI_SetDeviceName(device, "8BitDo SN30 Pro");
+ } else if (device->product_id == USB_PRODUCT_8BITDO_PRO_2 || device->product_id == USB_PRODUCT_8BITDO_PRO_2_BT) {
+ HIDAPI_SetDeviceName(device, "8BitDo Pro 2");
+ }
+
return HIDAPI_JoystickConnected(device, NULL);
}
@@ -187,7 +204,14 @@ static bool HIDAPI_Driver8BitDo_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys
SDL_zeroa(ctx->last_state);
// Initialize the joystick capabilities
- joystick->nbuttons = SDL_GAMEPAD_NUM_8BITDO_BUTTONS;
+ if (device->product_id == USB_PRODUCT_8BITDO_PRO_2 ||
+ device->product_id == USB_PRODUCT_8BITDO_PRO_2_BT ||
+ device->product_id == USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS) {
+ // This controller has additional buttons
+ joystick->nbuttons = SDL_GAMEPAD_NUM_8BITDO_BUTTONS;
+ } else {
+ joystick->nbuttons = 11;
+ }
joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
joystick->nhats = 1;
@@ -257,12 +281,95 @@ static bool HIDAPI_Driver8BitDo_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *dev
}
return SDL_Unsupported();
}
+
+static void HIDAPI_Driver8BitDo_HandleOldStatePacket(SDL_Joystick *joystick, SDL_Driver8BitDo_Context *ctx, Uint8 *data, int size)
+{
+ Sint16 axis;
+ Uint64 timestamp = SDL_GetTicksNS();
+
+ if (ctx->last_state[2] != data[2]) {
+ Uint8 hat;
+
+ switch (data[2]) {
+ case 0:
+ hat = SDL_HAT_UP;
+ break;
+ case 1:
+ hat = SDL_HAT_RIGHTUP;
+ break;
+ case 2:
+ hat = SDL_HAT_RIGHT;
+ break;
+ case 3:
+ hat = SDL_HAT_RIGHTDOWN;
+ break;
+ case 4:
+ hat = SDL_HAT_DOWN;
+ break;
+ case 5:
+ hat = SDL_HAT_LEFTDOWN;
+ break;
+ case 6:
+ hat = SDL_HAT_LEFT;
+ break;
+ case 7:
+ hat = SDL_HAT_LEFTUP;
+ break;
+ default:
+ hat = SDL_HAT_CENTERED;
+ break;
+ }
+ SDL_SendJoystickHat(timestamp, joystick, 0, hat);
+ }
+
+ if (ctx->last_state[0] != data[0]) {
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[0] & 0x01) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[0] & 0x02) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[0] & 0x08) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[0] & 0x10) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[0] & 0x40) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[0] & 0x80) != 0));
+ }
+
+ if (ctx->last_state[1] != data[1]) {
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[1] & 0x10) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[1] & 0x04) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[1] & 0x08) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[1] & 0x20) != 0));
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[1] & 0x40) != 0));
+
+ SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, (data[1] & 0x01) ? SDL_MAX_SINT16 : SDL_MIN_SINT16);
+ SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, (data[1] & 0x02) ? SDL_MAX_SINT16 : SDL_MIN_SINT16);
+ }
+
+#define READ_STICK_AXIS(offset) \
+ (data[offset] == 0x7f ? 0 : (Sint16)HIDAPI_RemapVal((float)((int)data[offset] - 0x7f), -0x7f, 0xff - 0x7f, SDL_MIN_SINT16, SDL_MAX_SINT16))
+ {
+ axis = READ_STICK_AXIS(3);
+ SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
+ axis = READ_STICK_AXIS(4);
+ SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
+ axis = READ_STICK_AXIS(5);
+ SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
+ axis = READ_STICK_AXIS(6);
+ SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis);
+ }
+#undef READ_STICK_AXIS
+
+ SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state)));
+}
+
static void HIDAPI_Driver8BitDo_HandleStatePacket(SDL_Joystick *joystick, SDL_Driver8BitDo_Context *ctx, Uint8 *data, int size)
{
Sint16 axis;
Uint64 timestamp = SDL_GetTicksNS();
- if (data[0] != SDL_8BITDO_REPORTID_SDL_REPORTID && data[0] != SDL_8BITDO_REPORTID_NOT_SUPPORTED_SDL_REPORTID &&
- data[0] != SDL_8BITDO_BT_REPORTID_SDL_REPORTID) {
+
+ switch (data[0]) {
+ case SDL_8BITDO_REPORTID_NOT_SUPPORTED_SDL_REPORTID: // Firmware without enhanced mode
+ case SDL_8BITDO_REPORTID_SDL_REPORTID: // Enhanced mode USB report
+ case SDL_8BITDO_BT_REPORTID_SDL_REPORTID: // Enhanced mode Bluetooth report
+ break;
+ default:
// We don't know how to handle this report
return;
}
@@ -323,7 +430,7 @@ static void HIDAPI_Driver8BitDo_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[9] & 0x40) != 0));
}
- if (ctx->last_state[10] != data[10]) {
+ if (size > 10 && ctx->last_state[10] != data[10]) {
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_8BITDO_L4, ((data[10] & 0x01) != 0));
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_8BITDO_R4, ((data[10] & 0x02) != 0));
}
@@ -381,7 +488,6 @@ static void HIDAPI_Driver8BitDo_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr
SDL_SendJoystickPowerInfo(joystick, state, percent);
}
-
if (ctx->sensors_enabled) {
Uint64 sensor_timestamp;
float values[3];
@@ -440,7 +546,12 @@ static bool HIDAPI_Driver8BitDo_UpdateDevice(SDL_HIDAPI_Device *device)
continue;
}
- HIDAPI_Driver8BitDo_HandleStatePacket(joystick, ctx, data, size);
+ if (size == 9) {
+ // Old firmware USB report for the SF30 Pro and SN30 Pro controllers
+ HIDAPI_Driver8BitDo_HandleOldStatePacket(joystick, ctx, data, size);
+ } else {
+ HIDAPI_Driver8BitDo_HandleStatePacket(joystick, ctx, data, size);
+ }
}
if (size < 0) {
diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h
index 812d0a4805fe5..323283f6ea879 100644
--- a/src/joystick/usb_ids.h
+++ b/src/joystick/usb_ids.h
@@ -60,9 +60,10 @@
#define USB_VENDOR_ZEROPLUS 0x0c12
#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012
+#define USB_PRODUCT_8BITDO_SF30_PRO 0x6000 // B + START
+#define USB_PRODUCT_8BITDO_SF30_PRO_BT 0x6100 // B + START
#define USB_PRODUCT_8BITDO_SN30_PRO 0x6001 // B + START
#define USB_PRODUCT_8BITDO_SN30_PRO_BT 0x6101 // B + START
-#define USB_PRODUCT_8BITDO_SF30_PRO 0x6000 // B + START
#define USB_PRODUCT_8BITDO_PRO_2 0x6003 // mode switch to D
#define USB_PRODUCT_8BITDO_PRO_2_BT 0x6006 // mode switch to D
#define USB_PRODUCT_AMAZON_LUNA_CONTROLLER 0x0419