1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#ifdef SDL_JOYSTICK_HIDAPI
24
25#include "../../SDL_hints_c.h"
26#include "../SDL_sysjoystick.h"
27#include "SDL_hidapijoystick_c.h"
28#include "SDL_hidapi_rumble.h"
29
30#ifdef SDL_JOYSTICK_HIDAPI_XBOX360
31
32// Define this if you want to log all packets from the controller
33// #define DEBUG_XBOX_PROTOCOL
34
35typedef struct
36{
37 SDL_HIDAPI_Device *device;
38 SDL_Joystick *joystick;
39 int player_index;
40 bool player_lights;
41 Uint8 last_state[USB_PACKET_LENGTH];
42} SDL_DriverXbox360_Context;
43
44static void HIDAPI_DriverXbox360_RegisterHints(SDL_HintCallback callback, void *userdata)
45{
46 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX, callback, userdata);
47 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360, callback, userdata);
48}
49
50static void HIDAPI_DriverXbox360_UnregisterHints(SDL_HintCallback callback, void *userdata)
51{
52 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX, callback, userdata);
53 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360, callback, userdata);
54}
55
56static bool HIDAPI_DriverXbox360_IsEnabled(void)
57{
58 return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360,
59 SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT)));
60}
61
62static bool HIDAPI_DriverXbox360_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GamepadType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol)
63{
64 const int XB360W_IFACE_PROTOCOL = 129; // Wireless
65
66 if (vendor_id == USB_VENDOR_ASTRO && product_id == USB_PRODUCT_ASTRO_C40_XBOX360) {
67 // This is the ASTRO C40 in Xbox 360 mode
68 return true;
69 }
70 if (vendor_id == USB_VENDOR_NVIDIA) {
71 // This is the NVIDIA Shield controller which doesn't talk Xbox controller protocol
72 return false;
73 }
74 if ((vendor_id == USB_VENDOR_MICROSOFT && (product_id == USB_PRODUCT_XBOX360_WIRELESS_RECEIVER_THIRDPARTY2 || product_id == USB_PRODUCT_XBOX360_WIRELESS_RECEIVER)) ||
75 (type == SDL_GAMEPAD_TYPE_XBOX360 && interface_protocol == XB360W_IFACE_PROTOCOL)) {
76 // This is the wireless dongle, which talks a different protocol
77 return false;
78 }
79 if (interface_number > 0) {
80 // This is the chatpad or other input interface, not the Xbox 360 interface
81 return false;
82 }
83#if defined(SDL_PLATFORM_MACOS) && defined(SDL_JOYSTICK_MFI)
84 if (SDL_IsJoystickSteamVirtualGamepad(vendor_id, product_id, version)) {
85 // GCController support doesn't work with the Steam Virtual Gamepad
86 return true;
87 } else {
88 // On macOS you can't write output reports to wired XBox controllers,
89 // so we'll just use the GCController support instead.
90 return false;
91 }
92#else
93 return (type == SDL_GAMEPAD_TYPE_XBOX360);
94#endif
95}
96
97static bool SetSlotLED(SDL_hid_device *dev, Uint8 slot, bool on)
98{
99 const bool blink = false;
100 Uint8 mode = on ? ((blink ? 0x02 : 0x06) + slot) : 0;
101 Uint8 led_packet[] = { 0x01, 0x03, 0x00 };
102
103 led_packet[2] = mode;
104 if (SDL_hid_write(dev, led_packet, sizeof(led_packet)) != sizeof(led_packet)) {
105 return false;
106 }
107 return true;
108}
109
110static void UpdateSlotLED(SDL_DriverXbox360_Context *ctx)
111{
112 if (ctx->player_lights && ctx->player_index >= 0) {
113 SetSlotLED(ctx->device->dev, (ctx->player_index % 4), true);
114 } else {
115 SetSlotLED(ctx->device->dev, 0, false);
116 }
117}
118
119static void SDLCALL SDL_PlayerLEDHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
120{
121 SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)userdata;
122 bool player_lights = SDL_GetStringBoolean(hint, true);
123
124 if (player_lights != ctx->player_lights) {
125 ctx->player_lights = player_lights;
126
127 UpdateSlotLED(ctx);
128 HIDAPI_UpdateDeviceProperties(ctx->device);
129 }
130}
131
132static bool HIDAPI_DriverXbox360_InitDevice(SDL_HIDAPI_Device *device)
133{
134 SDL_DriverXbox360_Context *ctx;
135
136 ctx = (SDL_DriverXbox360_Context *)SDL_calloc(1, sizeof(*ctx));
137 if (!ctx) {
138 return false;
139 }
140 ctx->device = device;
141
142 device->context = ctx;
143
144 device->type = SDL_GAMEPAD_TYPE_XBOX360;
145
146 if (SDL_IsJoystickSteamVirtualGamepad(device->vendor_id, device->product_id, device->version) &&
147 device->product_string && SDL_strncmp(device->product_string, "GamePad-", 8) == 0) {
148 int slot = 0;
149 SDL_sscanf(device->product_string, "GamePad-%d", &slot);
150 device->steam_virtual_gamepad_slot = (slot - 1);
151 }
152
153 return HIDAPI_JoystickConnected(device, NULL);
154}
155
156static int HIDAPI_DriverXbox360_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id)
157{
158 return -1;
159}
160
161static void HIDAPI_DriverXbox360_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index)
162{
163 SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)device->context;
164
165 if (!ctx->joystick) {
166 return;
167 }
168
169 ctx->player_index = player_index;
170
171 UpdateSlotLED(ctx);
172}
173
174static bool HIDAPI_DriverXbox360_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
175{
176 SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)device->context;
177
178 SDL_AssertJoysticksLocked();
179
180 ctx->joystick = joystick;
181 SDL_zeroa(ctx->last_state);
182
183 // Initialize player index (needed for setting LEDs)
184 ctx->player_index = SDL_GetJoystickPlayerIndex(joystick);
185 ctx->player_lights = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED, true);
186 UpdateSlotLED(ctx);
187
188 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED,
189 SDL_PlayerLEDHintChanged, ctx);
190
191 // Initialize the joystick capabilities
192 joystick->nbuttons = 11;
193 joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
194 joystick->nhats = 1;
195
196 return true;
197}
198
199static bool HIDAPI_DriverXbox360_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
200{
201 Uint8 rumble_packet[] = { 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
202
203 rumble_packet[3] = (low_frequency_rumble >> 8);
204 rumble_packet[4] = (high_frequency_rumble >> 8);
205
206 if (SDL_HIDAPI_SendRumble(device, rumble_packet, sizeof(rumble_packet)) != sizeof(rumble_packet)) {
207 return SDL_SetError("Couldn't send rumble packet");
208 }
209 return true;
210}
211
212static bool HIDAPI_DriverXbox360_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
213{
214 return SDL_Unsupported();
215}
216
217static Uint32 HIDAPI_DriverXbox360_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
218{
219 SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)device->context;
220 Uint32 result = SDL_JOYSTICK_CAP_RUMBLE;
221
222 if (ctx->player_lights) {
223 result |= SDL_JOYSTICK_CAP_PLAYER_LED;
224 }
225 return result;
226}
227
228static bool HIDAPI_DriverXbox360_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
229{
230 return SDL_Unsupported();
231}
232
233static bool HIDAPI_DriverXbox360_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size)
234{
235 return SDL_Unsupported();
236}
237
238static bool HIDAPI_DriverXbox360_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled)
239{
240 return SDL_Unsupported();
241}
242
243static void HIDAPI_DriverXbox360_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverXbox360_Context *ctx, Uint8 *data, int size)
244{
245 Sint16 axis;
246#ifdef SDL_PLATFORM_MACOS
247 const bool invert_y_axes = false;
248#else
249 const bool invert_y_axes = true;
250#endif
251 Uint64 timestamp = SDL_GetTicksNS();
252
253 if (ctx->last_state[2] != data[2]) {
254 Uint8 hat = 0;
255
256 if (data[2] & 0x01) {
257 hat |= SDL_HAT_UP;
258 }
259 if (data[2] & 0x02) {
260 hat |= SDL_HAT_DOWN;
261 }
262 if (data[2] & 0x04) {
263 hat |= SDL_HAT_LEFT;
264 }
265 if (data[2] & 0x08) {
266 hat |= SDL_HAT_RIGHT;
267 }
268 SDL_SendJoystickHat(timestamp, joystick, 0, hat);
269
270 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[2] & 0x10) != 0));
271 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[2] & 0x20) != 0));
272 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[2] & 0x40) != 0));
273 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[2] & 0x80) != 0));
274 }
275
276 if (ctx->last_state[3] != data[3]) {
277 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[3] & 0x01) != 0));
278 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[3] & 0x02) != 0));
279 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[3] & 0x04) != 0));
280 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x10) != 0));
281 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x20) != 0));
282 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x40) != 0));
283 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x80) != 0));
284 }
285
286 axis = ((int)data[4] * 257) - 32768;
287 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
288 axis = ((int)data[5] * 257) - 32768;
289 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis);
290 axis = SDL_Swap16LE(*(Sint16 *)(&data[6]));
291 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
292 axis = SDL_Swap16LE(*(Sint16 *)(&data[8]));
293 if (invert_y_axes) {
294 axis = ~axis;
295 }
296 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
297 axis = SDL_Swap16LE(*(Sint16 *)(&data[10]));
298 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
299 axis = SDL_Swap16LE(*(Sint16 *)(&data[12]));
300 if (invert_y_axes) {
301 axis = ~axis;
302 }
303 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis);
304
305 SDL_memcpy(ctx->last_state, data, SDL_min((size_t)size, sizeof(ctx->last_state)));
306}
307
308static bool HIDAPI_DriverXbox360_UpdateDevice(SDL_HIDAPI_Device *device)
309{
310 SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)device->context;
311 SDL_Joystick *joystick = NULL;
312 Uint8 data[USB_PACKET_LENGTH];
313 int size = 0;
314
315 if (device->num_joysticks > 0) {
316 joystick = SDL_GetJoystickFromID(device->joysticks[0]);
317 } else {
318 return false;
319 }
320
321 while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) {
322#ifdef DEBUG_XBOX_PROTOCOL
323 HIDAPI_DumpPacket("Xbox 360 packet: size = %d", data, size);
324#endif
325 if (!joystick) {
326 continue;
327 }
328
329 if (data[0] == 0x00) {
330 HIDAPI_DriverXbox360_HandleStatePacket(joystick, ctx, data, size);
331 }
332 }
333
334 if (size < 0) {
335 // Read error, device is disconnected
336 HIDAPI_JoystickDisconnected(device, device->joysticks[0]);
337 }
338 return (size >= 0);
339}
340
341static void HIDAPI_DriverXbox360_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
342{
343 SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)device->context;
344
345 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED,
346 SDL_PlayerLEDHintChanged, ctx);
347
348 ctx->joystick = NULL;
349}
350
351static void HIDAPI_DriverXbox360_FreeDevice(SDL_HIDAPI_Device *device)
352{
353}
354
355SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXbox360 = {
356 SDL_HINT_JOYSTICK_HIDAPI_XBOX_360,
357 true,
358 HIDAPI_DriverXbox360_RegisterHints,
359 HIDAPI_DriverXbox360_UnregisterHints,
360 HIDAPI_DriverXbox360_IsEnabled,
361 HIDAPI_DriverXbox360_IsSupportedDevice,
362 HIDAPI_DriverXbox360_InitDevice,
363 HIDAPI_DriverXbox360_GetDevicePlayerIndex,
364 HIDAPI_DriverXbox360_SetDevicePlayerIndex,
365 HIDAPI_DriverXbox360_UpdateDevice,
366 HIDAPI_DriverXbox360_OpenJoystick,
367 HIDAPI_DriverXbox360_RumbleJoystick,
368 HIDAPI_DriverXbox360_RumbleJoystickTriggers,
369 HIDAPI_DriverXbox360_GetJoystickCapabilities,
370 HIDAPI_DriverXbox360_SetJoystickLED,
371 HIDAPI_DriverXbox360_SendJoystickEffect,
372 HIDAPI_DriverXbox360_SetJoystickSensorsEnabled,
373 HIDAPI_DriverXbox360_CloseJoystick,
374 HIDAPI_DriverXbox360_FreeDevice,
375};
376
377#endif // SDL_JOYSTICK_HIDAPI_XBOX360
378
379#endif // SDL_JOYSTICK_HIDAPI
380