SDL: joystick: Rework GIP code to allow separate states for individual attachments

From 2248d3812e02e2541c767bf5cf6d37277fbd33a7 Mon Sep 17 00:00:00 2001
From: Vicki Pfau <[EMAIL REDACTED]>
Date: Tue, 6 May 2025 20:01:28 -0700
Subject: [PATCH] joystick: Rework GIP code to allow separate states for
 individual attachments

This is needed for future work bringing up things like the chatpad.

This commit also fixes a few minor things, such as still sending motor packets
to devices that don't support it, enabling quirks that hide trigger rumble on
devices that are marked as not having it, and fixing #12942.
---
 src/joystick/hidapi/SDL_hidapi_gip.c | 1026 ++++++++++++++++----------
 src/joystick/usb_ids.h               |    1 +
 2 files changed, 620 insertions(+), 407 deletions(-)

diff --git a/src/joystick/hidapi/SDL_hidapi_gip.c b/src/joystick/hidapi/SDL_hidapi_gip.c
index 81a51e10fba72..a1f72ce4bff91 100644
--- a/src/joystick/hidapi/SDL_hidapi_gip.c
+++ b/src/joystick/hidapi/SDL_hidapi_gip.c
@@ -38,6 +38,7 @@
 #endif
 
 #define MAX_MESSAGE_LENGTH 0x4000
+#define MAX_ATTACHMENTS 8
 
 #define GIP_DATA_CLASS_COMMAND (0u << 5)
 #define GIP_DATA_CLASS_LOW_LATENCY (1u << 5)
@@ -216,9 +217,11 @@
 #define GIP_FEATURE_SECURITY_OPT_OUT (1u << 4)
 #define GIP_FEATURE_MOTOR_CONTROL (1u << 5)
 #define GIP_FEATURE_GUIDE_COLOR (1u << 6)
+#define GIP_FEATURE_EXTENDED_SET_DEVICE_STATE (1u << 7)
 
 #define GIP_QUIRK_NO_HELLO (1u << 0)
 #define GIP_QUIRK_BROKEN_METADATA (1u << 1)
+#define GIP_QUIRK_NO_IMPULSE_VIBRATION (1u << 2)
 
 typedef enum
 {
@@ -240,7 +243,8 @@ typedef enum
     GIP_TYPE_WHEEL = 2,
     GIP_TYPE_FLIGHT_STICK = 3,
     GIP_TYPE_NAVIGATION_CONTROLLER = 4,
-} GIP_DeviceType;
+    GIP_TYPE_CHATPAD = 5,
+} GIP_AttachmentType;
 
 typedef enum
 {
@@ -272,6 +276,7 @@ SDL_COMPILE_TIME_ASSERT(GUID, sizeof(GUID) == 16);
 
 MAKE_GUID(GUID_ArcadeStick, 0x332054cc, 0xa34b, 0x41d5, 0xa3, 0x4a, 0xa6, 0xa6, 0x71, 0x1e, 0xc4, 0xb3);
 MAKE_GUID(GUID_DynamicLatencyInput, 0x87f2e56b, 0xc3bb, 0x49b1, 0x82, 0x65, 0xff, 0xff, 0xf3, 0x77, 0x99, 0xee);
+MAKE_GUID(GUID_FlightStick, 0x03f1a011, 0xefe9, 0x4cc1, 0x96, 0x9c, 0x38, 0xdc, 0x55, 0xf4, 0x04, 0xd0);
 MAKE_GUID(GUID_IConsoleFunctionMap_InputReport, 0xecddd2fe, 0xd387, 0x4294, 0xbd, 0x96, 0x1a, 0x71, 0x2e, 0x3d, 0xc7, 0x7d);
 MAKE_GUID(GUID_IConsoleFunctionMap_OverflowInputReport, 0x137d4bd0, 0x9347, 0x4472, 0xaa, 0x26, 0x8c, 0x34, 0xa0, 0x8f, 0xf9, 0xbd);
 MAKE_GUID(GUID_IController, 0x9776ff56, 0x9bfd, 0x4581, 0xad, 0x45, 0xb6, 0x45, 0xbb, 0xa5, 0x26, 0xd6);
@@ -302,31 +307,32 @@ typedef struct GIP_Quirks
 {
     Uint16 vendor_id;
     Uint16 product_id;
+    Uint8 attachment_index;
     Uint32 added_features;
     Uint32 filtered_features;
     Uint32 quirks;
     Uint32 extra_in_system[8];
     Uint32 extra_out_system[8];
-    GIP_DeviceType device_type;
+    GIP_AttachmentType device_type;
 } GIP_Quirks;
 
 static const GIP_Quirks quirks[] = {
-    { USB_VENDOR_MICROSOFT, USB_PRODUCT_XBOX_ONE_ELITE_SERIES_1,
+    { USB_VENDOR_MICROSOFT, USB_PRODUCT_XBOX_ONE_ELITE_SERIES_1, 0,
       .added_features = GIP_FEATURE_ELITE_BUTTONS,
       .filtered_features = GIP_FEATURE_CONSOLE_FUNCTION_MAP },
 
-    { USB_VENDOR_MICROSOFT, USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2,
-      .added_features = GIP_FEATURE_ELITE_BUTTONS | GIP_FEATURE_DYNAMIC_LATENCY_INPUT | GIP_FEATURE_CONSOLE_FUNCTION_MAP | GIP_FEATURE_GUIDE_COLOR,
+    { USB_VENDOR_MICROSOFT, USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2, 0,
+      .added_features = GIP_FEATURE_ELITE_BUTTONS | GIP_FEATURE_DYNAMIC_LATENCY_INPUT | GIP_FEATURE_CONSOLE_FUNCTION_MAP | GIP_FEATURE_GUIDE_COLOR | GIP_FEATURE_EXTENDED_SET_DEVICE_STATE,
       .extra_in_system = { 1 << GIP_CMD_FIRMWARE },
       .extra_out_system = { 1 << GIP_CMD_FIRMWARE } },
 
-    { USB_VENDOR_MICROSOFT, USB_PRODUCT_XBOX_SERIES_X,
+    { USB_VENDOR_MICROSOFT, USB_PRODUCT_XBOX_SERIES_X, 0,
       .added_features = GIP_FEATURE_DYNAMIC_LATENCY_INPUT },
 
-    { USB_VENDOR_PDP, USB_PRODUCT_PDP_ROCK_CANDY,
+    { USB_VENDOR_PDP, USB_PRODUCT_PDP_ROCK_CANDY, 0,
       .quirks = GIP_QUIRK_NO_HELLO },
 
-    { USB_VENDOR_POWERA, USB_PRODUCT_BDA_XB1_FIGHTPAD,
+    { USB_VENDOR_POWERA, USB_PRODUCT_BDA_XB1_FIGHTPAD, 0,
       .filtered_features = GIP_FEATURE_MOTOR_CONTROL },
 
     /*
@@ -335,10 +341,10 @@ static const GIP_Quirks quirks[] = {
      * However, since it just lets us bypass the metadata exchange, let's just
      * do that instead of having an unreliable init
      */
-    { USB_VENDOR_POWERA, USB_PRODUCT_BDA_XB1_CLASSIC,
-      .quirks = GIP_QUIRK_BROKEN_METADATA },
+    { USB_VENDOR_POWERA, USB_PRODUCT_BDA_XB1_CLASSIC, 0,
+      .quirks = GIP_QUIRK_BROKEN_METADATA | GIP_QUIRK_NO_IMPULSE_VIBRATION },
 
-    { USB_VENDOR_RAZER, USB_PRODUCT_RAZER_ATROX,
+    { USB_VENDOR_RAZER, USB_PRODUCT_RAZER_ATROX, 0,
       .filtered_features = GIP_FEATURE_MOTOR_CONTROL,
       .device_type = GIP_TYPE_ARCADE_STICK },
 
@@ -374,7 +380,7 @@ typedef struct GIP_DeviceMetadata
     GUID *supported_interfaces;
     Uint8 *hid_descriptor;
 
-    GIP_DeviceType device_type;
+    GIP_AttachmentType device_type;
 } GIP_DeviceMetadata;
 
 typedef struct GIP_MessageMetadata
@@ -398,9 +404,12 @@ typedef struct GIP_Metadata
     GIP_MessageMetadata *message_metadata;
 } GIP_Metadata;
 
-typedef struct GIP_Device
+struct GIP_Device;
+typedef struct GIP_Attachment
 {
-    SDL_HIDAPI_Device *device;
+    struct GIP_Device *device;
+    Uint8 attachment_index;
+    SDL_JoystickID joystick;
 
     Uint8 fragment_message;
     Uint16 total_length;
@@ -412,9 +421,6 @@ typedef struct GIP_Device
     Uint16 firmware_major_version;
     Uint16 firmware_minor_version;
 
-    Uint64 hello_deadline;
-    bool got_hello;
-
     GIP_MetadataStatus got_metadata;
     Uint64 metadata_next;
     int metadata_retries;
@@ -438,14 +444,24 @@ typedef struct GIP_Device
 
     Uint8 last_input[64];
 
-    bool reset_for_metadata;
-    GIP_DeviceType device_type;
+    GIP_AttachmentType attachment_type;
     GIP_PaddleFormat paddle_format;
     Uint32 features;
     Uint32 quirks;
     Uint8 share_button_idx;
     Uint8 paddle_idx;
     int paddle_offset;
+} GIP_Attachment;
+
+typedef struct GIP_Device
+{
+    SDL_HIDAPI_Device *device;
+
+    Uint64 hello_deadline;
+    bool got_hello;
+    bool reset_for_metadata;
+
+    GIP_Attachment *attachments[MAX_ATTACHMENTS];
 } GIP_Device;
 
 typedef struct GIP_HelloDevice
@@ -509,7 +525,7 @@ typedef struct GIP_InitialReportsRequest
     Uint8 data[2];
 } GIP_InitialReportsRequest;
 
-static bool GIP_SetMetadataDefaults(GIP_Device *device);
+static bool GIP_SetMetadataDefaults(GIP_Attachment *attachment);
 
 static int GIP_DecodeLength(Uint64 *length, const Uint8 *bytes, int num_bytes)
 {
@@ -546,23 +562,26 @@ static int GIP_EncodeLength(Uint64 length, Uint8 *bytes, int num_bytes)
     return offset;
 }
 
-static bool GIP_SupportsSystemMessage(GIP_Device *device, Uint8 command, bool upstream)
+static bool GIP_SupportsSystemMessage(GIP_Attachment *attachment, Uint8 command, bool upstream)
 {
     if (upstream) {
-        return device->metadata.device.in_system_messages[command >> 5] & (1u << command);
+        return attachment->metadata.device.in_system_messages[command >> 5] & (1u << command);
     } else {
-        return device->metadata.device.out_system_messages[command >> 5] & (1u << command);
+        return attachment->metadata.device.out_system_messages[command >> 5] & (1u << command);
     }
 }
 
-static bool GIP_SupportsVendorMessage(GIP_Device *device, Uint8 command, bool upstream)
+static bool GIP_SupportsVendorMessage(GIP_Attachment *attachment, Uint8 command, bool upstream)
 {
     size_t i;
-    for (i = 0; i < device->metadata.num_messages; i++) {
-        GIP_MessageMetadata *metadata = &device->metadata.message_metadata[i];
+    for (i = 0; i < attachment->metadata.num_messages; i++) {
+        GIP_MessageMetadata *metadata = &attachment->metadata.message_metadata[i];
         if (metadata->type != command) {
             continue;
         }
+        if (metadata->flags & GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE) {
+            return true;
+        }
         if (upstream) {
             return metadata->flags & GIP_MESSAGE_FLAG_UPSTREAM;
         } else {
@@ -572,34 +591,34 @@ static bool GIP_SupportsVendorMessage(GIP_Device *device, Uint8 command, bool up
     return false;
 }
 
-static Uint8 GIP_SequenceNext(GIP_Device *device, Uint8 command, bool system)
+static Uint8 GIP_SequenceNext(GIP_Attachment *attachment, Uint8 command, bool system)
 {
     Uint8 seq;
 
     if (system) {
         switch (command) {
         case GIP_CMD_SECURITY:
-            seq = device->seq_security++;
+            seq = attachment->seq_security++;
             if (!seq) {
-                seq = device->seq_security++;
+                seq = attachment->seq_security++;
             }
             break;
         case GIP_CMD_EXTENDED:
-            seq = device->seq_extended++;
+            seq = attachment->seq_extended++;
             if (!seq) {
-                seq = device->seq_extended++;
+                seq = attachment->seq_extended++;
             }
             break;
         case GIP_AUDIO_DATA:
-            seq = device->seq_audio++;
+            seq = attachment->seq_audio++;
             if (!seq) {
-                seq = device->seq_audio++;
+                seq = attachment->seq_audio++;
             }
             break;
         default:
-            seq = device->seq_system++;
+            seq = attachment->seq_system++;
             if (!seq) {
-                seq = device->seq_system++;
+                seq = attachment->seq_system++;
             }
             break;
         }
@@ -609,32 +628,35 @@ static Uint8 GIP_SequenceNext(GIP_Device *device, Uint8 command, bool system)
             return 0;
         }
 
-        seq = device->seq_vendor++;
+        seq = attachment->seq_vendor++;
         if (!seq) {
-            seq = device->seq_vendor++;
+            seq = attachment->seq_vendor++;
         }
     }
     return seq;
 }
 
-static void GIP_HandleQuirks(GIP_Device *device)
+static void GIP_HandleQuirks(GIP_Attachment *attachment)
 {
     size_t i, j;
     for (i = 0; quirks[i].vendor_id; i++) {
-        if (quirks[i].vendor_id != device->device->vendor_id) {
+        if (quirks[i].vendor_id != attachment->device->device->vendor_id) {
             continue;
         }
-        if (quirks[i].product_id != device->device->product_id) {
+        if (quirks[i].product_id != attachment->device->device->product_id) {
             continue;
         }
-        device->features |= quirks[i].added_features;
-        device->features &= ~quirks[i].filtered_features;
-        device->quirks = quirks[i].quirks;
-        device->device_type = quirks[i].device_type;
+        if (quirks[i].attachment_index != attachment->attachment_index) {
+            continue;
+        }
+        attachment->features |= quirks[i].added_features;
+        attachment->features &= ~quirks[i].filtered_features;
+        attachment->quirks = quirks[i].quirks;
+        attachment->attachment_type = quirks[i].device_type;
 
         for (j = 0; j < 8; ++j) {
-            device->metadata.device.in_system_messages[j] |= quirks[i].extra_in_system[j];
-            device->metadata.device.out_system_messages[j] |= quirks[i].extra_out_system[j];
+            attachment->metadata.device.in_system_messages[j] |= quirks[i].extra_in_system[j];
+            attachment->metadata.device.out_system_messages[j] |= quirks[i].extra_out_system[j];
         }
         break;
     }
@@ -687,16 +709,16 @@ static bool GIP_SendRawMessage(
 }
 
 static bool GIP_SendSystemMessage(
-    GIP_Device *device,
+    GIP_Attachment *attachment,
     Uint8 message_type,
     Uint8 flags,
     const Uint8 *bytes,
     int num_bytes)
 {
-    return GIP_SendRawMessage(device,
+    return GIP_SendRawMessage(attachment->device,
         message_type,
-        GIP_FLAG_SYSTEM | flags,
-        GIP_SequenceNext(device, message_type, true),
+        GIP_FLAG_SYSTEM | attachment->attachment_index | flags,
+        GIP_SequenceNext(attachment, message_type, true),
         bytes,
         num_bytes,
         true,
@@ -705,16 +727,16 @@ static bool GIP_SendSystemMessage(
 }
 
 static bool GIP_SendVendorMessage(
-    GIP_Device *device,
+    GIP_Attachment *attachment,
     Uint8 message_type,
     Uint8 flags,
     const Uint8 *bytes,
     int num_bytes)
 {
-    return GIP_SendRawMessage(device,
+    return GIP_SendRawMessage(attachment->device,
         message_type,
         flags,
-        GIP_SequenceNext(device, message_type, false),
+        GIP_SequenceNext(attachment, message_type, false),
         bytes,
         num_bytes,
         true,
@@ -798,6 +820,11 @@ static bool GIP_ParseDeviceMetadata(GIP_Metadata *metadata, const Uint8 *bytes,
 
         for (i = 0; i < count; i++) {
             Uint8 message = bytes[buffer_offset + 1 + i];
+#ifdef DEBUG_XBOX_PROTOCOL
+            SDL_LogDebug(SDL_LOG_CATEGORY_INPUT,
+                "GIP: Supported upstream system message %02x",
+                message);
+#endif
             device->in_system_messages[message >> 5] |= 1u << (message & 0x1F);
         }
     }
@@ -815,6 +842,11 @@ static bool GIP_ParseDeviceMetadata(GIP_Metadata *metadata, const Uint8 *bytes,
 
         for (i = 0; i < count; i++) {
             Uint8 message = bytes[buffer_offset + 1 + i];
+#ifdef DEBUG_XBOX_PROTOCOL
+            SDL_LogDebug(SDL_LOG_CATEGORY_INPUT,
+                "GIP: Supported downstream system message %02x",
+                message);
+#endif
             device->out_system_messages[message >> 5] |= 1u << (message & 0x1F);
         }
     }
@@ -919,9 +951,16 @@ static bool GIP_ParseMessageMetadata(GIP_MessageMetadata *metadata, const Uint8
 
 #ifdef DEBUG_XBOX_PROTOCOL
     SDL_LogDebug(SDL_LOG_CATEGORY_INPUT,
-        "GIP: Supported vendor message type %02x of length %d",
+        "GIP: Supported vendor message type %02x of length %d, %s, %s, %s",
         metadata->type,
-        metadata->length);
+        metadata->length,
+        metadata->flags & GIP_MESSAGE_FLAG_UPSTREAM ?
+            (metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM ? "bidirectional" : "upstream") :
+            metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM ? "downstream" :
+            metadata->flags & GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE ? "downstream request response" :
+            "unknown direction",
+        metadata->flags & GIP_MESSAGE_FLAG_SEQUENCED ? "sequenced" : "not sequenced",
+        metadata->flags & GIP_MESSAGE_FLAG_RELIABLE ? "reliable" : "unreliable");
 #endif
 
     *offset += length;
@@ -1014,24 +1053,25 @@ static bool GIP_Acknowledge(
         NULL,
         NULL);
 }
-static bool GIP_FragmentFailed(GIP_Device *device, const GIP_Header *header) {
-    device->fragment_retries++;
-    if (device->fragment_retries > 8) {
-        if (device->fragment_data) {
-            SDL_free(device->fragment_data);
-            device->fragment_data = NULL;
+
+static bool GIP_FragmentFailed(GIP_Attachment *attachment, const GIP_Header *header) {
+    attachment->fragment_retries++;
+    if (attachment->fragment_retries > 8) {
+        if (attachment->fragment_data) {
+            SDL_free(attachment->fragment_data);
+            attachment->fragment_data = NULL;
         }
-        device->fragment_message = 0;
+        attachment->fragment_message = 0;
     }
-    return GIP_Acknowledge(device,
+    return GIP_Acknowledge(attachment->device,
         header,
-        device->fragment_offset,
-        (Uint16) (device->total_length - device->fragment_offset));
+        attachment->fragment_offset,
+        (Uint16) (attachment->total_length - attachment->fragment_offset));
 }
 
-static bool GIP_EnableEliteButtons(GIP_Device *device) {
-    if (device->paddle_format == GIP_PADDLES_XBE2_RAW ||
-        (device->firmware_major_version != 4 && device->firmware_minor_version < 17))
+static bool GIP_EnableEliteButtons(GIP_Attachment *attachment) {
+    if (attachment->paddle_format == GIP_PADDLES_XBE2_RAW ||
+        (attachment->firmware_major_version != 4 && attachment->firmware_minor_version < 17))
     {
         /*
          * The meaning of this packet is unknown and not documented, but it's
@@ -1039,7 +1079,7 @@ static bool GIP_EnableEliteButtons(GIP_Device *device) {
          */
         static const Uint8 enable_raw_report[] = { 7, 0 };
 
-        if (!GIP_SendVendorMessage(device,
+        if (!GIP_SendVendorMessage(attachment,
             GIP_SL_ELITE_CONFIG,
             0,
             enable_raw_report,
@@ -1052,7 +1092,7 @@ static bool GIP_EnableEliteButtons(GIP_Device *device) {
     return true;
 }
 
-static bool GIP_SendGuideButtonLED(GIP_Device *device, Uint8 pattern, Uint8 intensity)
+static bool GIP_SendGuideButtonLED(GIP_Attachment *attachment, Uint8 pattern, Uint8 intensity)
 {
     Uint8 buffer[] = {
         GIP_LED_GUIDE,
@@ -1060,36 +1100,40 @@ static bool GIP_SendGuideButtonLED(GIP_Device *device, Uint8 pattern, Uint8 inte
         intensity,
     };
 
-    return GIP_SendSystemMessage(device, GIP_CMD_LED, 0, buffer, sizeof(buffer));
+    if (!GIP_SupportsSystemMessage(attachment, GIP_CMD_LED, false)) {
+        return true;
+    }
+    return GIP_SendSystemMessage(attachment, GIP_CMD_LED, 0, buffer, sizeof(buffer));
 }
 
-static bool GIP_SendQueryFirmware(GIP_Device *device, Uint8 slot)
+static bool GIP_SendQueryFirmware(GIP_Attachment *attachment, Uint8 slot)
 {
     /* The "slot" variable might not be correct; the packet format is still unclear */
     Uint8 buffer[] = { 0x1, slot, 0, 0, 0 };
 
-    return GIP_SendSystemMessage(device, GIP_CMD_FIRMWARE, 0, buffer, sizeof(buffer));
+    return GIP_SendSystemMessage(attachment, GIP_CMD_FIRMWARE, 0, buffer, sizeof(buffer));
 }
 
-static bool GIP_SendSetDeviceState(GIP_Device *device, Uint8 state, Uint8 attachment)
+static bool GIP_SendSetDeviceState(GIP_Attachment *attachment, Uint8 state)
 {
     Uint8 buffer[] = { state };
-    attachment &= GIP_FLAG_ATTACHMENT_MASK;
-    return GIP_SendSystemMessage(device, GIP_CMD_SET_DEVICE_STATE, attachment, buffer, sizeof(buffer));
+    return GIP_SendSystemMessage(attachment,
+        GIP_CMD_SET_DEVICE_STATE,
+        attachment->attachment_index,
+        buffer,
+        sizeof(buffer));
 }
 
-static bool GIP_SendInitSequence(GIP_Device *device)
+static bool GIP_SendInitSequence(GIP_Attachment *attachment)
 {
-    if (device->device->vendor_id == USB_VENDOR_MICROSOFT &&
-        device->device->product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2)
-    {
+    if (attachment->features & GIP_FEATURE_EXTENDED_SET_DEVICE_STATE) {
         /*
          * The meaning of this packet is unknown and not documented, but it's
          * needed for the Elite 2 controller to start up on older firmwares
          */
         static const Uint8 set_device_state[] = { GIP_STATE_UNK6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x55, 0x53, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 };
 
-        if (!GIP_SendSystemMessage(device,
+        if (!GIP_SendSystemMessage(attachment,
             GIP_CMD_SET_DEVICE_STATE,
             0,
             set_device_state,
@@ -1098,83 +1142,91 @@ static bool GIP_SendInitSequence(GIP_Device *device)
             return false;
         }
 
-        if (!GIP_EnableEliteButtons(device)) {
+        if (!GIP_EnableEliteButtons(attachment)) {
             return false;
         }
     }
-    if (!GIP_SendSetDeviceState(device, GIP_STATE_START, 0)) {
+    if (!GIP_SendSetDeviceState(attachment, GIP_STATE_START)) {
         return false;
     }
-    device->device_state = GIP_STATE_START;
+    attachment->device_state = GIP_STATE_START;
 
-    if (!GIP_SendGuideButtonLED(device, GIP_LED_GUIDE_ON, 20)) {
+    if (!GIP_SendGuideButtonLED(attachment, GIP_LED_GUIDE_ON, 20)) {
         return false;
     }
 
-    if (GIP_SupportsSystemMessage(device, GIP_CMD_SECURITY, false) &&
-        !(device->features & GIP_FEATURE_SECURITY_OPT_OUT))
+    if (GIP_SupportsSystemMessage(attachment, GIP_CMD_SECURITY, false) &&
+        !(attachment->features & GIP_FEATURE_SECURITY_OPT_OUT))
     {
         /* TODO: Implement Security command property */
         Uint8 buffer[] = { 0x1, 0x0 };
-        GIP_SendSystemMessage(device, GIP_CMD_SECURITY, 0, buffer, sizeof(buffer));
+        GIP_SendSystemMessage(attachment, GIP_CMD_SECURITY, 0, buffer, sizeof(buffer));
     }
 
-    if (GIP_SupportsVendorMessage(device, GIP_CMD_INITIAL_REPORTS_REQUEST, false)) {
+    if (GIP_SupportsVendorMessage(attachment, GIP_CMD_INITIAL_REPORTS_REQUEST, false)) {
         GIP_InitialReportsRequest request = { 0 };
-        GIP_SendVendorMessage(device, GIP_CMD_INITIAL_REPORTS_REQUEST, 0, (const Uint8 *)&request, sizeof(request));
+        GIP_SendVendorMessage(attachment, GIP_CMD_INITIAL_REPORTS_REQUEST, 0, (const Uint8 *)&request, sizeof(request));
     }
-    return HIDAPI_JoystickConnected(device->device, NULL);
+
+    if (!attachment->joystick) {
+        return HIDAPI_JoystickConnected(attachment->device->device, &attachment->joystick);
+    }
+    return true;
 }
 
-static bool GIP_EnsureMetadata(GIP_Device *device)
+static bool GIP_EnsureMetadata(GIP_Attachment *attachment)
 {
-
-    switch (device->got_metadata) {
+    switch (attachment->got_metadata) {
     case GIP_METADATA_GOT:
     case GIP_METADATA_FAKED:
         return true;
     case GIP_METADATA_NONE:
-        if (device->quirks & GIP_QUIRK_BROKEN_METADATA) {
-            GIP_SendSystemMessage(device, GIP_CMD_METADATA, 0, NULL, 0);
-            GIP_SetMetadataDefaults(device);
-            return GIP_SendInitSequence(device);
-        } else if (device->got_hello) {
-            device->got_metadata = GIP_METADATA_PENDING;
-            device->metadata_next = SDL_GetTicks() + 500;
-            device->metadata_retries = 0;
-            return GIP_SendSystemMessage(device, GIP_CMD_METADATA, 0, NULL, 0);
+        if (attachment->quirks & GIP_QUIRK_BROKEN_METADATA) {
+            GIP_SendSystemMessage(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+            GIP_SetMetadataDefaults(attachment);
+            return GIP_SendInitSequence(attachment);
+        } else if (attachment->device->got_hello) {
+            attachment->got_metadata = GIP_METADATA_PENDING;
+            attachment->metadata_next = SDL_GetTicks() + 500;
+            attachment->metadata_retries = 0;
+            return GIP_SendSystemMessage(attachment, GIP_CMD_METADATA, 0, NULL, 0);
         } else {
-            return GIP_SetMetadataDefaults(device);
+            return GIP_SetMetadataDefaults(attachment);
         }
     default:
         return true;
     }
 }
 
-static bool GIP_SetMetadataDefaults(GIP_Device *device)
+static bool GIP_SetMetadataDefaults(GIP_Attachment *attachment)
 {
-    /* Some decent default settings */
-    device->features |= GIP_FEATURE_MOTOR_CONTROL;
-    device->device_type = GIP_TYPE_GAMEPAD;
-    device->metadata.device.in_system_messages[0] |= (1u << GIP_CMD_GUIDE_BUTTON);
+    if (attachment->attachment_index == 0) {
+        /* Some decent default settings */
+        attachment->features |= GIP_FEATURE_MOTOR_CONTROL;
+        attachment->attachment_type = GIP_TYPE_GAMEPAD;
+        attachment->metadata.device.in_system_messages[0] |= (1u << GIP_CMD_GUIDE_BUTTON);
 
-    if (SDL_IsJoystickXboxSeriesX(device->device->vendor_id, device->device->product_id)) {
-        device->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP;
+        if (SDL_IsJoystickXboxSeriesX(attachment->device->device->vendor_id, attachment->device->device->product_id)) {
+            attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP;
+        }
     }
 
-    GIP_HandleQuirks(device);
+    GIP_HandleQuirks(attachment);
 
-    if (GIP_SupportsSystemMessage(device, GIP_CMD_FIRMWARE, false)) {
-        GIP_SendQueryFirmware(device, 2);
+    if (GIP_SupportsSystemMessage(attachment, GIP_CMD_FIRMWARE, false)) {
+        GIP_SendQueryFirmware(attachment, 2);
     }
 
-    device->got_metadata = GIP_METADATA_FAKED;
-    device->hello_deadline = 0;
-    return HIDAPI_JoystickConnected(device->device, NULL);
+    attachment->got_metadata = GIP_METADATA_FAKED;
+    attachment->device->hello_deadline = 0;
+    if (!attachment->joystick) {
+        return HIDAPI_JoystickConnected(attachment->device->device, &attachment->joystick);
+    }
+    return true;
 }
 
 static bool GIP_HandleCommandProtocolControl(
-    GIP_Device *device,
+    GIP_Attachment *attachment,
     const GIP_Header *header,
     const Uint8 *bytes,
     int num_bytes)
@@ -1185,7 +1237,7 @@ static bool GIP_HandleCommandProtocolControl(
 }
 
 static bool GIP_HandleCommandHelloDevice(
-    GIP_Device *device,
+    GIP_Attachment *attachment,
     const GIP_Header *header,
     const Uint8 *bytes,
     int num_bytes)
@@ -1268,28 +1320,30 @@ static bool GIP_HandleCommandHelloDevice(
     }
 
     if (header->flags & GIP_FLAG_ATTACHMENT_MASK) {
-        return GIP_SendSystemMessage(device, GIP_CMD_METADATA, header->flags & GIP_FLAG_ATTACHMENT_MASK, NULL, 0);
+        return GIP_SendSystemMessage(attachment, GIP_CMD_METADATA, 0, NULL, 0);
     } else {
-        device->firmware_major_version = message.firmware_major_version;
-        device->firmware_minor_version = message.firmware_minor_version;
+        attachment->firmware_major_version = message.firmware_major_version;
+        attachment->firmware_minor_version = message.firmware_minor_version;
 
-        device->hello_deadline = 0;
-        device->got_hello = true;
-        if (device->got_metadata == GIP_METADATA_FAKED) {
-            device->got_metadata = GIP_METADATA_NONE;
+        if (attachment->attachment_index == 0) {
+            attachment->device->hello_deadline = 0;
+            attachment->device->got_hello = true;
+        }
+        if (attachment->got_metadata == GIP_METADATA_FAKED) {
+            attachment->got_metadata = GIP_METADATA_NONE;
         }
-        GIP_EnsureMetadata(device);
+        GIP_EnsureMetadata(attachment);
     }
     return true;
 }
 
 static bool GIP_HandleCommandStatusDevice(
-    GIP_Device *device,
+    GIP_Attachment *attachment,
     const GIP_Header *header,
     const Uint8 *bytes,
     int num_bytes)
 {
-    GIP_ExtendedStatus status = {0};
+    GIP_ExtendedStatus status = {{0}};
     int i;
 
     if (num_bytes < 1) {
@@ -1332,12 +1386,12 @@ static bool GIP_HandleCommandStatusDevice(
         }
     }
 
-    GIP_EnsureMetadata(device);
+    GIP_EnsureMetadata(attachment);
     return true;
 }
 
 static bool GIP_HandleCommandMetadataRespose(
-    GIP_Device *device,
+    GIP_Attachment *attachment,
     const GIP_Header *header,
     const Uint8 *bytes,
     int num_bytes)
@@ -1348,68 +1402,70 @@ static bool GIP_HandleCommandMetadataRespose(
     bool found_controller_guid = false;
     int i;
 
-    if (header->flags & GIP_FLAG_ATTACHMENT_MASK) {
-        /* TODO: Parse properly */
-        return true;
-    }
-
     if (!GIP_ParseMetadata(&metadata, bytes, num_bytes)) {
         return false;
     }
 
-    if (device->got_metadata == GIP_METADATA_GOT) {
-        GIP_MetadataFree(&device->metadata);
+    if (attachment->got_metadata == GIP_METADATA_GOT) {
+        GIP_MetadataFree(&attachment->metadata);
     }
-    device->metadata = metadata;
-    device->got_metadata = GIP_METADATA_GOT;
-    device->features = 0;
+    attachment->metadata = metadata;
+    attachment->got_metadata = GIP_METADATA_GOT;
+    attachment->features = 0;
 
-    device->device_type = GIP_TYPE_UNKNOWN;
+    attachment->attachment_type = GIP_TYPE_UNKNOWN;
+#ifdef DEBUG_XBOX_PROTOCOL
     for (i = 0; i < metadata.device.num_preferred_types; i++) {
         const char *type = metadata.device.preferred_types[i];
-#ifdef DEBUG_XBOX_PROTOCOL
         SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "GIP: Device preferred type: %s", type);
+    }
 #endif
+    for (i = 0; i < metadata.device.num_preferred_types; i++) {
+        const char *type = metadata.device.preferred_types[i];
         if (SDL_strcmp(type, "Windows.Xbox.Input.Gamepad") == 0) {
-            device->device_type = GIP_TYPE_GAMEPAD;
+            attachment->attachment_type = GIP_TYPE_GAMEPAD;
             expected_guid = &GUID_IGamepad;
             break;
         }
         if (SDL_strcmp(type, "Microsoft.Xbox.Input.ArcadeStick") == 0) {
-            device->device_type = GIP_TYPE_ARCADE_STICK;
+            attachment->attachment_type = GIP_TYPE_ARCADE_STICK;
             expected_guid = &GUID_ArcadeStick;
             break;
         }
         if (SDL_strcmp(type, "Windows.Xbox.Input.ArcadeStick") == 0) {
-            device->device_type = GIP_TYPE_ARCADE_STICK;
+            attachment->attachment_type = GIP_TYPE_ARCADE_STICK;
             expected_guid = &GUID_ArcadeStick;
             break;
         }
         if (SDL_strcmp(type, "Microsoft.Xbox.Input.FlightStick") == 0) {
-            device->device_type = GIP_TYPE_FLIGHT_STICK;
-            expected_guid = &GUID_ArcadeStick;
+            attachment->attachment_type = GIP_TYPE_FLIGHT_STICK;
+            expected_guid = &GUID_FlightStick;
             break;
         }
         if (SDL_strcmp(type, "Windows.Xbox.Input.FlightStick") == 0) {
-            device->device_type = GIP_TYPE_FLIGHT_STICK;
-            expected_guid = &GUID_ArcadeStick;
+            attachment->attachment_type = GIP_TYPE_FLIGHT_STICK;
+            expected_guid = &GUID_FlightStick;
             break;
         }
         if (SDL_strcmp(type, "Microsoft.Xbox.Input.Wheel") == 0) {
-            device->device_type = GIP_TYPE_WHEEL;
+            attachment->attachment_type = GIP_TYPE_WHEEL;
             expected_guid = &GUID_Wheel;
             break;
         }
         if (SDL_strcmp(type, "Windows.Xbox.Input.Wheel") == 0) {
-            device->device_type = GIP_TYPE_WHEEL;
+            attachment->attachment_type = GIP_TYPE_WHEEL;
             expected_guid = &GUID_Wheel;
             break;
         }
         if (SDL_strcmp(type, "Windows.Xbox.Input.NavigationController") == 0) {
-            device->device_type = GIP_TYPE_NAVIGATION_CONTROLLER;
+            attachment->attachment_type = GIP_TYPE_NAVIGATION_CONTROLLER;
             expected_guid = &GUID_NavigationController;
             break;
         }
+        if (SDL_strcmp(type, "Windows.Xbox.Input.Chatpad") == 0) {
+            attachment->attachment_type = GIP_TYPE_CHATPAD;
+            break;
+        }
     }
 
     for (i = 0; i < metadata.device.num_supported_interfaces; i++) {
@@ -1428,23 +1484,23 @@ static bool GIP_HandleCommandMetadataRespose(
             continue;
         }
         if (SDL_memcmp(&GUID_IDevAuthPCOptOut, guid, sizeof(GUID)) == 0) {
-            device->features |= GIP_FEATURE_SECURITY_OPT_OUT;
+            attachment->features |= GIP_FEATURE_SECURITY_OPT_OUT;
             continue;
         }
         if (SDL_memcmp(&GUID_IConsoleFunctionMap_InputReport, guid, sizeof(GUID)) == 0) {
-            device->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP;
+            attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP;
             continue;
         }
         if (SDL_memcmp(&GUID_IConsoleFunctionMap_OverflowInputReport, guid, sizeof(GUID)) == 0) {
-            device->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW;
+            attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW;
             continue;
         }
         if (SDL_memcmp(&GUID_IEliteButtons, guid, sizeof(GUID)) == 0) {
-            device->features |= GIP_FEATURE_ELITE_BUTTONS;
+            attachment->features |= GIP_FEATURE_ELITE_BUTTONS;
             continue;
         }
         if (SDL_memcmp(&GUID_DynamicLatencyInput, guid, sizeof(GUID)) == 0) {
-            device->features |= GIP_FEATURE_DYNAMIC_LATENCY_INPUT;
+

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