SDL: Improved PS4 and PS5 third-party controller feature detection

From 0c4594ac72eadd9be29b5612cf5d23046a7694f3 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 22 Sep 2022 06:45:46 -0700
Subject: [PATCH] Improved PS4 and PS5 third-party controller feature detection

---
 src/joystick/hidapi/SDL_hidapi_ps4.c | 74 ++++++++--------------------
 src/joystick/hidapi/SDL_hidapi_ps5.c | 67 ++++++++-----------------
 2 files changed, 42 insertions(+), 99 deletions(-)

diff --git a/src/joystick/hidapi/SDL_hidapi_ps4.c b/src/joystick/hidapi/SDL_hidapi_ps4.c
index 0c0493a6129..63da2787d4c 100644
--- a/src/joystick/hidapi/SDL_hidapi_ps4.c
+++ b/src/joystick/hidapi/SDL_hidapi_ps4.c
@@ -69,6 +69,7 @@ typedef enum
 typedef enum 
 {
     k_ePS4FeatureReportIdGyroCalibration_USB = 0x02,
+    k_ePS4FeatureReportIdCapabilities = 0x03,
     k_ePS4FeatureReportIdGyroCalibration_BT = 0x05,
     k_ePS4FeatureReportIdSerialNumber = 0x12,
 } EPS4FeatureReportID;
@@ -125,7 +126,6 @@ typedef struct {
     SDL_bool is_dongle;
     SDL_bool is_bluetooth;
     SDL_bool official_controller;
-    SDL_bool audio_supported;
     SDL_bool effects_supported;
     SDL_bool sensors_supported;
     SDL_bool touchpad_supported;
@@ -192,42 +192,6 @@ static int ReadFeatureReport(SDL_hid_device *dev, Uint8 report_id, Uint8 *report
     return SDL_hid_get_feature_report(dev, report, length);
 }
 
-static SDL_bool HIDAPI_DriverPS4_CanRumble(Uint16 vendor_id, Uint16 product_id)
-{
-    /* The Razer Panthera fight stick hangs when trying to rumble */
-    if (vendor_id == USB_VENDOR_RAZER &&
-        (product_id == USB_PRODUCT_RAZER_PANTHERA || product_id == USB_PRODUCT_RAZER_PANTHERA_EVO)) {
-        return SDL_FALSE;
-    }
-
-    /* The Victrix Pro FS v2 will hang on reboot if we send output reports */
-    if (vendor_id == USB_VENDOR_PDP && product_id == USB_PRODUCT_VICTRIX_FS_PRO_V2) {
-        return SDL_FALSE;
-    }
-
-    /* The Hori controllers don't have any rumble hardware */
-    if (vendor_id == USB_VENDOR_HORI) {
-        return SDL_FALSE;
-    }
-
-    return SDL_TRUE;
-}
-
-static SDL_bool HIDAPI_DriverPS4_HasSensors(Uint16 vendor_id, Uint16 product_id)
-{
-    /* The Hori controllers don't have any gyro or accelerometer */
-    if (vendor_id == USB_VENDOR_HORI) {
-        return SDL_FALSE;
-    }
-
-    return SDL_TRUE;
-}
-
-static SDL_bool HIDAPI_DriverPS4_HasTouchpad(Uint16 vendor_id, Uint16 product_id)
-{
-    return SDL_TRUE;
-}
-
 static void
 SetLedsForPlayerIndex(DS4EffectsState_t *effects, int player_index)
 {
@@ -521,6 +485,8 @@ static SDL_bool
 HIDAPI_DriverPS4_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
 {
     SDL_DriverPS4_Context *ctx;
+    Uint8 data[USB_PACKET_LENGTH];
+    int size;
     SDL_bool enhanced_mode = SDL_FALSE;
 
     ctx = (SDL_DriverPS4_Context *)SDL_calloc(1, sizeof(*ctx));
@@ -547,9 +513,6 @@ HIDAPI_DriverPS4_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
         ctx->official_controller = SDL_TRUE;
         enhanced_mode = SDL_TRUE;
     } else if (device->vendor_id == USB_VENDOR_SONY) {
-        Uint8 data[USB_PACKET_LENGTH];
-        int size;
-
         /* This will fail if we're on Bluetooth */
         size = ReadFeatureReport(device->dev, k_ePS4FeatureReportIdSerialNumber, data, sizeof(data));
         if (size >= 7) {
@@ -588,22 +551,27 @@ HIDAPI_DriverPS4_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
     SDL_Log("PS4 dongle = %s, bluetooth = %s\n", ctx->is_dongle ? "TRUE" : "FALSE", ctx->is_bluetooth ? "TRUE" : "FALSE");
 #endif
 
-    /* Check to see if audio is supported */
-    if (device->vendor_id == USB_VENDOR_SONY &&
-        (device->product_id == USB_PRODUCT_SONY_DS4_SLIM || device->product_id == USB_PRODUCT_SONY_DS4_DONGLE)) {
-        ctx->audio_supported = SDL_TRUE;
-    }
-
-    if (HIDAPI_DriverPS4_CanRumble(device->vendor_id, device->product_id)) {
+    /* Get the device capabilities */
+    if (device->vendor_id == USB_VENDOR_SONY) {
         ctx->effects_supported = SDL_TRUE;
-    }
-
-    if (HIDAPI_DriverPS4_HasSensors(device->vendor_id, device->product_id)) {
         ctx->sensors_supported = SDL_TRUE;
-    }
-
-    if (HIDAPI_DriverPS4_HasTouchpad(device->vendor_id, device->product_id)) {
         ctx->touchpad_supported = SDL_TRUE;
+    } else if (ReadFeatureReport(device->dev, k_ePS4FeatureReportIdCapabilities, data, sizeof(data)) == 48 &&
+               data[2] == 0x27) {
+        Uint8 capabilities = data[4];
+
+#ifdef DEBUG_PS4_PROTOCOL
+        HIDAPI_DumpPacket("PS4 capabilities: size = %d", data, size);
+#endif
+        if ((capabilities & 0x0C) != 0) {
+            ctx->effects_supported = SDL_TRUE;
+        }
+        if ((capabilities & 0x02) != 0) {
+            ctx->sensors_supported = SDL_TRUE;
+        }
+        if ((capabilities & 0x40) != 0) {
+            ctx->touchpad_supported = SDL_TRUE;
+        }
     }
 
     if (!joystick->serial && device->serial && SDL_strlen(device->serial) == 12) {
diff --git a/src/joystick/hidapi/SDL_hidapi_ps5.c b/src/joystick/hidapi/SDL_hidapi_ps5.c
index 08f27fb7600..f3ac145da49 100644
--- a/src/joystick/hidapi/SDL_hidapi_ps5.c
+++ b/src/joystick/hidapi/SDL_hidapi_ps5.c
@@ -56,6 +56,7 @@ typedef enum
 
 typedef enum
 {
+    k_EPS5FeatureReportIdCapabilities = 0x03,
     k_EPS5FeatureReportIdCalibration = 0x05,
     k_EPS5FeatureReportIdSerialNumber = 0x09,
     k_EPS5FeatureReportIdFirmwareInfo = 0x20,
@@ -227,41 +228,6 @@ typedef struct {
     } last_state;
 } SDL_DriverPS5_Context;
 
-static SDL_bool HIDAPI_DriverPS5_CanRumble(Uint16 vendor_id, Uint16 product_id)
-{
-    /* The Hori controllers don't have any rumble hardware */
-    if (vendor_id == USB_VENDOR_HORI) {
-        return SDL_FALSE;
-    }
-
-    return SDL_TRUE;
-}
-
-static SDL_bool HIDAPI_DriverPS5_HasSensors(Uint16 vendor_id, Uint16 product_id)
-{
-    /* The Hori controllers don't have any gyro or accelerometer */
-    if (vendor_id == USB_VENDOR_HORI) {
-        return SDL_FALSE;
-    }
-
-    return SDL_TRUE;
-}
-
-static SDL_bool HIDAPI_DriverPS5_HasTouchpad(Uint16 vendor_id, Uint16 product_id)
-{
-    return SDL_TRUE;
-}
-
-static SDL_bool HIDAPI_DriverPS5_UseAlternateReport(Uint16 vendor_id, Uint16 product_id)
-{
-    /* The Hori Fighting Stick Alpha and Fighting Commander OCTA report touchpad at a different offset than the PS5 controller */
-    if (vendor_id == USB_VENDOR_HORI) {
-        return SDL_TRUE;
-    }
-
-    return SDL_FALSE;
-}
-
 static int HIDAPI_DriverPS5_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *effect, int size);
 
 static void
@@ -742,20 +708,29 @@ HIDAPI_DriverPS5_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
         }
     }
 
-    if (HIDAPI_DriverPS5_UseAlternateReport(device->vendor_id, device->product_id)) {
-        ctx->use_alternate_report = SDL_TRUE;
-    }
-
-    if (HIDAPI_DriverPS5_CanRumble(device->vendor_id, device->product_id)) {
+    /* Get the device capabilities */
+    if (device->vendor_id == USB_VENDOR_SONY) {
         ctx->effects_supported = SDL_TRUE;
-    }
-
-    if (HIDAPI_DriverPS5_HasSensors(device->vendor_id, device->product_id)) {
         ctx->sensors_supported = SDL_TRUE;
-    }
-
-    if (HIDAPI_DriverPS5_HasTouchpad(device->vendor_id, device->product_id)) {
         ctx->touchpad_supported = SDL_TRUE;
+    } else if (ReadFeatureReport(device->dev, k_EPS5FeatureReportIdCapabilities, data, sizeof(data)) == 48 &&
+               data[2] == 0x28) {
+        Uint8 capabilities = data[4];
+
+#ifdef DEBUG_PS5_PROTOCOL
+        HIDAPI_DumpPacket("PS5 capabilities: size = %d", data, size);
+#endif
+        if ((capabilities & 0x0C) != 0) {
+            ctx->effects_supported = SDL_TRUE;
+        }
+        if ((capabilities & 0x02) != 0) {
+            ctx->sensors_supported = SDL_TRUE;
+        }
+        if ((capabilities & 0x40) != 0) {
+            ctx->touchpad_supported = SDL_TRUE;
+        }
+
+        ctx->use_alternate_report = SDL_TRUE;
     }
 
     if (!joystick->serial && device->serial && SDL_strlen(device->serial) == 12) {