فهرست منبع

Added support for parsing the Xbox report descriptor

This gives us more robust handling of Bluetooth Xbox controllers which may vary the report format between firmware versions.

Firmware versions tested:
Xbox One S: 3.1.1, 4.8.1923, 5.13.3143
Xbox One S/X: 5.11.3118, 5.23.6
Xbox Elite Series 2: 5.22.16, 5.23.6

Fixes https://github.com/libsdl-org/SDL/issues/14597
Sam Lantinga 2 روز پیش
والد
کامیت
450a2cb5e4

+ 2 - 0
VisualC-GDK/SDL/SDL.vcxproj

@@ -469,6 +469,7 @@
     <ClInclude Include="..\..\src\joystick\controller_type.h" />
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapijoystick_c.h" />
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapi_rumble.h" />
+    <ClInclude Include="..\..\src\joystick\hidapi\SDL_report_descriptor.h" />
     <ClInclude Include="..\..\src\joystick\SDL_gamepad_c.h" />
     <ClInclude Include="..\..\src\joystick\SDL_gamepad_db.h" />
     <ClInclude Include="..\..\src\joystick\SDL_joystick_c.h" />
@@ -744,6 +745,7 @@
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_xboxone.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_lg4ff.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_zuiki.c" />
+    <ClCompile Include="..\..\src\joystick\hidapi\SDL_report_descriptor.c" />
     <ClCompile Include="..\..\src\joystick\SDL_gamepad.c" />
     <ClCompile Include="..\..\src\joystick\SDL_joystick.c" />
     <ClCompile Include="..\..\src\joystick\SDL_steam_virtual_gamepad.c" />

+ 2 - 0
VisualC-GDK/SDL/SDL.vcxproj.filters

@@ -88,6 +88,7 @@
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_xboxone.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_lg4ff.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_zuiki.c" />
+    <ClCompile Include="..\..\src\joystick\hidapi\SDL_report_descriptor.c" />
     <ClCompile Include="..\..\src\joystick\SDL_gamepad.c" />
     <ClCompile Include="..\..\src\joystick\SDL_joystick.c" />
     <ClCompile Include="..\..\src\joystick\SDL_steam_virtual_gamepad.c" />
@@ -372,6 +373,7 @@
     <ClInclude Include="..\..\src\joystick\controller_type.h" />
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapijoystick_c.h" />
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapi_rumble.h" />
+    <ClInclude Include="..\..\src\joystick\hidapi\SDL_report_descriptor.h" />
     <ClInclude Include="..\..\src\joystick\SDL_gamepad_c.h" />
     <ClInclude Include="..\..\src\joystick\SDL_gamepad_db.h" />
     <ClInclude Include="..\..\src\joystick\SDL_joystick_c.h" />

+ 2 - 0
VisualC/SDL/SDL.vcxproj

@@ -383,6 +383,7 @@
     <ClInclude Include="..\..\src\joystick\controller_type.h" />
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapijoystick_c.h" />
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapi_rumble.h" />
+    <ClInclude Include="..\..\src\joystick\hidapi\SDL_report_descriptor.h" />
     <ClInclude Include="..\..\src\joystick\SDL_gamepad_c.h" />
     <ClInclude Include="..\..\src\joystick\SDL_gamepad_db.h" />
     <ClInclude Include="..\..\src\joystick\SDL_joystick_c.h" />
@@ -635,6 +636,7 @@
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_xboxone.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_lg4ff.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_zuiki.c" />
+    <ClCompile Include="..\..\src\joystick\hidapi\SDL_report_descriptor.c" />
     <ClCompile Include="..\..\src\joystick\SDL_gamepad.c" />
     <ClCompile Include="..\..\src\joystick\SDL_joystick.c" />
     <ClCompile Include="..\..\src\joystick\SDL_steam_virtual_gamepad.c" />

+ 6 - 0
VisualC/SDL/SDL.vcxproj.filters

@@ -675,6 +675,9 @@
     <ClInclude Include="..\..\src\joystick\hidapi\SDL_hidapi_rumble.h">
       <Filter>joystick\hidapi</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\joystick\hidapi\SDL_report_descriptor.h">
+      <Filter>joystick\hidapi</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\src\joystick\windows\SDL_dinputjoystick_c.h">
       <Filter>joystick\windows</Filter>
     </ClInclude>
@@ -1305,6 +1308,9 @@
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapijoystick.c">
       <Filter>joystick\hidapi</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\joystick\hidapi\SDL_report_descriptor.c">
+      <Filter>joystick\hidapi</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\joystick\windows\SDL_dinputjoystick.c">
       <Filter>joystick\windows</Filter>
     </ClCompile>

+ 19 - 3
Xcode/SDL/SDL.xcodeproj/project.pbxproj

@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 54;
+	objectVersion = 55;
 	objects = {
 
 /* Begin PBXAggregateTarget section */
@@ -414,8 +414,8 @@
 		F386F6F02884663E001840AA /* SDL_utils_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F386F6E52884663E001840AA /* SDL_utils_c.h */; };
 		F386F6F92884663E001840AA /* SDL_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = F386F6E62884663E001840AA /* SDL_utils.c */; };
 		F388C95528B5F6F700661ECF /* SDL_hidapi_ps3.c in Sources */ = {isa = PBXBuildFile; fileRef = F388C95428B5F6F600661ECF /* SDL_hidapi_ps3.c */; };
-		F39344CE2E99771B0056986F /* SDL_dlopennote.h in Headers */ = {isa = PBXBuildFile; fileRef = F39344CD2E99771B0056986F /* SDL_dlopennote.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		F38C72492CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c in Sources */ = {isa = PBXBuildFile; fileRef = F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */; };
+		F39344CE2E99771B0056986F /* SDL_dlopennote.h in Headers */ = {isa = PBXBuildFile; fileRef = F39344CD2E99771B0056986F /* SDL_dlopennote.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		F395BF6525633B2400942BFF /* SDL_crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = F395BF6425633B2400942BFF /* SDL_crc32.c */; };
 		F395C1932569C68F00942BFF /* SDL_iokitjoystick_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F395C1912569C68E00942BFF /* SDL_iokitjoystick_c.h */; };
 		F395C19C2569C68F00942BFF /* SDL_iokitjoystick.c in Sources */ = {isa = PBXBuildFile; fileRef = F395C1922569C68E00942BFF /* SDL_iokitjoystick.c */; };
@@ -530,6 +530,10 @@
 		F3DDCC5B2AFD42B600B0842B /* SDL_video_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F3DDCC522AFD42B600B0842B /* SDL_video_c.h */; };
 		F3DDCC5D2AFD42B600B0842B /* SDL_rect_impl.h in Headers */ = {isa = PBXBuildFile; fileRef = F3DDCC542AFD42B600B0842B /* SDL_rect_impl.h */; };
 		F3E5A6EB2AD5E0E600293D83 /* SDL_properties.c in Sources */ = {isa = PBXBuildFile; fileRef = F3E5A6EA2AD5E0E600293D83 /* SDL_properties.c */; };
+		F3E6C3932EE9F20000A6B39E /* SDL_report_descriptor.c in Sources */ = {isa = PBXBuildFile; fileRef = F3E6C3922EE9F20000A6B39E /* SDL_report_descriptor.c */; };
+		F3E6C3942EE9F20000A6B39E /* SDL_hidapi_flydigi.h in Headers */ = {isa = PBXBuildFile; fileRef = F3E6C38F2EE9F20000A6B39E /* SDL_hidapi_flydigi.h */; };
+		F3E6C3952EE9F20000A6B39E /* SDL_hidapi_sinput.h in Headers */ = {isa = PBXBuildFile; fileRef = F3E6C3902EE9F20000A6B39E /* SDL_hidapi_sinput.h */; };
+		F3E6C3962EE9F20000A6B39E /* SDL_report_descriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = F3E6C3912EE9F20000A6B39E /* SDL_report_descriptor.h */; };
 		F3EFA5ED2D5AB97300BCF22F /* SDL_stb_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EFA5EA2D5AB97300BCF22F /* SDL_stb_c.h */; };
 		F3EFA5EE2D5AB97300BCF22F /* stb_image.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EFA5EC2D5AB97300BCF22F /* stb_image.h */; };
 		F3EFA5EF2D5AB97300BCF22F /* SDL_surface_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EFA5EB2D5AB97300BCF22F /* SDL_surface_c.h */; };
@@ -998,8 +1002,8 @@
 		F386F6E52884663E001840AA /* SDL_utils_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_utils_c.h; sourceTree = "<group>"; };
 		F386F6E62884663E001840AA /* SDL_utils.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_utils.c; sourceTree = "<group>"; };
 		F388C95428B5F6F600661ECF /* SDL_hidapi_ps3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps3.c; sourceTree = "<group>"; };
-		F39344CD2E99771B0056986F /* SDL_dlopennote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_dlopennote.h; sourceTree = "<group>"; };
 		F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_triton.c; sourceTree = "<group>"; };
+		F39344CD2E99771B0056986F /* SDL_dlopennote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_dlopennote.h; sourceTree = "<group>"; };
 		F395BF6425633B2400942BFF /* SDL_crc32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_crc32.c; sourceTree = "<group>"; };
 		F395C1912569C68E00942BFF /* SDL_iokitjoystick_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_iokitjoystick_c.h; sourceTree = "<group>"; };
 		F395C1922569C68E00942BFF /* SDL_iokitjoystick.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_iokitjoystick.c; sourceTree = "<group>"; };
@@ -1113,6 +1117,10 @@
 		F3DDCC522AFD42B600B0842B /* SDL_video_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_video_c.h; sourceTree = "<group>"; };
 		F3DDCC542AFD42B600B0842B /* SDL_rect_impl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_rect_impl.h; sourceTree = "<group>"; };
 		F3E5A6EA2AD5E0E600293D83 /* SDL_properties.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_properties.c; sourceTree = "<group>"; };
+		F3E6C38F2EE9F20000A6B39E /* SDL_hidapi_flydigi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_flydigi.h; sourceTree = "<group>"; };
+		F3E6C3902EE9F20000A6B39E /* SDL_hidapi_sinput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_sinput.h; sourceTree = "<group>"; };
+		F3E6C3912EE9F20000A6B39E /* SDL_report_descriptor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_report_descriptor.h; sourceTree = "<group>"; };
+		F3E6C3922EE9F20000A6B39E /* SDL_report_descriptor.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_report_descriptor.c; sourceTree = "<group>"; };
 		F3EFA5E92D5AB97300BCF22F /* SDL_stb.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_stb.c; sourceTree = "<group>"; };
 		F3EFA5EA2D5AB97300BCF22F /* SDL_stb_c.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_stb_c.h; sourceTree = "<group>"; };
 		F3EFA5EB2D5AB97300BCF22F /* SDL_surface_c.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_surface_c.h; sourceTree = "<group>"; };
@@ -1948,6 +1956,7 @@
 			children = (
 				F3395BA72D9A5971007246C8 /* SDL_hidapi_8bitdo.c */,
 				F32305FE28939F6400E66D30 /* SDL_hidapi_combined.c */,
+				F3E6C38F2EE9F20000A6B39E /* SDL_hidapi_flydigi.h */,
 				F3395BA72D9A5971007246C9 /* SDL_hidapi_flydigi.c */,
 				A7D8A7C923E2513E00DCD162 /* SDL_hidapi_gamecube.c */,
 				F3B6B8092DC3EA54004954FD /* SDL_hidapi_gip.c */,
@@ -1960,6 +1969,7 @@
 				A75FDBC323EA380300529352 /* SDL_hidapi_rumble.h */,
 				A75FDBC423EA380300529352 /* SDL_hidapi_rumble.c */,
 				9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */,
+				F3E6C3902EE9F20000A6B39E /* SDL_hidapi_sinput.h */,
 				02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */,
 				F3984CCF25BCC92800374F43 /* SDL_hidapi_stadia.c */,
 				A75FDAAC23E2795C00529352 /* SDL_hidapi_steam.c */,
@@ -1975,6 +1985,8 @@
 				63124A412E5C357500A53610 /* SDL_hidapi_zuiki.c */,
 				A7D8A7C423E2513E00DCD162 /* SDL_hidapijoystick.c */,
 				A7D8A7C723E2513E00DCD162 /* SDL_hidapijoystick_c.h */,
+				F3E6C3912EE9F20000A6B39E /* SDL_report_descriptor.h */,
+				F3E6C3922EE9F20000A6B39E /* SDL_report_descriptor.c */,
 			);
 			path = hidapi;
 			sourceTree = "<group>";
@@ -2554,6 +2566,9 @@
 				F3D46ADB2D20625800D9CBDF /* SDL_pen.h in Headers */,
 				F3D46ADC2D20625800D9CBDF /* SDL_render.h in Headers */,
 				F3D46ADD2D20625800D9CBDF /* SDL_assert.h in Headers */,
+				F3E6C3942EE9F20000A6B39E /* SDL_hidapi_flydigi.h in Headers */,
+				F3E6C3952EE9F20000A6B39E /* SDL_hidapi_sinput.h in Headers */,
+				F3E6C3962EE9F20000A6B39E /* SDL_report_descriptor.h in Headers */,
 				F3D46ADE2D20625800D9CBDF /* SDL_atomic.h in Headers */,
 				F3D46ADF2D20625800D9CBDF /* SDL_begin_code.h in Headers */,
 				F3D46AE02D20625800D9CBDF /* SDL_log.h in Headers */,
@@ -2923,6 +2938,7 @@
 				F3C1BD752D1F1A3000846529 /* SDL_tray_utils.c in Sources */,
 				F382071D284F362F004DD584 /* SDL_guid.c in Sources */,
 				A7D8BB8D23E2514500DCD162 /* SDL_touch.c in Sources */,
+				F3E6C3932EE9F20000A6B39E /* SDL_report_descriptor.c in Sources */,
 				F31A92D228D4CB39003BFD6A /* SDL_offscreenopengles.c in Sources */,
 				A1626A3E2617006A003F1973 /* SDL_triangle.c in Sources */,
 				A7D8B3F223E2514300DCD162 /* SDL_thread.c in Sources */,

+ 296 - 29
src/joystick/hidapi/SDL_hidapi_xboxone.c

@@ -26,6 +26,7 @@
 #include "../SDL_sysjoystick.h"
 #include "SDL_hidapijoystick_c.h"
 #include "SDL_hidapi_rumble.h"
+#include "SDL_report_descriptor.h"
 
 #ifdef SDL_JOYSTICK_HIDAPI_XBOXONE
 
@@ -33,7 +34,9 @@
 // #define DEBUG_JOYSTICK
 
 // Define this if you want to log all packets from the controller
-// #define DEBUG_XBOX_PROTOCOL
+#if 0
+#define DEBUG_XBOX_PROTOCOL
+#endif
 
 #if defined(SDL_PLATFORM_WIN32) || defined(SDL_PLATFORM_WINGDK)
 #define XBOX_ONE_DRIVER_ACTIVE  1
@@ -134,6 +137,8 @@ typedef struct
     bool has_unmapped_state;
     bool has_trigger_rumble;
     bool has_share_button;
+    bool has_separate_back_button;
+    bool has_separate_guide_button;
     Uint8 last_paddle_state;
     Uint8 low_frequency_rumble;
     Uint8 high_frequency_rumble;
@@ -142,6 +147,7 @@ typedef struct
     SDL_XboxOneRumbleState rumble_state;
     Uint64 rumble_time;
     bool rumble_pending;
+    SDL_ReportDescriptor *descriptor;
     Uint8 last_state[USB_PACKET_LENGTH];
     Uint8 *chunk_buffer;
     Uint32 chunk_length;
@@ -375,6 +381,32 @@ static bool HIDAPI_DriverXboxOne_InitDevice(SDL_HIDAPI_Device *device)
 
     device->context = ctx;
 
+    Uint8 descriptor[1024];
+    int descriptor_len = SDL_hid_get_report_descriptor(device->dev, descriptor, sizeof(descriptor));
+    if (descriptor_len > 0) {
+        HIDAPI_DumpPacket("Xbox One report descriptor: size = %d", descriptor, descriptor_len);
+
+        ctx->descriptor = SDL_ParseReportDescriptor(descriptor, descriptor_len);
+        if (ctx->descriptor) {
+            if (!SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_X) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_Y) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_Z) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_RZ) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_SIMULATION, USB_USAGE_SIMULATION_BRAKE) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_SIMULATION, USB_USAGE_SIMULATION_ACCELERATOR) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_BUTTON, 1) ||
+                !SDL_DescriptorHasUsage(ctx->descriptor, USB_USAGEPAGE_BUTTON, 15)) {
+                SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Xbox report descriptor missing expected usages, ignoring");
+                SDL_DestroyDescriptor(ctx->descriptor);
+                ctx->descriptor = NULL;
+            }
+        } else {
+            SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Couldn't parse Xbox report descriptor: %s", SDL_GetError());
+        }
+    } else {
+        SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "Xbox report descriptor not available");
+    }
+
     ctx->vendor_id = device->vendor_id;
     ctx->product_id = device->product_id;
     ctx->start_time = SDL_GetTicks();
@@ -583,6 +615,260 @@ static bool HIDAPI_DriverXboxOne_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *de
     return SDL_Unsupported();
 }
 
+static void HIDAPI_DriverXboxOne_HandleBatteryState(SDL_Joystick *joystick, unsigned int flags)
+{
+    bool on_usb = (((flags & 0x0C) >> 2) == 0);
+    SDL_PowerState state;
+    int percent = 0;
+
+    // Mapped percentage value from:
+    // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/input/gameinput/interfaces/igameinputdevice/methods/igameinputdevice_getbatterystate
+    switch (flags & 0x03) {
+    case 0:
+        percent = 10;
+        break;
+    case 1:
+        percent = 40;
+        break;
+    case 2:
+        percent = 70;
+        break;
+    case 3:
+        percent = 100;
+        break;
+    }
+    if (on_usb) {
+        state = SDL_POWERSTATE_CHARGING;
+    } else {
+        state = SDL_POWERSTATE_ON_BATTERY;
+    }
+    SDL_SendJoystickPowerInfo(joystick, state, percent);
+}
+
+static bool HIDAPI_DriverXboxOne_HandleDescriptorReport(SDL_Joystick *joystick, SDL_DriverXboxOne_Context *ctx, Uint8 *data, int size)
+{
+    const SDL_ReportDescriptor *descriptor = ctx->descriptor;
+    Uint64 timestamp = SDL_GetTicksNS();
+
+    // Skip the report ID
+    const Uint8 report_id = *data;
+    ++data;
+    --size;
+
+    for (int i = 0; i < descriptor->field_count; ++i) {
+        DescriptorInputField *field = &descriptor->fields[i];
+        if (field->report_id != report_id) {
+            continue;
+        }
+
+        unsigned int value;
+        if (!SDL_ReadReportData(data, size, field->bit_offset, field->bit_size, &value)) {
+            continue;
+        }
+
+        switch (field->usage) {
+        case MAKE_USAGE(USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_X):
+        {
+            Sint16 axis = (Sint16)((int)value - 0x8000);
+            SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_Y):
+        {
+            Sint16 axis = (Sint16)((int)value - 0x8000);
+            SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_Z):
+        {
+            Sint16 axis = (Sint16)((int)value - 0x8000);
+            SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_RZ):
+        {
+            Sint16 axis = (Sint16)((int)value - 0x8000);
+            SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_SIMULATION, USB_USAGE_SIMULATION_BRAKE):
+        {
+            Sint16 axis = (Sint16)(((int)value * 64) - 32768);
+            if (axis == 32704) {
+                axis = 32767;
+            }
+            SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_SIMULATION, USB_USAGE_SIMULATION_ACCELERATOR):
+        {
+            Sint16 axis = (Sint16)(((int)value * 64) - 32768);
+            if (axis == 32704) {
+                axis = 32767;
+            }
+            SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_GENERIC_DESKTOP, USB_USAGE_GENERIC_HAT):
+        {
+            Uint8 hat;
+
+            switch (value) {
+            case 1:
+                hat = SDL_HAT_UP;
+                break;
+            case 2:
+                hat = SDL_HAT_RIGHTUP;
+                break;
+            case 3:
+                hat = SDL_HAT_RIGHT;
+                break;
+            case 4:
+                hat = SDL_HAT_RIGHTDOWN;
+                break;
+            case 5:
+                hat = SDL_HAT_DOWN;
+                break;
+            case 6:
+                hat = SDL_HAT_LEFTDOWN;
+                break;
+            case 7:
+                hat = SDL_HAT_LEFT;
+                break;
+            case 8:
+                hat = SDL_HAT_LEFTUP;
+                break;
+            default:
+                hat = SDL_HAT_CENTERED;
+                break;
+            }
+            SDL_SendJoystickHat(timestamp, joystick, 0, hat);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 1):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 2):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 3):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 4):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 5):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 6):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 7):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 8):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 9):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 10):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 11):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 12):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 13):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 14):
+        case MAKE_USAGE(USB_USAGEPAGE_BUTTON, 15):
+        {
+            static const SDL_GamepadButton button_map[] = {
+                // 0x0001
+                SDL_GAMEPAD_BUTTON_SOUTH,
+                // 0x0002
+                SDL_GAMEPAD_BUTTON_EAST,
+                // 0x0004
+                SDL_GAMEPAD_BUTTON_INVALID,
+                // 0x0008
+                SDL_GAMEPAD_BUTTON_WEST,
+                // 0x0010
+                SDL_GAMEPAD_BUTTON_NORTH,
+                // 0x0020
+                SDL_GAMEPAD_BUTTON_INVALID,
+                // 0x0040
+                SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
+                // 0x0080
+                SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
+                // 0x0100
+                SDL_GAMEPAD_BUTTON_INVALID,
+                // 0x0200
+                SDL_GAMEPAD_BUTTON_INVALID,
+                // 0x0400
+                SDL_GAMEPAD_BUTTON_BACK,
+                // 0x0800
+                SDL_GAMEPAD_BUTTON_START,
+                // 0x1000
+                SDL_GAMEPAD_BUTTON_GUIDE,
+                // 0x2000
+                SDL_GAMEPAD_BUTTON_LEFT_STICK,
+                // 0x4000
+                SDL_GAMEPAD_BUTTON_RIGHT_STICK,
+            };
+
+            int button_index = (field->usage - MAKE_USAGE(USB_USAGEPAGE_BUTTON, 1));
+            SDL_GamepadButton button = button_map[button_index];
+            if (button == SDL_GAMEPAD_BUTTON_INVALID) {
+                break;
+            }
+            if (button == SDL_GAMEPAD_BUTTON_BACK && ctx->has_separate_back_button) {
+                break;
+            }
+            if (button == SDL_GAMEPAD_BUTTON_GUIDE && ctx->has_separate_guide_button) {
+                break;
+            }
+
+            bool pressed = (value != 0);
+            SDL_SendJoystickButton(timestamp, joystick, button, pressed);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_CONSUMER, USB_USAGE_CONSUMER_AC_BACK):
+        {
+            bool pressed = (value != 0);
+            if (pressed) {
+                ctx->has_separate_back_button = true;
+            }
+            SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, pressed);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_CONSUMER, USB_USAGE_CONSUMER_AC_HOME):
+        {
+            bool pressed = (value != 0);
+            if (pressed) {
+                ctx->has_separate_guide_button = true;
+            }
+            SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, pressed);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_CONSUMER, USB_USAGE_CONSUMER_RECORD):
+        {
+            if (ctx->has_share_button) {
+                bool pressed = (value != 0);
+                SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_XBOX_SHARE_BUTTON, pressed);
+            }
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_CONSUMER, USB_USAGE_CONSUMER_ORDER_MOVIE):
+        {
+            // This value is the currently selected profile
+            ctx->has_unmapped_state = (value == 0);
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_CONSUMER, USB_USAGE_CONSUMER_ASSIGN_SELECTION):
+        {
+            if (ctx->has_paddles) {
+                if (!ctx->has_unmapped_state) {
+                    value = 0;
+                }
+
+                Uint8 button = (Uint8)(SDL_GAMEPAD_BUTTON_XBOX_SHARE_BUTTON + ctx->has_share_button); // Next available button
+                SDL_SendJoystickButton(timestamp, joystick, button++, ((value & 0x1) != 0));
+                SDL_SendJoystickButton(timestamp, joystick, button++, ((value & 0x2) != 0));
+                SDL_SendJoystickButton(timestamp, joystick, button++, ((value & 0x4) != 0));
+                SDL_SendJoystickButton(timestamp, joystick, button++, ((value & 0x8) != 0));
+            }
+            break;
+        }
+        case MAKE_USAGE(USB_USAGEPAGE_DEVICE_CONTROLS, USB_USAGE_DEVICE_CONTROLS_BATTERY_STRENGTH):
+        {
+            HIDAPI_DriverXboxOne_HandleBatteryState(joystick, value);
+            break;
+        }
+        default:
+            break;
+        }
+    }
+    return true;
+}
+
 /*
  * The Xbox One Elite controller with 5.13+ firmware sends the unmapped state in a separate packet.
  * We can use this to send the paddle state when they aren't mapped
@@ -1066,33 +1352,7 @@ static void HIDAPI_DriverXboxOneBluetooth_HandleGuidePacket(SDL_Joystick *joysti
 
 static void HIDAPI_DriverXboxOneBluetooth_HandleBatteryPacket(SDL_Joystick *joystick, SDL_DriverXboxOne_Context *ctx, const Uint8 *data, int size)
 {
-    Uint8 flags = data[1];
-    bool on_usb = (((flags & 0x0C) >> 2) == 0);
-    SDL_PowerState state;
-    int percent = 0;
-
-    // Mapped percentage value from:
-    // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/input/gameinput/interfaces/igameinputdevice/methods/igameinputdevice_getbatterystate
-    switch (flags & 0x03) {
-    case 0:
-        percent = 10;
-        break;
-    case 1:
-        percent = 40;
-        break;
-    case 2:
-        percent = 70;
-        break;
-    case 3:
-        percent = 100;
-        break;
-    }
-    if (on_usb) {
-        state = SDL_POWERSTATE_CHARGING;
-    } else {
-        state = SDL_POWERSTATE_ON_BATTERY;
-    }
-    SDL_SendJoystickPowerInfo(joystick, state, percent);
+    HIDAPI_DriverXboxOne_HandleBatteryState(joystick, data[1]);
 }
 
 static void HIDAPI_DriverXboxOne_HandleSerialIDPacket(SDL_DriverXboxOne_Context *ctx, const Uint8 *data, int size)
@@ -1588,7 +1848,12 @@ static bool HIDAPI_DriverXboxOne_UpdateDevice(SDL_HIDAPI_Device *device)
 #ifdef DEBUG_XBOX_PROTOCOL
         HIDAPI_DumpPacket("Xbox One packet: size = %d", data, size);
 #endif
-        if (device->is_bluetooth) {
+        if (ctx->descriptor) {
+            if (!joystick) {
+                break;
+            }
+            HIDAPI_DriverXboxOne_HandleDescriptorReport(joystick, ctx, data, size);
+        } else if (device->is_bluetooth) {
             switch (data[0]) {
             case 0x01:
                 if (!joystick) {
@@ -1647,6 +1912,8 @@ static void HIDAPI_DriverXboxOne_FreeDevice(SDL_HIDAPI_Device *device)
 {
     SDL_DriverXboxOne_Context *ctx = (SDL_DriverXboxOne_Context *)device->context;
 
+    SDL_DestroyDescriptor(ctx->descriptor);
+
     HIDAPI_GIP_DestroyChunkBuffer(ctx);
 }
 

+ 616 - 0
src/joystick/hidapi/SDL_report_descriptor.c

@@ -0,0 +1,616 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#include "SDL_report_descriptor.h"
+
+// This is a very simple (and non-compliant!) report descriptor parser
+// used to quickly parse Xbox Bluetooth reports
+
+typedef enum
+{
+    DescriptorItemTypeMain = 0,
+    DescriptorItemTypeGlobal = 1,
+    DescriptorItemTypeLocal = 2,
+    DescriptorItemTypeReserved = 3,
+} ItemType;
+
+typedef enum
+{
+    MainTagInput = 0x8,
+    MainTagOutput = 0x9,
+    MainTagFeature = 0xb,
+    MainTagCollection = 0xa,
+    MainTagEndCollection = 0xc,
+} MainTag;
+
+typedef enum
+{
+    MainFlagConstant        = 0x0001,
+    MainFlagVariable        = 0x0002,
+    MainFlagRelative        = 0x0004,
+    MainFlagWrap            = 0x0008,
+    MainFlagNonLinear        = 0x0010,
+    MainFlagNoPreferred        = 0x0020,
+    MainFlagNullState        = 0x0040,
+    MainFlagVolatile        = 0x0080,
+    MainFlagBufferedBytes    = 0x0100,
+} MainFlag;
+
+typedef enum
+{
+    GlobalTagUsagePage = 0x0,
+    GlobalTagLogicalMinimum = 0x1,
+    GlobalTagLogicalMaximum = 0x2,
+    GlobalTagPhysicalMinimum = 0x3,
+    GlobalTagPhysicalMaximum = 0x4,
+    GlobalTagUnitExponent = 0x5,
+    GlobalTagUnit = 0x6,
+    GlobalTagReportSize = 0x7,
+    GlobalTagReportID = 0x8,
+    GlobalTagReportCount = 0x9,
+    GlobalTagPush = 0xa,
+    GlobalTagPop = 0xb,
+} GlobalTag;
+
+typedef enum
+{
+    LocalTagUsage = 0x0,
+    LocalTagUsageMinimum = 0x1,
+    LocalTagUsageMaximum = 0x2,
+    LocalTagDesignatorIndex = 0x3,
+    LocalTagDesignatorMinimum = 0x4,
+    LocalTagDesignatorMaximum = 0x5,
+    LocalTagStringIndex = 0x7,
+    LocalTagStringMinimum = 0x8,
+    LocalTagStringMaximum = 0x9,
+    LocalTagDelimiter = 0xa,
+} LocalTag;
+
+typedef struct
+{
+    unsigned int usage_page;
+    unsigned int report_size;
+    unsigned int report_count;
+    unsigned int report_id;
+} DescriptorGlobalState;
+
+typedef struct
+{
+    unsigned int usage_minimum;
+    unsigned int usage_maximum;
+    int usage_maxcount;
+    int usage_count;
+    Uint32 *usages;
+} DescriptorLocalState;
+
+typedef struct
+{
+    int collection_depth;
+    DescriptorGlobalState global;
+    DescriptorLocalState local;
+    int field_maxcount;
+    int field_count;
+    int field_offset;
+    DescriptorInputField *fields;
+} DescriptorContext;
+
+static void DebugDescriptor(DescriptorContext *ctx, const char *fmt, ...)
+{
+#ifdef DEBUG_DESCRIPTOR
+    va_list ap;
+    va_start(ap, fmt);
+    char *message = NULL;
+    SDL_vasprintf(&message, fmt, ap);
+    va_end(ap);
+    if (ctx->collection_depth > 0) {
+        size_t len = 4 * ctx->collection_depth + SDL_strlen(message) + 1;
+        char *output = (char *)SDL_malloc(len);
+        if (output) {
+            SDL_memset(output, ' ', 4 * ctx->collection_depth);
+            output[4 * ctx->collection_depth] = '\0';
+            SDL_strlcat(output, message, len);
+            SDL_free(message);
+            message = output;
+        }
+    }
+    SDL_Log("%s", message);
+    SDL_free(message);
+#endif // DEBUG_DESCRIPTOR
+}
+
+static void DebugMainTag(DescriptorContext *ctx, const char *tag, unsigned int flags)
+{
+#ifdef DEBUG_DESCRIPTOR
+    char message[1024] = { 0 };
+
+    SDL_strlcat(message, tag, sizeof(message));
+    SDL_strlcat(message, "(", sizeof(message));
+    if (flags & MainFlagConstant) {
+        SDL_strlcat(message, " Constant", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Data", sizeof(message));
+    }
+    if (flags & MainFlagVariable) {
+        SDL_strlcat(message, " Variable", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Array", sizeof(message));
+    }
+    if (flags & MainFlagRelative) {
+        SDL_strlcat(message, " Relative", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Absolute", sizeof(message));
+    }
+    if (flags & MainFlagWrap) {
+        SDL_strlcat(message, " Wrap", sizeof(message));
+    } else {
+        SDL_strlcat(message, " No Wrap", sizeof(message));
+    }
+    if (flags & MainFlagNonLinear) {
+        SDL_strlcat(message, " Non Linear", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Linear", sizeof(message));
+    }
+    if (flags & MainFlagNoPreferred) {
+        SDL_strlcat(message, " No Preferred", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Preferred State", sizeof(message));
+    }
+    if (flags & MainFlagNullState) {
+        SDL_strlcat(message, " Null State", sizeof(message));
+    } else {
+        SDL_strlcat(message, " No Null Position", sizeof(message));
+    }
+    if (flags & MainFlagVolatile) {
+        SDL_strlcat(message, " Volatile", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Non Volatile", sizeof(message));
+    }
+    if (flags & MainFlagBufferedBytes) {
+        SDL_strlcat(message, " Buffered Bytes", sizeof(message));
+    } else {
+        SDL_strlcat(message, " Bit Field", sizeof(message));
+    }
+    SDL_strlcat(message, " )", sizeof(message));
+
+    DebugDescriptor(ctx, "%s", message);
+
+#endif // DEBUG_DESCRIPTOR
+}
+
+static unsigned int ReadValue(const Uint8 *data, int size)
+{
+    unsigned int value = 0;
+
+    int shift = 0;
+    while (size--) {
+        value |= ((unsigned int)*data++) << shift;
+        shift += 8;
+    }
+    return value;
+}
+
+static void ResetLocalState(DescriptorContext *ctx)
+{
+    ctx->local.usage_minimum = 0;
+    ctx->local.usage_maximum = 0;
+    ctx->local.usage_count = 0;
+}
+
+static bool AddUsage(DescriptorContext *ctx, unsigned int usage)
+{
+    if (ctx->local.usage_count == ctx->local.usage_maxcount) {
+        int usage_maxcount = ctx->local.usage_maxcount + 4;
+        Uint32 *usages = (Uint32 *)SDL_realloc(ctx->local.usages, usage_maxcount * sizeof(*usages));
+        if (!usages) {
+            return false;
+        }
+        ctx->local.usages = usages;
+        ctx->local.usage_maxcount = usage_maxcount;
+    }
+
+    if (usage <= 0xFFFF) {
+        usage |= (ctx->global.usage_page << 16);
+    }
+    ctx->local.usages[ctx->local.usage_count++] = usage;
+    return true;
+}
+
+static bool AddInputField(DescriptorContext *ctx, unsigned int usage, int bit_size)
+{
+    if (ctx->field_count == ctx->field_maxcount) {
+        int field_maxcount = ctx->field_maxcount + 4;
+        DescriptorInputField *fields = (DescriptorInputField *)SDL_realloc(ctx->fields, field_maxcount * sizeof(*fields));
+        if (!fields) {
+            return false;
+        }
+        ctx->fields = fields;
+        ctx->field_maxcount = field_maxcount;
+    }
+
+    DescriptorInputField *field = &ctx->fields[ctx->field_count++];
+    field->report_id = (Uint8)ctx->global.report_id;
+    field->usage = usage;
+    field->bit_offset = ctx->field_offset;
+    field->bit_size = bit_size;
+
+    DebugDescriptor(ctx, "Adding report %d field 0x%.8x size %d bits at bit offset %d", field->report_id, field->usage, field->bit_size, field->bit_offset);
+    return true;
+}
+
+static bool AddInputFields(DescriptorContext *ctx)
+{
+    unsigned int usage = 0;
+
+    if (ctx->global.report_count == 0 || ctx->global.report_size == 0) {
+        return true;
+    }
+
+    if (ctx->local.usage_count == 0 &&
+        ctx->local.usage_minimum > 0 &&
+        ctx->local.usage_maximum >= ctx->local.usage_minimum) {
+        for (usage = ctx->local.usage_minimum; usage <= ctx->local.usage_maximum; ++usage) {
+            if (!AddUsage(ctx, usage)) {
+                return false;
+            }
+        }
+    }
+
+    int usage_index = 0;
+    for (unsigned int i = 0; i < ctx->global.report_count; ++i) {
+        if (usage_index < ctx->local.usage_count) {
+            usage = ctx->local.usages[usage_index];
+            if (usage_index < (ctx->local.usage_count - 1)) {
+                ++usage_index;
+            }
+        }
+
+        int size = (int)ctx->global.report_size;
+        if (usage > 0) {
+            if (!AddInputField(ctx, usage, size)) {
+                return false;
+            }
+        }
+        ctx->field_offset += size;
+    }
+    return true;
+}
+
+static bool ParseMainItem(DescriptorContext *ctx, int tag, int size, const Uint8 *data)
+{
+    unsigned int flags;
+
+    switch (tag) {
+    case MainTagInput:
+        flags = ReadValue(data, size);
+        DebugMainTag(ctx, "MainTagInput", flags);
+        AddInputFields(ctx);
+        break;
+    case MainTagOutput:
+        flags = ReadValue(data, size);
+        DebugMainTag(ctx, "MainTagOutput", flags);
+        break;
+    case MainTagFeature:
+        flags = ReadValue(data, size);
+        DebugMainTag(ctx, "MainTagFeature", flags);
+        break;
+    case MainTagCollection:
+        DebugDescriptor(ctx, "MainTagCollection");
+        switch (*data) {
+        case 0x00:
+            DebugDescriptor(ctx, "Physical");
+            break;
+        case 0x01:
+            DebugDescriptor(ctx, "Application");
+            break;
+        case 0x02:
+            DebugDescriptor(ctx, "Logical");
+            break;
+        case 0x03:
+            DebugDescriptor(ctx, "Report");
+            break;
+        case 0x04:
+            DebugDescriptor(ctx, "Named Array");
+            break;
+        case 0x05:
+            DebugDescriptor(ctx, "Usage Switch");
+            break;
+        case 0x06:
+            DebugDescriptor(ctx, "Usage Modifier");
+            break;
+        default:
+            break;
+        }
+        ++ctx->collection_depth;
+        break;
+    case MainTagEndCollection:
+        if (ctx->collection_depth > 0) {
+            --ctx->collection_depth;
+        }
+        DebugDescriptor(ctx, "MainTagEndCollection");
+        break;
+    default:
+        DebugDescriptor(ctx, "Unknown main tag: %d", tag);
+        break;
+    }
+
+    ResetLocalState(ctx);
+
+    return true;
+}
+
+static bool ParseGlobalItem(DescriptorContext *ctx, int tag, int size, const Uint8 *data)
+{
+    unsigned int value;
+
+    switch (tag) {
+    case GlobalTagUsagePage:
+        ctx->global.usage_page = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagUsagePage: 0x%.4x", ctx->global.usage_page);
+        break;
+    case GlobalTagLogicalMinimum:
+        value = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagLogicalMinimum: %u", value);
+        break;
+    case GlobalTagLogicalMaximum:
+        value = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagLogicalMaximum: %u", value);
+        break;
+    case GlobalTagPhysicalMinimum:
+        value = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagPhysicalMinimum: %u", value);
+        break;
+    case GlobalTagPhysicalMaximum:
+        value = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagPhysicalMaximum: %u", value);
+        break;
+    case GlobalTagUnitExponent:
+        DebugDescriptor(ctx, "GlobalTagUnitExponent");
+        break;
+    case GlobalTagUnit:
+        DebugDescriptor(ctx, "GlobalTagUnit");
+        break;
+    case GlobalTagReportSize:
+        ctx->global.report_size = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagReportSize: %u", ctx->global.report_size);
+        break;
+    case GlobalTagReportID:
+        ctx->global.report_id = ReadValue(data, size);
+        ctx->field_offset = 0;
+        DebugDescriptor(ctx, "GlobalTagReportID: %u", ctx->global.report_id);
+        break;
+    case GlobalTagReportCount:
+        ctx->global.report_count = ReadValue(data, size);
+        DebugDescriptor(ctx, "GlobalTagReportCount: %u", ctx->global.report_count);
+        break;
+    case GlobalTagPush:
+        DebugDescriptor(ctx, "GlobalTagPush");
+        break;
+    case GlobalTagPop:
+        DebugDescriptor(ctx, "GlobalTagPop");
+        break;
+    default:
+        DebugDescriptor(ctx, "Unknown global tag");
+        break;
+    }
+    return true;
+}
+
+static bool ParseLocalItem(DescriptorContext *ctx, int tag, int size, const Uint8 *data)
+{
+    unsigned int value;
+
+    switch (tag) {
+    case LocalTagUsage:
+        value = ReadValue(data, size);
+        AddUsage(ctx, value);
+        DebugDescriptor(ctx, "LocalTagUsage: 0x%.4x", value);
+        break;
+    case LocalTagUsageMinimum:
+        ctx->local.usage_minimum = ReadValue(data, size);
+        DebugDescriptor(ctx, "LocalTagUsageMinimum: 0x%.4x", ctx->local.usage_minimum);
+        break;
+    case LocalTagUsageMaximum:
+        ctx->local.usage_maximum = ReadValue(data, size);
+        DebugDescriptor(ctx, "LocalTagUsageMaximum: 0x%.4x", ctx->local.usage_maximum);
+        break;
+    case LocalTagDesignatorIndex:
+        DebugDescriptor(ctx, "LocalTagDesignatorIndex");
+        break;
+    case LocalTagDesignatorMinimum:
+        DebugDescriptor(ctx, "LocalTagDesignatorMinimum");
+        break;
+    case LocalTagDesignatorMaximum:
+        DebugDescriptor(ctx, "LocalTagDesignatorMaximum");
+        break;
+    case LocalTagStringIndex:
+        DebugDescriptor(ctx, "LocalTagStringIndex");
+        break;
+    case LocalTagStringMinimum:
+        DebugDescriptor(ctx, "LocalTagStringMinimum");
+        break;
+    case LocalTagStringMaximum:
+        DebugDescriptor(ctx, "LocalTagStringMaximum");
+        break;
+    case LocalTagDelimiter:
+        DebugDescriptor(ctx, "LocalTagDelimiter");
+        break;
+    default:
+        DebugDescriptor(ctx, "Unknown local tag");
+        break;
+    }
+    return true;
+}
+
+bool ParseDescriptor(DescriptorContext *ctx, const Uint8 *descriptor, int descriptor_size)
+{
+    SDL_zerop(ctx);
+
+    for (const Uint8 *here = descriptor; here < descriptor + descriptor_size; ) {
+        static const int sizes[4] = { 0, 1, 2, 4 };
+        Uint8 data = *here++;
+        int size = sizes[(data & 0x3)];
+        int type = ((data >> 2) & 0x3);
+        int tag = (data >> 4);
+
+        if ((here + size) > (descriptor + descriptor_size)) {
+            return SDL_SetError("Invalid descriptor");
+        }
+
+#ifdef DEBUG_DESCRIPTOR
+        SDL_Log("Data: 0x%.2x, size: %d, type: %d, tag: %d", data, size, type, tag);
+#endif
+        switch (type) {
+        case DescriptorItemTypeMain:
+            if (!ParseMainItem(ctx, tag, size, here)) {
+                return false;
+            }
+            break;
+        case DescriptorItemTypeGlobal:
+            if (!ParseGlobalItem(ctx, tag, size, here)) {
+                return false;
+            }
+            break;
+        case DescriptorItemTypeLocal:
+            if (!ParseLocalItem(ctx, tag, size, here)) {
+                return false;
+            }
+            break;
+        case DescriptorItemTypeReserved:
+            // Long items are currently unsupported
+            return SDL_Unsupported();
+        }
+
+        here += size;
+    }
+    return true;
+}
+
+static void CleanupContext(DescriptorContext *ctx)
+{
+    SDL_free(ctx->local.usages);
+    SDL_free(ctx->fields);
+}
+
+SDL_ReportDescriptor *SDL_ParseReportDescriptor(const Uint8 *descriptor, int descriptor_size)
+{
+    SDL_ReportDescriptor *result = NULL;
+
+    DescriptorContext ctx;
+    if (ParseDescriptor(&ctx, descriptor, descriptor_size)) {
+        result = (SDL_ReportDescriptor *)SDL_malloc(sizeof(*result));
+        if (result) {
+            result->field_count = ctx.field_count;
+            result->fields = ctx.fields;
+            ctx.fields = NULL;
+        }
+    }
+    CleanupContext(&ctx);
+
+    return result;
+}
+
+bool SDL_DescriptorHasUsage(SDL_ReportDescriptor *descriptor, Uint16 usage_page, Uint16 usage)
+{
+    if (!descriptor) {
+        return false;
+    }
+
+    Uint32 full_usage = (((Uint32)usage_page << 16) | usage);
+    for (int i = 0; i < descriptor->field_count; ++i) {
+        if (descriptor->fields[i].usage == full_usage) {
+            return true;
+        }
+    }
+    return false;
+}
+
+void SDL_DestroyDescriptor(SDL_ReportDescriptor *descriptor)
+{
+    if (descriptor) {
+        SDL_free(descriptor->fields);
+        SDL_free(descriptor);
+    }
+}
+
+bool SDL_ReadReportData(const Uint8 *data, int size, int bit_offset, int bit_size, unsigned int *value)
+{
+    int offset = (bit_offset / 8);
+    if (offset >= size) {
+        *value = 0;
+        return SDL_SetError("Out of bounds reading report data");
+    }
+
+    *value = ReadValue(data + offset, (bit_size + 7) / 8);
+
+    int shift = (bit_offset % 8);
+    if (shift > 0) {
+        *value >>= shift;
+    }
+
+    switch (bit_size) {
+    case 1:
+        *value &= 0x1;
+        break;
+    case 4:
+        *value &= 0xf;
+        break;
+    case 10:
+        *value &= 0x3ff;
+        break;
+    case 15:
+        *value &= 0x7fff;
+        break;
+    default:
+        SDL_assert((bit_size % 8) == 0);
+        break;
+    }
+    return true;
+}
+
+#ifdef TEST_MAIN
+
+#include <SDL3/SDL_main.h>
+
+int main(int argc, char *argv[])
+{
+    const char *file = argv[1];
+    if (argc < 2) {
+        SDL_Log("Usage: %s file", argv[0]);
+        return 1;
+    }
+
+    size_t descriptor_size = 0;
+    Uint8 *descriptor = SDL_LoadFile(argv[1], &descriptor_size);
+    if (!descriptor) {
+        SDL_Log("Couldn't load %s: %s", argv[1], SDL_GetError());
+        return 2;
+    }
+
+    DescriptorContext ctx;
+    if (!ParseDescriptor(&ctx, descriptor, descriptor_size)) {
+        SDL_Log("Couldn't parse %s: %s", argv[1], SDL_GetError());
+        return 3;
+    }
+    return 0;
+}
+
+#endif // TEST_MAIN

+ 40 - 0
src/joystick/hidapi/SDL_report_descriptor.h

@@ -0,0 +1,40 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+typedef struct
+{
+	Uint8 report_id;
+	Uint32 usage;
+	int bit_offset;
+	int bit_size;
+} DescriptorInputField;
+
+typedef struct
+{
+	int field_count;
+	DescriptorInputField *fields;
+} SDL_ReportDescriptor;
+
+extern SDL_ReportDescriptor *SDL_ParseReportDescriptor(const Uint8 *descriptor, int descriptor_size);
+extern bool SDL_DescriptorHasUsage(SDL_ReportDescriptor *descriptor, Uint16 usage_page, Uint16 usage);
+extern void SDL_DestroyDescriptor(SDL_ReportDescriptor *descriptor);
+extern bool SDL_ReadReportData(const Uint8 *data, int size, int bit_offset, int bit_size, unsigned int *value);

+ 19 - 0
src/joystick/usb_ids.h

@@ -182,7 +182,10 @@
 
 // USB usage pages
 #define USB_USAGEPAGE_GENERIC_DESKTOP 0x0001
+#define USB_USAGEPAGE_SIMULATION      0x0002
+#define USB_USAGEPAGE_DEVICE_CONTROLS 0x0006
 #define USB_USAGEPAGE_BUTTON          0x0009
+#define USB_USAGEPAGE_CONSUMER        0x000C
 #define USB_USAGEPAGE_VENDOR_FLYDIGI  0xFFA0
 
 // USB usages for USAGE_PAGE_GENERIC_DESKTOP
@@ -204,6 +207,22 @@
 #define USB_USAGE_GENERIC_WHEEL               0x0038
 #define USB_USAGE_GENERIC_HAT                 0x0039
 
+// USB usages for USB_USAGEPAGE_SIMULATION
+#define USB_USAGE_SIMULATION_ACCELERATOR      0x00C4
+#define USB_USAGE_SIMULATION_BRAKE            0x00C5
+
+// USB usages for USB_USAGEPAGE_DEVICE_CONTROLS
+#define USB_USAGE_DEVICE_CONTROLS_BATTERY_STRENGTH 0x0020
+
+// USB usages for USB_USAGEPAGE_CONSUMER
+#define USB_USAGE_CONSUMER_ASSIGN_SELECTION   0x0081
+#define USB_USAGE_CONSUMER_ORDER_MOVIE        0x0085
+#define USB_USAGE_CONSUMER_RECORD             0x00B2
+#define USB_USAGE_CONSUMER_AC_HOME            0x0223
+#define USB_USAGE_CONSUMER_AC_BACK            0x0224
+
+#define MAKE_USAGE(PAGE, USAGE) (((Uint32)PAGE) << 16 | USAGE)
+
 /* Bluetooth SIG assigned Company Identifiers
    https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/ */
 #define BLUETOOTH_VENDOR_AMAZON 0x0171