SDL: support 8bitdo ultimate 3 (#15927)

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