From efa0e2d5edcda11f415d84ca4f5efe73632c4671 Mon Sep 17 00:00:00 2001
From: 8BitDo <[EMAIL REDACTED]>
Date: Mon, 6 Jul 2026 20:50:38 +0800
Subject: [PATCH] support 8bitdo ultimate 3 (#15927)
---
src/joystick/hidapi/SDL_hidapi_8bitdo.c | 84 ++++++++++++++++++++++++-
src/joystick/usb_ids.h | 1 +
2 files changed, 82 insertions(+), 3 deletions(-)
diff --git a/src/joystick/hidapi/SDL_hidapi_8bitdo.c b/src/joystick/hidapi/SDL_hidapi_8bitdo.c
index 6b1dece533ebb..8c607c2fb3a2b 100644
--- a/src/joystick/hidapi/SDL_hidapi_8bitdo.c
+++ b/src/joystick/hidapi/SDL_hidapi_8bitdo.c
@@ -42,6 +42,13 @@ enum
SDL_GAMEPAD_NUM_8BITDO_BUTTONS,
};
+enum
+{
+ SDL_GAMEPAD_BUTTON_8BITDO_SHARE = 15,
+ SDL_GAMEPAD_NUM_8BITDO_ULTIMATE3_BUTTONS,
+};
+
+#define SDL_8BITDO_FEATURE_REPORTID 0x30
#define SDL_8BITDO_FEATURE_REPORTID_ENABLE_SDL_REPORTID 0x06
#define SDL_8BITDO_REPORTID_SDL_REPORTID 0x04
#define SDL_8BITDO_REPORTID_NOT_SUPPORTED_SDL_REPORTID 0x03
@@ -65,6 +72,7 @@ typedef struct
bool touchpad_01_supported;
bool touchpad_02_supported;
bool rumble_supported;
+ bool trigger_rumble_supported;
bool rumble_type;
bool rgb_supported;
bool player_led_supported;
@@ -78,6 +86,10 @@ typedef struct
Uint8 last_state[USB_PACKET_LENGTH];
Uint64 sensor_timestamp; // Nanoseconds. Simulate onboard clock. Different models have different rates vs different connection styles.
Uint64 sensor_timestamp_interval;
+ Uint8 rumble_left_value;
+ Uint8 rumble_right_value;
+ Uint8 trigger_rumble_left_value;
+ Uint8 trigger_rumble_right_value;
Uint32 last_tick;
} SDL_Driver8BitDo_Context;
@@ -147,6 +159,7 @@ static bool HIDAPI_Driver8BitDo_IsSupportedDevice(SDL_HIDAPI_Device *device, con
case USB_PRODUCT_8BITDO_PRO_2_BT:
case USB_PRODUCT_8BITDO_PRO_3:
case USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS:
+ case USB_PRODUCT_8BITDO_ULTIMATE3:
return true;
default:
break;
@@ -182,7 +195,41 @@ static bool HIDAPI_Driver8BitDo_InitDevice(SDL_HIDAPI_Device *device)
}
break;
}
- } else {
+ } else if (device->product_id == USB_PRODUCT_8BITDO_ULTIMATE3) {
+ // Supported by default
+ ctx->sensors_supported = true;
+ ctx->rumble_supported = true;
+ ctx->powerstate_supported = true;
+ ctx->sensor_timestamp_supported = true;
+ Uint8 data[USB_PACKET_LENGTH];
+ const int MAX_ATTEMPTS = 5;
+ for (int attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
+ int size = ReadFeatureReport(device->dev, SDL_8BITDO_FEATURE_REPORTID, data, sizeof(data));
+ if (size > 0) {
+ if (data[3] == 0) {
+ ctx->rumble_supported = false;
+ ctx->trigger_rumble_supported = false;
+ }
+ if (data[3] & 0x02) {
+ ctx->trigger_rumble_supported = true;
+ }
+
+ if (data[4] == 0) {
+ ctx->sensors_supported = false;
+ ctx->sensor_timestamp_supported = false;
+ }
+ if (size >= 17 && data[11] != 0) {
+ char serial[18];
+ (void)SDL_snprintf(serial, sizeof(serial), "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x",
+ data[16], data[15], data[14], data[13], data[12], data[11]);
+ HIDAPI_SetDeviceSerial(device, serial);
+ }
+ break;
+ }
+ SDL_Delay(10);
+ }
+ }
+ else {
Uint8 data[USB_PACKET_LENGTH];
const int MAX_ATTEMPTS = 5;
for (int attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
@@ -277,6 +324,8 @@ static Uint64 HIDAPI_Driver8BitDo_GetIMURateForProductID(SDL_HIDAPI_Device *devi
// This firmware appears to update at 1000 Hz over USB dongle
return 1000;
}
+ case USB_PRODUCT_8BITDO_ULTIMATE3:
+ return 120;
default:
return 120;
}
@@ -299,8 +348,10 @@ static bool HIDAPI_Driver8BitDo_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys
device->product_id == USB_PRODUCT_8BITDO_PRO_2_BT ||
device->product_id == USB_PRODUCT_8BITDO_PRO_3 ||
device->product_id == USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS) {
- // This controller has additional buttons
+ // This controller has additional buttons
joystick->nbuttons = SDL_GAMEPAD_NUM_8BITDO_BUTTONS;
+ } else if (device->product_id == USB_PRODUCT_8BITDO_ULTIMATE3) {
+ joystick->nbuttons = SDL_GAMEPAD_NUM_8BITDO_ULTIMATE3_BUTTONS;
} else {
joystick->nbuttons = 11;
}
@@ -331,6 +382,12 @@ static bool HIDAPI_Driver8BitDo_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Jo
Uint8 rumble_packet[5] = { 0x05, 0x00, 0x00, 0x00, 0x00 };
rumble_packet[1] = low_frequency_rumble >> 8;
rumble_packet[2] = high_frequency_rumble >> 8;
+ if (ctx->trigger_rumble_supported) {
+ rumble_packet[3] = ctx->trigger_rumble_left_value;
+ rumble_packet[4] = ctx->trigger_rumble_right_value;
+ ctx->rumble_left_value = rumble_packet[1];
+ ctx->rumble_right_value = rumble_packet[2];
+ }
if (SDL_HIDAPI_SendRumble(device, rumble_packet, sizeof(rumble_packet)) != sizeof(rumble_packet)) {
return SDL_SetError("Couldn't send rumble packet");
@@ -343,7 +400,25 @@ static bool HIDAPI_Driver8BitDo_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Jo
static bool HIDAPI_Driver8BitDo_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
{
- return SDL_Unsupported();
+ SDL_Driver8BitDo_Context* ctx = (SDL_Driver8BitDo_Context*)device->context;
+ if (ctx->trigger_rumble_supported) {
+ Uint8 rumble_packet[5] = { 0x05, 0x00, 0x00, 0x00, 0x00 };
+ rumble_packet[3] = left_rumble >> 8;
+ rumble_packet[4] = right_rumble >> 8;
+ if (ctx->rumble_supported) {
+ rumble_packet[1] = ctx->rumble_left_value;
+ rumble_packet[2] = ctx->rumble_right_value;
+ ctx->trigger_rumble_left_value = rumble_packet[3];
+ ctx->trigger_rumble_right_value = rumble_packet[4];
+ }
+
+ if (SDL_HIDAPI_SendRumble(device, rumble_packet, sizeof(rumble_packet)) != sizeof(rumble_packet)) {
+ return SDL_SetError("Couldn't send rumble packet");
+ }
+ return true;
+ } else {
+ return SDL_Unsupported();
+ }
}
static Uint32 HIDAPI_Driver8BitDo_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
@@ -525,6 +600,9 @@ static void HIDAPI_Driver8BitDo_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[9] & 0x08) != 0));
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[9] & 0x20) != 0));
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[9] & 0x40) != 0));
+ if (joystick->nbuttons >= SDL_GAMEPAD_NUM_8BITDO_ULTIMATE3_BUTTONS) {
+ SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_8BITDO_SHARE, ((data[9] & 0x80) != 0));
+ }
}
if (size > 10 && ctx->last_state[10] != data[10]) {
diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h
index a91117f9c0b22..108a0e86c0e9e 100644
--- a/src/joystick/usb_ids.h
+++ b/src/joystick/usb_ids.h
@@ -78,6 +78,7 @@
#define USB_PRODUCT_8BITDO_PRO_2_BT 0x6006 // mode switch to D
#define USB_PRODUCT_8BITDO_PRO_3 0x6009 // mode switch to D
#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 // mode switch to BT
+#define USB_PRODUCT_8BITDO_ULTIMATE3 0x202f // mode switch to BT
#define USB_PRODUCT_AMAZON_LUNA_CONTROLLER 0x0419
#define USB_PRODUCT_ASTRO_C40_XBOX360 0x0024
#define USB_PRODUCT_BACKBONE_ONE_IOS 0x0103