SDL: Added support for the Switch 2 Joy-Cons with charging grip

From 4b93e7488f10f5d713cb12ef04deb2bec4c55481 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 30 Aug 2025 09:52:08 -0700
Subject: [PATCH] Added support for the Switch 2 Joy-Cons with charging grip

---
 src/hidapi/SDL_hidapi.c                  |   4 +-
 src/joystick/SDL_gamepad.c               |  34 +-
 src/joystick/SDL_joystick.c              |  16 +-
 src/joystick/controller_list.h           |   4 +
 src/joystick/hidapi/SDL_hidapi_switch.c  |  10 +-
 src/joystick/hidapi/SDL_hidapi_switch2.c | 594 ++++++++++++++++++-----
 src/joystick/hidapi/SDL_hidapijoystick.c |  10 +-
 src/joystick/usb_ids.h                   |   7 +-
 8 files changed, 549 insertions(+), 130 deletions(-)

diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c
index 7ec04e203998d..275a12553a8cf 100644
--- a/src/hidapi/SDL_hidapi.c
+++ b/src/hidapi/SDL_hidapi.c
@@ -844,8 +844,10 @@ static const struct {
     Uint16 product;
 } SDL_libusb_whitelist[] = {
     { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_GAMECUBE_ADAPTER },
-    { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_PRO },
     { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER },
+    { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT },
+    { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT },
+    { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_PRO },
 };
 
 static bool IsInWhitelist(Uint16 vendor, Uint16 product)
diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c
index 91de1d3b6e3c8..c5ef232b31b7c 100644
--- a/src/joystick/SDL_gamepad.c
+++ b/src/joystick/SDL_gamepad.c
@@ -716,12 +716,32 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
         // GameCube driver has 12 buttons and 6 axes
         SDL_strlcat(mapping_string, "a:b0,b:b2,dpdown:b6,dpleft:b4,dpright:b5,dpup:b7,lefttrigger:a4,leftx:a0,lefty:a1~,rightshoulder:b9,righttrigger:a5,rightx:a2,righty:a3~,start:b8,x:b1,y:b3,misc3:b11,misc4:b10,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string));
     } else if (vendor == USB_VENDOR_NINTENDO &&
-               (product == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER)) {
-        // Switch 2 GameCube has additional buttons for ZL and C
-        SDL_strlcat(mapping_string, "a:b1,b:b3,dpdown:b8,dpleft:b10,dpright:b9,dpup:b11,guide:b16,leftshoulder:b13,lefttrigger:a4,leftx:a0,lefty:a1~,misc1:b17,misc2:b20,misc3:b4,misc4:b12,rightshoulder:b5,righttrigger:a5,rightx:a2,righty:a3~,start:b6,x:b0,y:b2,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string));
+               product == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) {
+        SDL_strlcat(mapping_string, "a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,leftshoulder:b6,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a5,rightx:a2,righty:a3,start:b5,x:b2,y:b3,misc1:b8,misc2:b9,misc3:b10,misc4:b11,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string));
     } else if (vendor == USB_VENDOR_NINTENDO &&
-               (product == USB_PRODUCT_NINTENDO_SWITCH2_PRO)) {
-        SDL_strlcat(mapping_string, "a:b0,b:b1,dpdown:b8,dpleft:b10,dpright:b9,dpup:b11,guide:b16,leftshoulder:b12,lefttrigger:b13,leftx:a0,lefty:a1~,misc1:b17,misc2:b20,rightshoulder:b4,righttrigger:b5,rightx:a2,righty:a3~,start:b6,back:b14,x:b2,y:b3,leftstick:b15,rightstick:b7,paddle1:b18,paddle2:b19,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
+               product == USB_PRODUCT_NINTENDO_SWITCH2_PRO) {
+        SDL_strlcat(mapping_string, "a:b0,b:b1,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:b2,y:b3,misc1:b11,misc2:b12,paddle1:b13,paddle2:b14,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT) {
+        if (SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false)) {
+            // Vertical mode
+            SDL_strlcat(mapping_string, "back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b11,paddle2:b14,paddle4:b16,", sizeof(mapping_string));
+        } else {
+            // Mini gamepad mode
+            SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b2,y:b3,paddle2:b14,paddle4:b16,", sizeof(mapping_string));
+        }
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT) {
+        if (SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false)) {
+            // Vertical mode
+            SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,misc2:b12,paddle1:b13,paddle3:b15,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
+        } else {
+            // Mini gamepad mode
+            SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b2,y:b3,misc2:b12,paddle1:b13,paddle3:b15,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
+        }
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR) {
+        SDL_strlcat(mapping_string, "a:b0,b:b1,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:b2,y:b3,misc1:b11,misc2:b12,paddle1:b13,paddle2:b14,paddle3:b15,paddle4:b16,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string));
     } else if (vendor == USB_VENDOR_NINTENDO &&
                (guid.data[15] == k_eSwitchDeviceInfoControllerType_HVCLeft ||
                 guid.data[15] == k_eSwitchDeviceInfoControllerType_HVCRight ||
@@ -840,7 +860,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
                 // 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) {
@@ -850,7 +870,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
                 break;
             }
             break;
-        
+
         case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD:
         default:
             // Unmapped device
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index d88a479186b77..a6d047bd14113 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -2953,10 +2953,14 @@ SDL_GamepadType SDL_GetGamepadTypeFromVIDPID(Uint16 vendor, Uint16 product, cons
     } else if (vendor == 0x0001 && product == 0x0001) {
         type = SDL_GAMEPAD_TYPE_STANDARD;
 
-    } else if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT) {
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT ||
+                product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT)) {
         type = SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT;
 
-    } else if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT) {
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT ||
+                product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT)) {
         if (name && SDL_strstr(name, "NES Controller") != NULL) {
             // We don't have a type for the Nintendo Online NES Controller
             type = SDL_GAMEPAD_TYPE_STANDARD;
@@ -2971,7 +2975,9 @@ SDL_GamepadType SDL_GetGamepadTypeFromVIDPID(Uint16 vendor, Uint16 product, cons
             type = SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT;
         }
 
-    } else if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR) {
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR ||
+                product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR)) {
         type = SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR;
 
     } else if (forUI && SDL_IsJoystickGameCube(vendor, product)) {
@@ -3161,7 +3167,9 @@ bool SDL_IsJoystickNintendoSwitchJoyConGrip(Uint16 vendor_id, Uint16 product_id)
 
 bool SDL_IsJoystickNintendoSwitchJoyConPair(Uint16 vendor_id, Uint16 product_id)
 {
-    return vendor_id == USB_VENDOR_NINTENDO && product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR;
+    return vendor_id == USB_VENDOR_NINTENDO &&
+           (product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR ||
+            product_id == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR);
 }
 
 bool SDL_IsJoystickGameCube(Uint16 vendor_id, Uint16 product_id)
diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h
index cd7c4871e50dd..f1994e3756cdb 100644
--- a/src/joystick/controller_list.h
+++ b/src/joystick/controller_list.h
@@ -540,8 +540,11 @@ static const ControllerDescription_t arrControllers[] = {
 	{ MAKE_CONTROLLER_ID( 0x05ac, 0x0002 ), k_eControllerType_AppleController, NULL },	// MFI Standard Gamepad (generic entry for iOS/tvOS)
 
     { MAKE_CONTROLLER_ID( 0x057e, 0x2006 ), k_eControllerType_SwitchJoyConLeft, NULL },    // Nintendo Switch Joy-Con (Left)
+    { MAKE_CONTROLLER_ID( 0x057e, 0x2067 ), k_eControllerType_SwitchJoyConLeft, NULL },    // Nintendo Switch 2 Joy-Con (Left)
     { MAKE_CONTROLLER_ID( 0x057e, 0x2007 ), k_eControllerType_SwitchJoyConRight, NULL },   // Nintendo Switch Joy-Con (Right)
+    { MAKE_CONTROLLER_ID( 0x057e, 0x2066 ), k_eControllerType_SwitchJoyConRight, NULL },   // Nintendo Switch 2 Joy-Con (Right)
     { MAKE_CONTROLLER_ID( 0x057e, 0x2008 ), k_eControllerType_SwitchJoyConPair, NULL },    // Nintendo Switch Joy-Con (Left+Right Combined)
+    { MAKE_CONTROLLER_ID( 0x057e, 0x2068 ), k_eControllerType_SwitchJoyConPair, NULL },    // Nintendo Switch 2 Joy-Con (Left+Right Combined)
 
     // This same controller ID is spoofed by many 3rd-party Switch controllers.
     // The ones we currently know of are:
@@ -550,6 +553,7 @@ static const ControllerDescription_t arrControllers[] = {
     // * ZhiXu Gamepad Wireless
     // * Sunwaytek Wireless Motion Controller for Nintendo Switch
 	{ MAKE_CONTROLLER_ID( 0x057e, 0x2009 ), k_eControllerType_SwitchProController, NULL },        // Nintendo Switch Pro Controller
+	{ MAKE_CONTROLLER_ID( 0x057e, 0x2069 ), k_eControllerType_SwitchProController, NULL },        // Nintendo Switch 2 Pro Controller
     //{ MAKE_CONTROLLER_ID( 0x057e, 0x2017 ), k_eControllerType_SwitchProController, NULL },        // Nintendo Online SNES Controller
     //{ MAKE_CONTROLLER_ID( 0x057e, 0x2019 ), k_eControllerType_SwitchProController, NULL },        // Nintendo Online N64 Controller
     //{ MAKE_CONTROLLER_ID( 0x057e, 0x201e ), k_eControllerType_SwitchProController, NULL },        // Nintendo Online SEGA Genesis Controller
diff --git a/src/joystick/hidapi/SDL_hidapi_switch.c b/src/joystick/hidapi/SDL_hidapi_switch.c
index 7038e4f405bbf..92d14ab2f65df 100644
--- a/src/joystick/hidapi/SDL_hidapi_switch.c
+++ b/src/joystick/hidapi/SDL_hidapi_switch.c
@@ -1388,7 +1388,15 @@ static bool HIDAPI_DriverSwitch_IsSupportedDevice(SDL_HIDAPI_Device *device, con
         return false;
     }
 
-    return (type == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO);
+    if (type != SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO) {
+        return false;
+    }
+
+    // The Nintendo Switch 2 Pro uses another driver
+    if (vendor_id == USB_VENDOR_NINTENDO && product_id == USB_PRODUCT_NINTENDO_SWITCH2_PRO) {
+        return false;
+    }
+    return true;
 }
 
 static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
diff --git a/src/joystick/hidapi/SDL_hidapi_switch2.c b/src/joystick/hidapi/SDL_hidapi_switch2.c
index 95d1cb46c4d67..22762709aa2e3 100644
--- a/src/joystick/hidapi/SDL_hidapi_switch2.c
+++ b/src/joystick/hidapi/SDL_hidapi_switch2.c
@@ -32,6 +32,44 @@
 
 #ifdef SDL_JOYSTICK_HIDAPI_SWITCH2
 
+// Define this if you want to log all packets from the controller
+#if 0
+#define DEBUG_SWITCH2_PROTOCOL
+#endif
+
+enum
+{
+    SDL_GAMEPAD_BUTTON_SWITCH2_PRO_SHARE = 11,
+    SDL_GAMEPAD_BUTTON_SWITCH2_PRO_C,
+    SDL_GAMEPAD_BUTTON_SWITCH2_PRO_RIGHT_PADDLE,
+    SDL_GAMEPAD_BUTTON_SWITCH2_PRO_LEFT_PADDLE,
+    SDL_GAMEPAD_NUM_SWITCH2_PRO_BUTTONS
+};
+
+enum
+{
+    SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_SHARE = 11,
+    SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_C,
+    SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE1,
+    SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE1,
+    SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE2,
+    SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE2,
+    SDL_GAMEPAD_NUM_SWITCH2_JOYCON_BUTTONS
+};
+
+enum
+{
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_GUIDE = 4,
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_START,
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_SHOULDER,
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_SHOULDER,
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_SHARE,
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_C,
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_TRIGGER,   // Full trigger pull click
+    SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_TRIGGER,  // Full trigger pull click
+    SDL_GAMEPAD_NUM_SWITCH2_GAMECUBE_BUTTONS
+};
+
 typedef struct
 {
     Uint16 neutral;
@@ -58,6 +96,9 @@ typedef struct
     Switch2_StickCalibration right_stick;
     Uint8 left_trigger_max;
     Uint8 right_trigger_max;
+
+    bool vertical_mode;
+    Uint8 last_state[USB_PACKET_LENGTH];
 } SDL_DriverSwitch2_Context;
 
 static void ParseStickCalibration(Switch2_StickCalibration *stick_data, const Uint8 *data)
@@ -127,7 +168,7 @@ static int RecvBulkData(SDL_DriverSwitch2_Context *ctx, Uint8 *data, unsigned si
     return total_transferred;
 }
 
-static void MapJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, int axis, const Switch2_AxisCalibration *calib, float value)
+static void MapJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, const Switch2_AxisCalibration *calib, float value, bool invert)
 {
     Sint16 mapped_value;
     if (calib && calib->neutral && calib->min && calib->max) {
@@ -141,10 +182,13 @@ static void MapJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, int axis,
     } else {
         mapped_value = (Sint16) HIDAPI_RemapVal(value, 0, 4096, SDL_MIN_SINT16, SDL_MAX_SINT16);
     }
+    if (invert) {
+        mapped_value = ~mapped_value;
+    }
     SDL_SendJoystickAxis(timestamp, joystick, axis, mapped_value);
 }
 
-static void MapTriggerAxis(Uint64 timestamp, SDL_Joystick *joystick, int axis, Uint8 max, float value)
+static void MapTriggerAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, Uint8 max, float value)
 {
     Sint16 mapped_value = (Sint16) HIDAPI_RemapVal(
         SDL_clamp((value - max) / (232.f - max), 0, 1),
@@ -174,6 +218,8 @@ static bool HIDAPI_DriverSwitch2_IsSupportedDevice(SDL_HIDAPI_Device *device, co
     if (vendor_id == USB_VENDOR_NINTENDO) {
         switch (product_id) {
         case USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER:
+        case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT:
+        case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT:
         case USB_PRODUCT_NINTENDO_SWITCH2_PRO:
             return true;
         }
@@ -366,122 +412,31 @@ static void HIDAPI_DriverSwitch2_SetDevicePlayerIndex(SDL_HIDAPI_Device *device,
 {
 }
 
-static bool HIDAPI_DriverSwitch2_UpdateDevice(SDL_HIDAPI_Device *device)
+static bool HIDAPI_DriverSwitch2_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
 {
     SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context;
 
-    const struct {
-        int byte;
-        unsigned char mask;
-    } buttons[] = {
-        {3, 0x01}, // B
-        {3, 0x02}, // A
-        {3, 0x04}, // Y
-        {3, 0x08}, // X
-        {3, 0x10}, // R (GameCube R Click)
-        {3, 0x20}, // ZR (GameCube Z)
-        {3, 0x40}, // PLUS (GameCube Start)
-        {3, 0x80}, // RS (not on GameCube)
-        {4, 0x01}, // DPAD_DOWN
-        {4, 0x02}, // DPAD_RIGHT
-        {4, 0x04}, // DPAD_LEFT
-        {4, 0x08}, // DPAD_UP
-        {4, 0x10}, // L (GameCube L Click)
-        {4, 0x20}, // ZL
-        {4, 0x40}, // MINUS (not on GameCube)
-        {4, 0x80}, // LS (not on GameCube)
-        {5, 0x01}, // Home
-        {5, 0x02}, // Capture
-        {5, 0x04}, // GR (not on GameCube)
-        {5, 0x08}, // GL (not on GameCube)
-        {5, 0x10}, // C
-    };
-
-    SDL_Joystick *joystick = NULL;
-    if (device->num_joysticks > 0) {
-        joystick = SDL_GetJoystickFromID(device->joysticks[0]);
-    }
-    if (joystick == NULL) {
-        return true;
-    }
-
-    // Read input packet
-
-    Uint8 packet[USB_PACKET_LENGTH];
-    int size;
-    while ((size = SDL_hid_read_timeout(device->dev, packet, sizeof(packet), 0)) > 0) {
-        if (size < 15) {
-            continue;
-        }
-
-        Uint64 timestamp = SDL_GetTicksNS();
-        for (size_t i = 0; i < SDL_arraysize(buttons); ++i) {
-            SDL_SendJoystickButton(
-                timestamp,
-                joystick,
-                (Uint8) i,
-                (packet[buttons[i].byte] & buttons[i].mask) != 0
-            );
-        }
-
-        MapJoystickAxis(
-            timestamp,
-            joystick,
-            SDL_GAMEPAD_AXIS_LEFTX,
-            &ctx->left_stick.x,
-            (float) (packet[6] | ((packet[7] & 0x0F) << 8))
-        );
-        MapJoystickAxis(
-            timestamp,
-            joystick,
-            SDL_GAMEPAD_AXIS_LEFTY,
-            &ctx->left_stick.y,
-            (float) ((packet[7] >> 4) | (packet[8] << 4))
-        );
-        MapJoystickAxis(
-            timestamp,
-            joystick,
-            SDL_GAMEPAD_AXIS_RIGHTX,
-            &ctx->right_stick.x,
-            (float) (packet[9] | ((packet[10] & 0x0F) << 8))
-        );
-        MapJoystickAxis(
-            timestamp,
-            joystick,
-            SDL_GAMEPAD_AXIS_RIGHTY,
-            &ctx->right_stick.y,
-            (float) ((packet[10] >> 4) | (packet[11] << 4))
-        );
-
-        if (device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) {
-            MapTriggerAxis(
-                timestamp,
-                joystick,
-                SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
-                ctx->left_trigger_max,
-                packet[13]
-            );
-            MapTriggerAxis(
-                timestamp,
-                joystick,
-                SDL_GAMEPAD_AXIS_RIGHT_TRIGGER,
-                ctx->right_trigger_max,
-                packet[14]
-            );
-        }
-    }
-    return true;
-}
-
-static bool HIDAPI_DriverSwitch2_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
-{
     // Initialize the joystick capabilities
-    joystick->nbuttons = 21;
-    if (device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) {
-        joystick->naxes = 6;
-    } else {
-        joystick->naxes = 4;
+    switch (device->product_id) {
+    case USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER:
+        joystick->nbuttons = SDL_GAMEPAD_NUM_SWITCH2_GAMECUBE_BUTTONS;
+        break;
+    case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT:
+    case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT:
+        joystick->nbuttons = SDL_GAMEPAD_NUM_SWITCH2_JOYCON_BUTTONS;
+        break;
+    case USB_PRODUCT_NINTENDO_SWITCH2_PRO:
+        joystick->nbuttons = SDL_GAMEPAD_NUM_SWITCH2_PRO_BUTTONS;
+        break;
+    default:
+        // FIXME: How many buttons does this have?
+        break;
     }
+    joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
+    joystick->nhats = 1;
+
+    // Set up for vertical mode
+    ctx->vertical_mode = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false);
 
     return true;
 }
@@ -516,6 +471,419 @@ static bool HIDAPI_DriverSwitch2_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *de
     return SDL_Unsupported();
 }
 
+static void HandleGameCubeState(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
+{
+    if (data[3] != ctx->last_state[3]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x02) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x04) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x08) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_TRIGGER, ((data[3] & 0x10) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_SHOULDER, ((data[3] & 0x20) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_START, ((data[3] & 0x40) != 0));
+    }
+
+    if (data[4] != ctx->last_state[4]) {
+        Uint8 hat = 0;
+
+        if (data[4] & 0x01) {
+            hat |= SDL_HAT_DOWN;
+        }
+        if (data[4] & 0x02) {
+            hat |= SDL_HAT_RIGHT;
+        }
+        if (data[4] & 0x04) {
+            hat |= SDL_HAT_LEFT;
+        }
+        if (data[4] & 0x08) {
+            hat |= SDL_HAT_UP;
+        }
+        SDL_SendJoystickHat(timestamp, joystick, 0, hat);
+
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_TRIGGER, ((data[4] & 0x10) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_SHOULDER, ((data[4] & 0x20) != 0));
+    }
+
+    if (data[5] != ctx->last_state[5]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_GUIDE, ((data[5] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_SHARE, ((data[5] & 0x02) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_C, ((data[5] & 0x10) != 0));
+    }
+
+    MapTriggerAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
+        ctx->left_trigger_max,
+        data[13]
+    );
+    MapTriggerAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_RIGHT_TRIGGER,
+        ctx->right_trigger_max,
+        data[14]
+    );
+
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTX,
+        &ctx->left_stick.x,
+        (float) (data[6] | ((data[7] & 0x0F) << 8)),
+        false
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTY,
+        &ctx->left_stick.y,
+        (float) ((data[7] >> 4) | (data[8] << 4)),
+        true
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_RIGHTX,
+        &ctx->right_stick.x,
+        (float) (data[9] | ((data[10] & 0x0F) << 8)),
+        false
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_RIGHTY,
+        &ctx->right_stick.y,
+        (float)((data[10] >> 4) | (data[11] << 4)),
+        true
+    );
+}
+
+static void HandleCombinedControllerStateL(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
+{
+    // FIXME: When we find out what the SL and SR buttons are, map them to paddles
+
+    if (data[3] != ctx->last_state[3]) {
+        Uint8 hat = 0;
+
+        if (data[3] & 0x01) {
+            hat |= SDL_HAT_DOWN;
+        }
+        if (data[3] & 0x02) {
+            hat |= SDL_HAT_RIGHT;
+        }
+        if (data[3] & 0x04) {
+            hat |= SDL_HAT_LEFT;
+        }
+        if (data[3] & 0x08) {
+            hat |= SDL_HAT_UP;
+        }
+        SDL_SendJoystickHat(timestamp, joystick, 0, hat);
+
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[3] & 0x10) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[3] & 0x40) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[3] & 0x80) != 0));
+    }
+
+    if (data[4] != ctx->last_state[4]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_PRO_SHARE, ((data[4] & 0x01) != 0));
+    }
+
+    Sint16 axis = (data[3] & 0x20) ? 32767 : -32768;
+    SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
+
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTX,
+        &ctx->left_stick.x,
+        (float) (data[6] | ((data[7] & 0x0F) << 8)),
+        false
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTY,
+        &ctx->left_stick.y,
+        (float) ((data[7] >> 4) | (data[8] << 4)),
+        true
+    );
+}
+
+static void HandleMiniControllerStateL(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
+{
+    // FIXME: When we find out what the SL and SR buttons are, map them to shoulder buttons
+
+    if (data[3] != ctx->last_state[3]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x02) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x04) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x08) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE1, ((data[3] & 0x10) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE2, ((data[3] & 0x20) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[3] & 0x80) != 0));
+    }
+
+    if (data[4] != ctx->last_state[4]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[4] & 0x01) != 0));
+    }
+
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTX,
+        &ctx->left_stick.y,
+        (float) ((data[7] >> 4) | (data[8] << 4)),
+        true
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTY,
+        &ctx->left_stick.x,
+        (float) (data[6] | ((data[7] & 0x0F) << 8)),
+        true
+    );
+}
+
+static void HandleCombinedControllerStateR(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
+{
+    // FIXME: When we find out what the SL and SR buttons are, map them to paddles
+
+    if (data[3] != ctx->last_state[3]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x02) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x04) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x08) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[3] & 0x10) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[3] & 0x80) != 0));
+    }
+
+    if (data[4] != ctx->last_state[4]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[4] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_C, ((data[4] & 0x10) != 0));
+    }
+
+    Sint16 axis = (data[3] & 0x20) ? 32767 : -32768;
+    SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis);
+
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_RIGHTX,
+        &ctx->left_stick.x,
+        (float) (data[6] | ((data[7] & 0x0F) << 8)),
+        false
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_RIGHTY,
+        &ctx->left_stick.y,
+        (float) ((data[7] >> 4) | (data[8] << 4)),
+        true
+    );
+}
+
+static void HandleMiniControllerStateR(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
+{
+    // FIXME: When we find out what the SL and SR buttons are, map them to shoulder buttons
+
+    if (data[3] != ctx->last_state[3]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x02) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x04) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x08) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE1, ((data[3] & 0x10) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE2, ((data[3] & 0x20) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[3] & 0x80) != 0));
+    }
+
+    if (data[4] != ctx->last_state[4]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[4] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_C, ((data[4] & 0x10) != 0));
+    }
+
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTX,
+        &ctx->left_stick.y,
+        (float) ((data[7] >> 4) | (data[8] << 4)),
+        false
+    );
+    MapJoystickAxis(
+        timestamp,
+        joystick,
+        SDL_GAMEPAD_AXIS_LEFTY,
+        &ctx->left_stick.x,
+        (float) (data[6] | ((data[7] & 0x0F) << 8)),
+        false
+    );
+}
+
+static void HandleSwitchProState(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
+{
+    Sint16 axis;
+
+    if (data[3] != ctx->last_state[3]) {
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x02) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x04) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x08) != 

(Patch may be truncated, please check the link at the top of this post.)