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
This commit is contained in:
Sam Lantinga
2025-12-10 09:56:30 -08:00
parent ef416e84a1
commit 450a2cb5e4
9 changed files with 1002 additions and 32 deletions

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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 */,

View File

@@ -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);
}

View File

@@ -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 = 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

View File

@@ -0,0 +1,40 @@
/*
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"
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);

View File

@@ -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