SDL: Added support for parsing the Xbox report descriptor

From 450a2cb5e485455c5b05dd8cfc1d3cb10cd7452b Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 10 Dec 2025 09:56:30 -0800
Subject: [PATCH] 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
---
 VisualC-GDK/SDL/SDL.vcxproj                 |   2 +
 VisualC-GDK/SDL/SDL.vcxproj.filters         |   2 +
 VisualC/SDL/SDL.vcxproj                     |   2 +
 VisualC/SDL/SDL.vcxproj.filters             |   6 +
 Xcode/SDL/SDL.xcodeproj/project.pbxproj     |  22 +-
 src/joystick/hidapi/SDL_hidapi_xboxone.c    | 325 ++++++++++-
 src/joystick/hidapi/SDL_report_descriptor.c | 616 ++++++++++++++++++++
 src/joystick/hidapi/SDL_report_descriptor.h |  40 ++
 src/joystick/usb_ids.h                      |  19 +
 9 files changed, 1002 insertions(+), 32 deletions(-)
 create mode 100644 src/joystick/hidapi/SDL_report_descriptor.c
 create mode 100644 src/joystick/hidapi/SDL_report_descriptor.h

diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index 7e2e93a4f1d28..a1356ab17f8f5 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/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" />
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index e54d76cbfa355..a2f431abf1942 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/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" />
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index f5a1612f4dee0..2937791cf7169 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/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" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index b23275a29e50d..e1014918b2a31 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/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>
diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
index b6aca459a4483..7dbf5a28bd53a 100644
--- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj
+++ b/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 */,
diff --git a/src/joystick/hidapi/SDL_hidapi_xboxone.c b/src/joystick/hidapi/SDL_hidapi_xboxone.c
index c48b9e1b8ccca..35e317e967090 100644
--- a/src/joystick/hidapi/SDL_hidapi_xboxone.c
+++ b/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);
 }
 
diff --git a/src/joystick/hidapi/SDL_report_descriptor.c b/src/joystick/hidapi/SDL_report_descriptor.c
new file mode 100644
index 0000000000000..5dbc5246ed005
--- /dev/null
+++ b/src/joystick/hidapi/SDL_report_descriptor.c
@@ -0,0 +1,616 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  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        = 

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