diff --git a/CMakeLists.txt b/CMakeLists.txt index d609224783019..065d483518a54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1245,6 +1245,35 @@ if(SDL_JOYSTICK) file(GLOB JOYSTICK_VIRTUAL_SOURCES ${SDL2_SOURCE_DIR}/src/joystick/virtual/*.c) list(APPEND SOURCE_FILES ${JOYSTICK_VIRTUAL_SOURCES}) endif() + + # DSU (DualShock UDP) client support + option(SDL_DSU_JOYSTICK "Enable DSU client joystick support" ON) + if(SDL_DSU_JOYSTICK) + set(SDL_JOYSTICK_DSU 1) + file(GLOB JOYSTICK_DSU_SOURCES ${SDL2_SOURCE_DIR}/src/joystick/dsu/*.c) + list(APPEND SOURCE_FILES ${JOYSTICK_DSU_SOURCES}) + + # DSU requires network libraries on Unix-like systems + if(UNIX AND NOT APPLE AND NOT ANDROID) + if(NOT WIN32) + # Check if we need to link against socket libraries + include(CheckFunctionExists) + check_function_exists(socket HAVE_SOCKET_IN_LIBC) + if(NOT HAVE_SOCKET_IN_LIBC) + # Try to find socket in libsocket (Solaris) + check_library_exists(socket socket "" HAVE_LIBSOCKET) + if(HAVE_LIBSOCKET) + list(APPEND EXTRA_LIBS socket) + endif() + # Try to find inet_addr in libnsl (Solaris) + check_library_exists(nsl inet_addr "" HAVE_LIBNSL) + if(HAVE_LIBNSL) + list(APPEND EXTRA_LIBS nsl) + endif() + endif() + endif() + endif() + endif() endif() if(SDL_VIDEO) @@ -1984,6 +2013,10 @@ elseif(WINDOWS) # Libraries for Win32 native and MinGW if(NOT WINDOWS_STORE) list(APPEND EXTRA_LIBS kernel32 user32 gdi32 winmm imm32 ole32 oleaut32 version uuid advapi32 setupapi shell32) + # Add Winsock library for DSU support + if(SDL_DSU_JOYSTICK) + list(APPEND EXTRA_LIBS ws2_32) + endif() endif() if(WINDOWS_STORE) @@ -2468,6 +2501,11 @@ elseif(HAIKU) CheckPTHREAD() list(APPEND EXTRA_LIBS root be media game device textencoding) + + # Add network library for DSU support on Haiku + if(SDL_DSU_JOYSTICK) + list(APPEND EXTRA_LIBS network) + endif() elseif(RISCOS) if(SDL_MISC) diff --git a/include/SDL_config.h.cmake b/include/SDL_config.h.cmake index 64b8413c8cdc7..c810f122d9b0d 100644 --- a/include/SDL_config.h.cmake +++ b/include/SDL_config.h.cmake @@ -352,6 +352,7 @@ #cmakedefine SDL_JOYSTICK_RAWINPUT @SDL_JOYSTICK_RAWINPUT@ #cmakedefine SDL_JOYSTICK_EMSCRIPTEN @SDL_JOYSTICK_EMSCRIPTEN@ #cmakedefine SDL_JOYSTICK_VIRTUAL @SDL_JOYSTICK_VIRTUAL@ +#cmakedefine SDL_JOYSTICK_DSU @SDL_JOYSTICK_DSU@ #cmakedefine SDL_JOYSTICK_VITA @SDL_JOYSTICK_VITA@ #cmakedefine SDL_JOYSTICK_PSP @SDL_JOYSTICK_PSP@ #cmakedefine SDL_JOYSTICK_PS2 @SDL_JOYSTICK_PS2@ diff --git a/include/SDL_config_windows.h b/include/SDL_config_windows.h index 77d2d74fd1a91..c15d7db89a7ea 100644 --- a/include/SDL_config_windows.h +++ b/include/SDL_config_windows.h @@ -261,6 +261,7 @@ typedef unsigned int uintptr_t; #define SDL_JOYSTICK_RAWINPUT 1 #endif #define SDL_JOYSTICK_VIRTUAL 1 +#define SDL_JOYSTICK_DSU 1 #ifdef HAVE_WINDOWS_GAMING_INPUT_H #define SDL_JOYSTICK_WGI 1 #endif diff --git a/include/SDL_hints.h b/include/SDL_hints.h index 51324451981f8..e12dd761f01b3 100644 --- a/include/SDL_hints.h +++ b/include/SDL_hints.h @@ -1234,6 +1234,40 @@ extern "C" { */ #define SDL_HINT_JOYSTICK_HIDAPI_XBOX_ONE_HOME_LED "SDL_JOYSTICK_HIDAPI_XBOX_ONE_HOME_LED" +/** + * A variable controlling whether the DSU (DualShock UDP) joystick driver should be used. + * + * This variable can be set to the following values: + * + * - "0": DSU driver is disabled + * - "1": DSU driver is enabled (default) + * + * The DSU driver allows SDL to connect to DSU servers (DS4Windows, BetterJoy, etc.) + * to receive controller data over UDP, including motion sensors and touchpad data. + */ +#define SDL_HINT_JOYSTICK_DSU "SDL_JOYSTICK_DSU" + +/** + * A variable controlling the DSU server address. + * + * The default value is "127.0.0.1" + */ +#define SDL_HINT_DSU_SERVER "SDL_DSU_SERVER" + +/** + * A variable controlling the DSU server port. + * + * The default value is "26760" + */ +#define SDL_HINT_DSU_SERVER_PORT "SDL_DSU_SERVER_PORT" + +/** + * A variable controlling the DSU client port. + * + * The default value is "0" (auto-select) + */ +#define SDL_HINT_DSU_CLIENT_PORT "SDL_DSU_CLIENT_PORT" + /** * A variable controlling whether IOKit should be used for controller * handling. diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index e968288599582..2784434de2690 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -51,6 +51,10 @@ #include "./virtual/SDL_virtualjoystick_c.h" #endif +#ifdef SDL_JOYSTICK_DSU +#include "./dsu/SDL_dsujoystick_c.h" +#endif + static SDL_JoystickDriver *SDL_joystick_drivers[] = { #ifdef SDL_JOYSTICK_HIDAPI /* Before WINDOWS_ driver, as WINDOWS wants to check if this driver is handling things */ &SDL_HIDAPI_JoystickDriver, @@ -100,6 +104,9 @@ static SDL_JoystickDriver *SDL_joystick_drivers[] = { #ifdef SDL_JOYSTICK_VIRTUAL &SDL_VIRTUAL_JoystickDriver, #endif +#ifdef SDL_JOYSTICK_DSU + &SDL_DSU_JoystickDriver, +#endif #ifdef SDL_JOYSTICK_VITA &SDL_VITA_JoystickDriver, #endif diff --git a/src/joystick/dsu/SDL_dsujoystick.c b/src/joystick/dsu/SDL_dsujoystick.c new file mode 100644 index 0000000000000..27aed4b9827a0 --- /dev/null +++ b/src/joystick/dsu/SDL_dsujoystick.c @@ -0,0 +1,465 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +#ifdef SDL_JOYSTICK_DSU + +/* DSU (DualShock UDP) client joystick driver - Main Implementation */ + +#include "SDL_joystick.h" +#include "SDL_endian.h" +#include "SDL_timer.h" +#include "SDL_hints.h" +#include "SDL_thread.h" +#include "SDL_atomic.h" +#include "../SDL_sysjoystick.h" +#include "../SDL_joystick_c.h" +#include "SDL_dsujoystick_c.h" + +/* Platform-specific socket includes */ +#ifdef _WIN32 + #include + #include + #pragma comment(lib, "ws2_32.lib") + typedef int socklen_t; + #define DSU_SOCKET_ERROR SOCKET_ERROR + #define DSU_INVALID_SOCKET INVALID_SOCKET +#else + #include + #include + #include + #include + #include + #include + #define DSU_SOCKET_ERROR -1 + #define DSU_INVALID_SOCKET -1 + #define closesocket close +#endif + +#include + +/* Constants */ +#define SERVER_REREGISTER_INTERVAL 1000 /* ms */ +#define SERVER_TIMEOUT_INTERVAL 2000 /* ms */ +#define GRAVITY_ACCELERATION 9.80665f /* m/s² */ + +/* Global DSU context - defined in SDL_dsujoystick_driver.c */ +extern DSU_Context *g_dsu_context; + +/* Forward declarations */ +void DSU_RequestControllerInfo(DSU_Context *ctx, Uint8 slot); +void DSU_RequestControllerData(DSU_Context *ctx, Uint8 slot); + +/* Socket helpers implementation */ +int DSU_InitSockets(void) +{ +#ifdef _WIN32 + WSADATA wsaData; + return WSAStartup(MAKEWORD(2, 2), &wsaData); +#else + return 0; +#endif +} + +void DSU_QuitSockets(void) +{ +#ifdef _WIN32 + WSACleanup(); +#endif +} + +int DSU_CreateSocket(Uint16 port) +{ + int sock; + struct sockaddr_in addr; + int reuse = 1; +#ifdef _WIN32 + u_long mode = 1; +#else + int flags; +#endif + + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock == DSU_INVALID_SOCKET) { + return DSU_INVALID_SOCKET; + } + + /* Allow address reuse */ + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)); + + /* Set socket to non-blocking */ +#ifdef _WIN32 + ioctlsocket(sock, FIONBIO, &mode); +#else + flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + /* Bind to client port if specified */ + if (port != 0) { + SDL_memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = INADDR_ANY; + + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + /* Bind failure is not fatal, continue anyway */ + } + } + + return sock; +} + +void DSU_CloseSocket(int socket) +{ + if (socket != DSU_INVALID_SOCKET) { + closesocket(socket); + } +} + +/* Complete CRC32 table */ +static const Uint32 crc32_table[256] = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +}; + +Uint32 DSU_CalculateCRC32(const Uint8 *data, size_t length) +{ + Uint32 crc = 0xFFFFFFFF; + size_t i; + + for (i = 0; i < length; i++) { + crc = crc32_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8); + } + + return crc ^ 0xFFFFFFFF; +} + +/* Send a packet to the DSU server */ +static int DSU_SendPacket(DSU_Context *ctx, void *packet, size_t size) +{ + struct sockaddr_in server; + DSU_Header *header = (DSU_Header *)packet; + + /* Set header fields */ + SDL_memcpy(header->magic, DSU_MAGIC_CLIENT, 4); + header->version = SDL_SwapLE16(DSU_PROTOCOL_VERSION); + header->length = SDL_SwapLE16((Uint16)(size - sizeof(DSU_Header))); + header->client_id = SDL_SwapLE32(ctx->client_id); + + /* Calculate CRC32 */ + header->crc32 = 0; + header->crc32 = SDL_SwapLE32(DSU_CalculateCRC32((Uint8*)packet, size)); + + /* Send to server */ + SDL_memset(&server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_port = htons(ctx->server_port); + server.sin_addr.s_addr = inet_addr(ctx->server_address); + + return sendto(ctx->socket, (const char*)packet, (int)size, 0, + (struct sockaddr *)&server, sizeof(server)); +} + +/* Request controller information */ +void DSU_RequestControllerInfo(DSU_Context *ctx, Uint8 slot) +{ + DSU_PortRequest request; + + SDL_memset(&request, 0, sizeof(request)); + request.header.message_type = SDL_SwapLE32(DSU_MSG_PORTS_INFO); + request.flags = 0; + request.slot_id = slot; /* 0xFF for all slots */ + /* MAC is zeros for all controllers */ + + DSU_SendPacket(ctx, &request, sizeof(request)); +} + +/* Request controller data */ +void DSU_RequestControllerData(DSU_Context *ctx, Uint8 slot) +{ + DSU_PortRequest request; + + SDL_memset(&request, 0, sizeof(request)); + request.header.message_type = SDL_SwapLE32(DSU_MSG_DATA); + request.flags = 0; /* Subscribe to data */ + request.slot_id = slot; + + DSU_SendPacket(ctx, &request, sizeof(request)); +} + +/* Process incoming controller data */ +void DSU_ProcessControllerData(DSU_Context *ctx, DSU_ControllerData *data) +{ + DSU_ControllerSlot *slot; + int slot_id; + SDL_bool was_connected; + + /* Get slot ID */ + slot_id = data->info.slot; + if (slot_id >= DSU_MAX_SLOTS) { + return; + } + + SDL_LockMutex(ctx->slots_mutex); + slot = &ctx->slots[slot_id]; + + /* Update connection state */ + was_connected = slot->connected; + slot->connected = (data->info.slot_state == DSU_STATE_CONNECTED); + + if (slot->connected) { + /* Update controller info */ + SDL_memcpy(slot->mac, data->info.mac, 6); + slot->battery = data->info.battery; + slot->model = data->info.device_model; + slot->connection = data->info.connection_type; + slot->slot_id = slot_id; + + /* Generate name */ + SDL_snprintf(slot->name, sizeof(slot->name), "DSUClient/%d", slot_id); + + /* Update button states */ + slot->buttons = 0; + + /* Map DSU buttons to SDL buttons */ + if (data->button_states_2 & DSU_BUTTON_CROSS) slot->buttons |= (1 << 0); + if (data->button_states_2 & DSU_BUTTON_CIRCLE) slot->buttons |= (1 << 1); + if (data->button_states_2 & DSU_BUTTON_SQUARE) slot->buttons |= (1 << 2); + if (data->button_states_2 & DSU_BUTTON_TRIANGLE) slot->buttons |= (1 << 3); + if (data->button_states_2 & DSU_BUTTON_L1) slot->buttons |= (1 << 4); + if (data->button_states_2 & DSU_BUTTON_R1) slot->buttons |= (1 << 5); + if (data->button_states_1 & DSU_BUTTON_SHARE) slot->buttons |= (1 << 6); + if (data->button_states_1 & DSU_BUTTON_OPTIONS) slot->buttons |= (1 << 7); + if (data->button_states_1 & DSU_BUTTON_L3) slot->buttons |= (1 << 8); + if (data->button_states_1 & DSU_BUTTON_R3) slot->buttons |= (1 << 9); + if (data->button_ps) slot->buttons |= (1 << 10); + if (data->button_touch) slot->buttons |= (1 << 11); + + /* Update analog sticks */ + slot->axes[0] = ((Sint16)data->left_stick_x - 128) * 257; + slot->axes[1] = ((Sint16)data->left_stick_y - 128) * -257; + slot->axes[2] = ((Sint16)data->right_stick_x - 128) * 257; + slot->axes[3] = ((Sint16)data->right_stick_y - 128) * -257; + + /* Triggers */ + slot->axes[4] = ((Sint16)data->analog_trigger_l2) * 128; + slot->axes[5] = ((Sint16)data->analog_trigger_r2) * 128; + + /* D-Pad as hat */ + slot->hat = SDL_HAT_CENTERED; + if (data->button_states_1 & DSU_BUTTON_DPAD_UP) slot->hat |= SDL_HAT_UP; + if (data->button_states_1 & DSU_BUTTON_DPAD_DOWN) slot->hat |= SDL_HAT_DOWN; + if (data->button_states_1 & DSU_BUTTON_DPAD_LEFT) slot->hat |= SDL_HAT_LEFT; + if (data->button_states_1 & DSU_BUTTON_DPAD_RIGHT) slot->hat |= SDL_HAT_RIGHT; + + /* Motion data */ + if (data->motion_timestamp != 0) { + slot->has_gyro = SDL_TRUE; + slot->has_accel = SDL_TRUE; + slot->motion_timestamp = SDL_SwapLE64(data->motion_timestamp); + + /* Convert gyro from deg/s to rad/s (handling endianness) */ + slot->gyro[0] = SDL_SwapFloatLE(data->gyro_pitch) * (M_PI / 180.0f); + slot->gyro[1] = SDL_SwapFloatLE(data->gyro_yaw) * (M_PI / 180.0f); + slot->gyro[2] = SDL_SwapFloatLE(data->gyro_roll) * (M_PI / 180.0f); + + /* Convert accel from g to m/s² (handling endianness) */ + slot->accel[0] = SDL_SwapFloatLE(data->accel_x) * GRAVITY_ACCELERATION; + slot->accel[1] = SDL_SwapFloatLE(data->accel_y) * GRAVITY_ACCELERATION; + slot->accel[2] = SDL_SwapFloatLE(data->accel_z) * GRAVITY_ACCELERATION; + } + + /* Update last packet time */ + slot->last_packet_time = SDL_GetTicks64(); + + /* Touch data */ + slot->has_touchpad = SDL_TRUE; + slot->touch1_active = data->touch1_active; + slot->touch2_active = data->touch2_active; + slot->touch1_id = data->touch1_id; + slot->touch2_id = data->touch2_id; + slot->touch1_x = SDL_SwapLE16(data->touch1_x); + slot->touch1_y = SDL_SwapLE16(data->touch1_y); + slot->touch2_x = SDL_SwapLE16(data->touch2_x); + slot->touch2_y = SDL_SwapLE16(data->touch2_y); + + /* Update timing */ + slot->last_packet_time = SDL_GetTicks64(); + slot->packet_number = SDL_SwapLE32(data->packet_number); + } + + SDL_UnlockMutex(ctx->slots_mutex); + + /* Handle connection state changes */ + if (!was_connected && slot->connected) { + Uint16 vendor; + Uint16 product; + + /* New controller connected */ + slot->instance_id = SDL_GetNextJoystickInstanceID(); + + /* Update controller ID for SDL */ + vendor = 0x054C; /* Sony vendor ID */ + product = 0x05C4; /* DS4 product ID by default */ + if (slot->model == DSU_MODEL_FULL_GYRO) { + product = 0x09CC; + } + slot->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, 0, + NULL, slot->name, 'd', 0); + + /* Subscribe to controller data updates */ + DSU_RequestControllerData(ctx, slot_id); + + SDL_PrivateJoystickAdded(slot->instance_id); + } else if (was_connected && !slot->connected) { + /* Controller disconnected */ + SDL_PrivateJoystickRemoved(slot->instance_id); + slot->instance_id = 0; + } +} + +/* Receiver thread implementation */ +int DSU_ReceiverThread(void *data) +{ + DSU_Context *ctx = (DSU_Context *)data; + Uint8 buffer[1024]; + struct sockaddr_in sender; + socklen_t sender_len = sizeof(sender); + DSU_Header *header; + int received; + + SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); + + while (SDL_AtomicGet(&ctx->running)) { + received = recvfrom(ctx->socket, (char*)buffer, sizeof(buffer), 0, + (struct sockaddr *)&sender, &sender_len); + + if (received > (int)sizeof(DSU_Header)) { + header = (DSU_Header *)buffer; + + /* Validate magic */ + if (SDL_memcmp(header->magic, DSU_MAGIC_SERVER, 4) == 0) { + Uint32 received_crc; + Uint32 calculated_crc; + + /* Validate CRC32 */ + received_crc = SDL_SwapLE32(header->crc32); + header->crc32 = 0; + calculated_crc = DSU_CalculateCRC32(buffer, received); + + if (received_crc == calculated_crc) { + Uint32 msg_type = SDL_SwapLE32(header->message_type); + + switch (msg_type) { + case DSU_MSG_VERSION: + /* Version info received */ + break; + + case DSU_MSG_PORTS_INFO: { + /* Port info response - tells us which slots have controllers */ + if (received >= (int)(sizeof(DSU_Header) + 4)) { + Uint8 *data_ptr; + Uint8 slot_id; + Uint8 slot_state; + + /* Parse port info */ + data_ptr = buffer + sizeof(DSU_Header); + slot_id = data_ptr[0]; + slot_state = data_ptr[1]; + /* Skip device_model = data_ptr[2] and connection_type = data_ptr[3] - not used */ + + /* If controller is connected in this slot, request data */ + if (slot_state == DSU_STATE_CONNECTED && slot_id < DSU_MAX_SLOTS) { + DSU_RequestControllerData(ctx, slot_id); + } + } + break; + } + + case DSU_MSG_DATA: + /* Controller data */ + if (received >= (int)sizeof(DSU_ControllerData)) { + DSU_ProcessControllerData(ctx, (DSU_ControllerData *)buffer); + } + break; + + default: + /* Unknown message type */ + break; + } + } + } + } else if (received < 0) { + /* Check for real errors (not just EWOULDBLOCK) */ +#ifdef _WIN32 + int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK && error != WSAEINTR && error != WSAECONNRESET) { + SDL_Delay(100); /* Back off on errors */ + } +#else + if (errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR) { + SDL_Delay(100); + } +#endif + } + + /* Small delay to prevent CPU spinning */ + SDL_Delay(1); + } + + return 0; +} + +#endif /* SDL_JOYSTICK_DSU */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/joystick/dsu/SDL_dsujoystick_c.h b/src/joystick/dsu/SDL_dsujoystick_c.h new file mode 100644 index 0000000000000..8042a3bdb3767 --- /dev/null +++ b/src/joystick/dsu/SDL_dsujoystick_c.h @@ -0,0 +1,108 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +#ifndef SDL_dsujoystick_c_h_ +#define SDL_dsujoystick_c_h_ + +#include "../../SDL_internal.h" + +#ifdef SDL_JOYSTICK_DSU + +#include "../SDL_sysjoystick.h" +#include "SDL_dsuprotocol.h" + +/* DSU Joystick driver */ +extern SDL_JoystickDriver SDL_DSU_JoystickDriver; + +/* Internal structures */ +typedef struct DSU_ControllerSlot { + SDL_bool connected; + SDL_JoystickID instance_id; + SDL_JoystickGUID guid; + char name[128]; + + /* DSU protocol data */ + Uint8 slot_id; + Uint8 mac[6]; + Uint8 battery; + DSU_DeviceModel model; + DSU_ConnectionType connection; + + /* Controller state */ + Uint16 buttons; + Sint16 axes[6]; /* LX, LY, RX, RY, L2, R2 */ + Uint8 hat; + + /* Motion data */ + SDL_bool has_gyro; + SDL_bool has_accel; + float gyro[3]; /* Pitch, Yaw, Roll in rad/s */ + float accel[3]; /* X, Y, Z in m/s² */ + Uint64 motion_timestamp; + + /* Touch data */ + SDL_bool has_touchpad; + SDL_bool touch1_active; + SDL_bool touch2_active; + Uint8 touch1_id; + Uint8 touch2_id; + Uint16 touch1_x, touch1_y; + Uint16 touch2_x, touch2_y; + + /* Timing */ + Uint64 last_packet_time; + Uint32 packet_number; +} DSU_ControllerSlot; + +typedef struct DSU_Context { + /* Network */ + int socket; + SDL_Thread *receiver_thread; + SDL_atomic_t running; + + /* Server configuration */ + char server_address[256]; + Uint16 server_port; + Uint16 client_port; + Uint32 client_id; + + /* Controller slots (4 max per DSU protocol) */ + DSU_ControllerSlot slots[DSU_MAX_SLOTS]; + SDL_mutex *slots_mutex; + + /* Timing for periodic updates */ + Uint64 last_request_time; +} DSU_Context; + +/* Socket helpers */ +int DSU_InitSockets(void); +void DSU_QuitSockets(void); +int DSU_CreateSocket(Uint16 port); +void DSU_CloseSocket(int socket); + +/* CRC32 calculation */ +Uint32 DSU_CalculateCRC32(const Uint8 *data, size_t length); + +#endif /* SDL_JOYSTICK_DSU */ + +#endif /* SDL_dsujoystick_c_h_ */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/joystick/dsu/SDL_dsujoystick_driver.c b/src/joystick/dsu/SDL_dsujoystick_driver.c new file mode 100644 index 0000000000000..9de9cacaa4745 --- /dev/null +++ b/src/joystick/dsu/SDL_dsujoystick_driver.c @@ -0,0 +1,615 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +#ifdef SDL_JOYSTICK_DSU + +/* DSU Joystick Driver - SDL Driver Interface Implementation */ + +#include "SDL_joystick.h" +#include "SDL_endian.h" +#include "SDL_timer.h" +#include "SDL_hints.h" +#include "SDL_thread.h" +#include "SDL_atomic.h" +#include "SDL_mutex.h" +#include "SDL_events.h" +#include "../SDL_sysjoystick.h" +#include "../SDL_joystick_c.h" +#include "../../thread/SDL_systhread.h" +#include "SDL_dsujoystick_c.h" + +/* Platform-specific socket includes for rumble support */ +#ifdef _WIN32 + #include + #include +#else + #include + #include + #include +#endif + +/* Global DSU context (defined in SDL_dsujoystick.c) */ +DSU_Context *g_dsu_context = NULL; + +/* Forward declarations */ +extern int DSU_ReceiverThread(void *data); +extern void DSU_RequestControllerInfo(DSU_Context *ctx, Uint8 slot); +extern void DSU_RequestControllerData(DSU_Context *ctx, Uint8 slot); + +/* Driver functions */ +static int DSU_JoystickInit(void) +{ + const char *enabled; + const char *server; + const char *server_port; + const char *client_port; + + /* Check if DSU is enabled */ + enabled = SDL_GetHint(SDL_HINT_JOYSTICK_DSU); + if (enabled && SDL_atoi(enabled) == 0) { + return 0; /* DSU disabled */ + } + + /* Allocate context */ + g_dsu_context = (DSU_Context *)SDL_calloc(1, sizeof(DSU_Context)); + if (!g_dsu_context) { + return SDL_OutOfMemory(); + } + + /* Get configuration from hints with fallbacks */ + server = SDL_GetHint(SDL_HINT_DSU_SERVER); + if (!server || !*server) { + server = DSU_SERVER_ADDRESS_DEFAULT; + } + SDL_strlcpy(g_dsu_context->server_address, server, + sizeof(g_dsu_context->server_address)); + + server_port = SDL_GetHint(SDL_HINT_DSU_SERVER_PORT); + if (server_port && *server_port) { + g_dsu_context->server_port = SDL_atoi(server_port); + } else { + g_dsu_context->server_port = DSU_SERVER_PORT_DEFAULT; + } + + client_port = SDL_GetHint(SDL_HINT_DSU_CLIENT_PORT); + if (client_port && *client_port) { + g_dsu_context->client_port = SDL_atoi(client_port); + } else { + g_dsu_context->client_port = DSU_CLIENT_PORT_DEFAULT; + } + + g_dsu_context->client_id = SDL_GetTicks(); + + /* Initialize sockets */ + if (DSU_InitSockets() != 0) { + SDL_free(g_dsu_context); + g_dsu_context = NULL; + return -1; + } + + /* Create UDP socket */ + g_dsu_context->socket = DSU_CreateSocket(g_dsu_context->client_port); + if (g_dsu_context->socket == -1) { + DSU_QuitSockets(); + SDL_free(g_dsu_context); + g_dsu_context = NULL; + return -1; + } + + /* Create mutex */ + g_dsu_context->slots_mutex = SDL_CreateMutex(); + if (!g_dsu_context->slots_mutex) { + DSU_CloseSocket(g_dsu_context->socket); + DSU_QuitSockets(); + SDL_free(g_dsu_context); + g_dsu_context = NULL; + return SDL_OutOfMemory(); + } + + /* Start receiver thread */ + SDL_AtomicSet(&g_dsu_context->running, 1); + g_dsu_context->receiver_thread = SDL_CreateThreadInternal( + DSU_ReceiverThread, "DSU_Receiver", 0, g_dsu_context); + if (!g_dsu_context->receiver_thread) { + SDL_DestroyMutex(g_dsu_context->slots_mutex); + DSU_CloseSocket(g_dsu_context->socket); + DSU_QuitSockets(); + SDL_free(g_dsu_context); + g_dsu_context = NULL; + return SDL_SetError("Failed to create DSU receiver thread"); + } + + /* Request controller info from all slots */ + DSU_RequestControllerInfo(g_dsu_context, 0xFF); + + return 0; +} + +static int DSU_JoystickGetCount(void) +{ + int count = 0; + int i; + + if (!g_dsu_context) { + return 0; + } + + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + count++; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); + + return count; +} + +static void DSU_JoystickDetect(void) +{ + Uint64 now; + int i; + + if (!g_dsu_context) { + return; + } + + /* Periodically request controller info and re-subscribe to data */ + now = SDL_GetTicks64(); + if (now - g_dsu_context->last_request_time >= 500) { /* Request more frequently */ + DSU_RequestControllerInfo(g_dsu_context, 0xFF); + + /* Re-subscribe to data for connected controllers */ + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + DSU_RequestControllerData(g_dsu_context, i); + } + } + + g_dsu_context->last_request_time = now; + } + + /* Check for timeouts */ + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected && + now - g_dsu_context->slots[i].last_packet_time > 5000) { /* Increased timeout */ + /* Controller timed out */ + g_dsu_context->slots[i].connected = SDL_FALSE; + SDL_PrivateJoystickRemoved(g_dsu_context->slots[i].instance_id); + g_dsu_context->slots[i].instance_id = 0; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); +} + +static const char *DSU_JoystickGetDeviceName(int device_index) +{ + int i, count = 0; + + if (!g_dsu_context) { + return NULL; + } + + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + if (count == device_index) { + SDL_UnlockMutex(g_dsu_context->slots_mutex); + return g_dsu_context->slots[i].name; + } + count++; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); + + return NULL; +} + +static const char *DSU_JoystickGetDevicePath(int device_index) +{ + return NULL; /* No path for network devices */ +} + +static int DSU_JoystickGetDevicePlayerIndex(int device_index) +{ + int i, count = 0; + + if (!g_dsu_context) { + return -1; + } + + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + if (count == device_index) { + SDL_UnlockMutex(g_dsu_context->slots_mutex); + return i; /* Return slot ID as player index */ + } + count++; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); + + return -1; +} + +static void DSU_JoystickSetDevicePlayerIndex(int device_index, int player_index) +{ + /* DSU controllers have fixed slots, can't change */ +} + +static SDL_JoystickGUID DSU_JoystickGetDeviceGUID(int device_index) +{ + SDL_JoystickGUID guid; + int i, count = 0; + + SDL_zero(guid); + + if (!g_dsu_context) { + return guid; + } + + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + if (count == device_index) { + guid = g_dsu_context->slots[i].guid; + SDL_UnlockMutex(g_dsu_context->slots_mutex); + return guid; + } + count++; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); + + return guid; +} + +static SDL_JoystickID DSU_JoystickGetDeviceInstanceID(int device_index) +{ + int i, count = 0; + + if (!g_dsu_context) { + return -1; + } + + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + if (count == device_index) { + SDL_JoystickID id = g_dsu_context->slots[i].instance_id; + SDL_UnlockMutex(g_dsu_context->slots_mutex); + return id; + } + count++; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); + + return -1; +} + +static int DSU_JoystickOpen(SDL_Joystick *joystick, int device_index) +{ + DSU_ControllerSlot *slot = NULL; + int i, count = 0; + + if (!g_dsu_context) { + return SDL_SetError("DSU not initialized"); + } + + /* Find the slot for this device */ + SDL_LockMutex(g_dsu_context->slots_mutex); + for (i = 0; i < DSU_MAX_SLOTS; i++) { + if (g_dsu_context->slots[i].connected) { + if (count == device_index) { + slot = &g_dsu_context->slots[i]; + break; + } + count++; + } + } + SDL_UnlockMutex(g_dsu_context->slots_mutex); + + if (!slot) { + return SDL_SetError("DSU device not found"); + } + + joystick->instance_id = slot->instance_id; + joystick->hwdata = (struct joystick_hwdata *)slot; + joystick->nbuttons = 12; /* Standard PS4 buttons */ + joystick->naxes = 6; /* LX, LY, RX, RY, L2, R2 */ + joystick->nhats = 1; /* D-Pad */ + + /* Set up touchpad if available */ + if (slot->has_touchpad) { + joystick->ntouchpads = 1; + joystick->touchpads = (SDL_JoystickTouchpadInfo *)SDL_calloc(1, sizeof(SDL_JoystickTouchpadInfo)); + if (joystick->touchpads) { + joystick->touchpads[0].nfingers = 2; /* DSU supports 2 fingers */ + } else { + joystick->ntouchpads = 0; /* Failed to allocate, disable touchpad */ + } + } + + /* Register sensors if available */ + if (slot->has_gyro || (slot->model == DSU_MODEL_FULL_GYRO) || (slot->model == DSU_MODEL_PARTIAL_GYRO)) { + /* DSU reports gyro at varying rates, but typically 250-1000Hz for DS4/DS5 */ + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 250.0f); + slot->has_gyro = SDL_TRUE; + } + if (slot->has_accel || (slot->model == DSU_MODEL_FULL_GYRO)) { + /* DSU reports accelerometer at same rate as gyro */ + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 250.0f); + slot->has_accel = SDL_TRUE; + } + + return 0; +} + +static int DSU_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) +{ + DSU_ControllerSlot *slot = (DSU_ControllerSlot *)joystick->hwdata; + DSU_RumblePacket packet; + struct sockaddr_in server; + + if (!g_dsu_context || !slot || !slot->connected) { + return SDL_SetError("DSU controller not available"); + } + + /* Build rumble packet */ + SDL_memset(&packet, 0, sizeof(packet)); + SDL_memcpy(packet.header.magic, DSU_MAGIC_CLIENT, 4); + packet.header.version = SDL_SwapLE16(DSU_PROTOCOL_VERSION); + packet.header.length = SDL_SwapLE16((Uint16)(sizeof(packet) - sizeof(DSU_Header))); + packet.header.client_id = SDL_SwapLE32(g_dsu_context->client_id); + packet.header.message_type = SDL_SwapLE32(DSU_MSG_RUMBLE); + + /* Set rumble values */ + packet.slot = slot->slot_id; + packet.motor_left = (Uint8)(low_frequency_rumble >> 8); /* Convert from 16-bit to 8-bit */ + packet.motor_right = (Uint8)(high_frequency_rumble >> 8); + + /* Calculate CRC32 */ + packet.header.crc32 = 0; + packet.header.crc32 = SDL_SwapLE32(DSU_CalculateCRC32((Uint8*)&packet, sizeof(packet))); + + /* Send to server */ + SDL_memset(&server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_port = htons(g_dsu_context->server_port); + server.sin_addr.s_addr = inet_addr(g_dsu_context->server_address); + + if (sendto(g_dsu_context->socket, (const char*)&packet, sizeof(packet), 0, + (struct sockaddr *)&server, sizeof(server)) < 0) { + return SDL_SetError("Failed to send rumble packet"); + } + + return 0; +} + +static int DSU_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) +{ + return SDL_Unsupported(); +} + +static Uint32 DSU_JoystickGetCapabilities(SDL_Joystick *joystick) +{ + Uint32 caps = 0; + + /* DSU protocol supports rumble through unofficial extensions */ + caps |= SDL_JOYCAP_RUMBLE; + + /* Note: SDL doesn't have a capability flag for motion sensors yet, + * but they're supported through SDL_JoystickGetSensor* APIs */ + + return caps; +} + +static int DSU_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) +{ + return SDL_Unsupported(); +} + +static int DSU_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size) +{ + return SDL_Unsupported(); +} + +static int DSU_JoystickSetSensorsEnabled(SDL_Joystick *joystick, SDL_bool enabled) +{ + DSU_ControllerSlot *slot = (DSU_ControllerSlot *)joystick->hwdata; + + /* Sensors are always enabled if available */ + return (slot->has_gyro || slot->has_accel) ? 0 : SDL_Unsupported(); +} + +static void DSU_JoystickUpdate(SDL_Joystick *joystick) +{ + DSU_ControllerSlot *slot = (DSU_ControllerSlot *)joystick->hwdata; + int i; + + if (!slot || !slot->connected) { + return; + } + + SDL_LockMutex(g_dsu_context->slots_mutex); + + /* Update buttons */ + for (i = 0; i < 12; i++) { + SDL_PrivateJoystickButton(joystick, i, (slot->buttons & (1 << i)) ? 1 : 0); + } + + /* Update axes */ + for (i = 0; i < 6; i++) { + SDL_PrivateJoystickAxis(joystick, i, slot->axes[i]); + } + + /* Update hat */ + SDL_PrivateJoystickHat(joystick, 0, slot->hat); + + /* Update touchpad if available */ + if (slot->has_touchpad && joystick->ntouchpads > 0) { + /* DS4/DS5 touchpad resolution is typically 1920x943 */ + const float TOUCHPAD_WIDTH = 1920.0f; + const float TOUCHPAD_HEIGHT = 943.0f; + + /* First touch point */ + Uint8 touchpad_state = slot->touch1_active ? SDL_PRESSED : SDL_RELEASED; + float touchpad_x = (float)slot->touch1_x / TOUCHPAD_WIDTH; + float touchpad_y = (float)slot->touch1_y / TOUCHPAD_HEIGHT; + + /* Clamp to valid range */ + if (touchpad_x < 0.0f) touchpad_x = 0.0f; + if (touchpad_x > 1.0f) touchpad_x = 1.0f; + if (touchpad_y < 0.0f) touchpad_y = 0.0f; + if (touchpad_y > 1.0f) touchpad_y = 1.0f; + + SDL_PrivateJoystickTouchpad(joystick, 0, 0, touchpad_state, + touchpad_x, touchpad_y, + touchpad_state ? 1.0f : 0.0f); + + /* Second touch point */ + touchpad_state = slot->touch2_active ? SDL_PRESSED : SDL_RELEASED; + touchpad_x = (float)slot->touch2_x / TOUCHPAD_WIDTH; + touchpad_y = (float)slot->touch2_y / TOUCHPAD_HEIGHT; + + /* Clamp to valid range */ + if (touchpad_x < 0.0f) touchpad_x = 0.0f; + if (touchpad_x > 1.0f) touchpad_x = 1.0f; + if (touchpad_y < 0.0f) touchpad_y = 0.0f; + if (touchpad_y > 1.0f) touchpad_y = 1.0f; + + SDL_PrivateJoystickTouchpad(joystick, 0, 1, touchpad_state, + touchpad_x, touchpad_y, + touchpad_state ? 1.0f : 0.0f); + } + + /* Update battery level */ + switch (slot->battery) { + case DSU_BATTERY_DYING: + case DSU_BATTERY_LOW: + SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_LOW); + break; + case DSU_BATTERY_MEDIUM: + SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_MEDIUM); + break; + case DSU_BATTERY_HIGH: + case DSU_BATTERY_FULL: + SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL); + break; + case DSU_BATTERY_CHARGING: + case DSU_BATTERY_CHARGED: + SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_WIRED); + break; + default: + SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_UNKNOWN); + break; + } + + /* Update sensors if available */ + if (slot->has_gyro) { + SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_GYRO, + slot->motion_timestamp, slot->gyro, 3); + } + if (slot->has_accel) { + SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_ACCEL, + slot->motion_timestamp, slot->accel, 3); + } + + SDL_UnlockMutex(g_dsu_context->slots_mutex); +} + +static void DSU_JoystickClose(SDL_Joystick *joystick) +{ + /* Free touchpad info if allocated */ + if (joystick->touchpads) { + SDL_free(joystick->touchpads); + joystick->touchpads = NULL; + joystick->ntouchpads = 0; + } + + joystick->hwdata = NULL; +} + +static void DSU_JoystickQuit(void) +{ + if (!g_dsu_context) { + return; + } + + /* Stop receiver thread */ + SDL_AtomicSet(&g_dsu_context->running, 0); + if (g_dsu_context->receiver_thread) { + SDL_WaitThread(g_dsu_context->receiver_thread, NULL); + } + + /* Close socket */ + DSU_CloseSocket(g_dsu_context->socket); + DSU_QuitSockets(); + + /* Clean up mutex */ + if (g_dsu_context->slots_mutex) { + SDL_DestroyMutex(g_dsu_context->slots_mutex); + } + + /* Free context */ + SDL_free(g_dsu_context); + g_dsu_context = NULL; +} + +static SDL_bool DSU_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) +{ + /* DSU controllers map well to standard gamepad layout */ + return SDL_FALSE; /* Use default mapping */ +} + +/* Export the driver */ +SDL_JoystickDriver SDL_DSU_JoystickDriver = { + DSU_JoystickInit, + DSU_JoystickGetCount, + DSU_JoystickDetect, + DSU_JoystickGetDeviceName, + DSU_JoystickGetDevicePath, + NULL, /* GetDeviceSteamVirtualGamepadSlot */ + DSU_JoystickGetDevicePlayerIndex, + DSU_JoystickSetDevicePlayerIndex, + DSU_JoystickGetDeviceGUID, + DSU_JoystickGetDeviceInstanceID, + DSU_JoystickOpen, + DSU_JoystickRumble, + DSU_JoystickRumbleTriggers, + DSU_JoystickGetCapabilities, + DSU_JoystickSetLED, + DSU_JoystickSendEffect, + DSU_JoystickSetSensorsEnabled, + DSU_JoystickUpdate, + DSU_JoystickClose, + DSU_JoystickQuit, + DSU_JoystickGetGamepadMapping +}; + +#endif /* SDL_JOYSTICK_DSU */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/joystick/dsu/SDL_dsuprotocol.h b/src/joystick/dsu/SDL_dsuprotocol.h new file mode 100644 index 0000000000000..6e8a687f42bd8 --- /dev/null +++ b/src/joystick/dsu/SDL_dsuprotocol.h @@ -0,0 +1,201 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +#ifndef SDL_dsuprotocol_h_ +#define SDL_dsuprotocol_h_ + +#include "../../SDL_internal.h" + +/* DSU (DualShock UDP) Protocol Constants - Based on CemuHook */ + +#define DSU_PROTOCOL_VERSION 1001 +#define DSU_SERVER_PORT_DEFAULT 26760 +#define DSU_CLIENT_PORT_DEFAULT 26761 +#define DSU_SERVER_ADDRESS_DEFAULT "127.0.0.1" + +/* Magic strings */ +#define DSU_MAGIC_CLIENT "DSUC" +#define DSU_MAGIC_SERVER "DSUS" + +/* Message types */ +typedef enum { + DSU_MSG_VERSION = 0x100000, + DSU_MSG_PORTS_INFO = 0x100001, + DSU_MSG_DATA = 0x100002, + DSU_MSG_RUMBLE_INFO = 0x110001, /* Unofficial */ + DSU_MSG_RUMBLE = 0x110002 /* Unofficial */ +} DSU_MessageType; + +/* Controller states */ +typedef enum { + DSU_STATE_DISCONNECTED = 0, + DSU_STATE_RESERVED = 1, + DSU_STATE_CONNECTED = 2 +} DSU_SlotState; + +/* Device models */ +typedef enum { + DSU_MODEL_NONE = 0, + DSU_MODEL_PARTIAL_GYRO = 1, + DSU_MODEL_FULL_GYRO = 2, /* DS4, DS5 */ + DSU_MODEL_NO_GYRO = 3 +} DSU_DeviceModel; + +/* Connection types */ +typedef enum { + DSU_CONN_NONE = 0, + DSU_CONN_USB = 1, + DSU_CONN_BLUETOOTH = 2 +} DSU_ConnectionType; + +/* Battery states */ +typedef enum { + DSU_BATTERY_NONE = 0x00, + DSU_BATTERY_DYING = 0x01, /* 0-10% */ + DSU_BATTERY_LOW = 0x02, /* 10-40% */ + DSU_BATTERY_MEDIUM = 0x03, /* 40-70% */ + DSU_BATTERY_HIGH = 0x04, /* 70-100% */ + DSU_BATTERY_FULL = 0x05, /* 100% */ + DSU_BATTERY_CHARGING = 0xEE, + DSU_BATTERY_CHARGED = 0xEF +} DSU_BatteryState; + +/* Packet structures */ +#pragma pack(push, 1) + +typedef struct { + char magic[4]; /* DSUC or DSUS */ + Uint16 version; /* Protocol version (1001) */ + Uint16 length; /* Packet length after header */ + Uint32 crc32; /* CRC32 of packet (with this field zeroed) */ + Uint32 client_id; /* Random client ID */ + Uint32 message_type; /* Message type enum */ +} DSU_Header; + +typedef struct { + DSU_Header header; + Uint8 flags; /* Slot registration flags */ + Uint8 slot_id; /* 0-3 for specific slot, 0xFF for all */ + Uint8 mac[6]; /* MAC address filter (zeros for all) */ +} DSU_PortRequest; + +typedef struct { + Uint8 slot; /* Controller slot 0-3 */ + Uint8 slot_state; /* DSU_SlotState */ + Uint8 device_model; /* DSU_DeviceModel */ + Uint8 connection_type; /* DSU_ConnectionType */ + Uint8 mac[6]; /* Controller MAC address */ + Uint8 battery; /* DSU_BatteryState */ + Uint8 is_active; /* 0 or 1 */ +} DSU_ControllerInfo; + +typedef struct { + DSU_Header header; + Uint8 slot; /* Controller slot 0-3 */ + Uint8 motor_left; /* Left/Low frequency motor intensity (0-255) */ + Uint8 motor_right; /* Right/High frequency motor intensity (0-255) */ +} DSU_RumblePacket; + +typedef struct { + DSU_Header header; + DSU_ControllerInfo info; + + /* Controller data */ + Uint32 packet_number; /* Incremental counter */ + + /* Digital buttons */ + Uint8 button_states_1; /* Share, L3, R3, Options, DPad */ + Uint8 button_states_2; /* L2, R2, L1, R1, Triangle, Circle, Cross, Square */ + Uint8 button_ps; /* PS/Home button */ + Uint8 button_touch; /* Touchpad button */ + + /* Analog sticks (0-255, 128=center) */ + Uint8 left_stick_x; + Uint8 left_stick_y; + Uint8 right_stick_x; + Uint8 right_stick_y; + + /* Analog buttons (0-255, pressure sensitive) */ + Uint8 analog_dpad_left; + Uint8 analog_dpad_down; + Uint8 analog_dpad_right; + Uint8 analog_dpad_up; + Uint8 analog_button_square; + Uint8 analog_button_cross; + Uint8 analog_button_circle; + Uint8 analog_button_triangle; + Uint8 analog_button_r1; + Uint8 analog_button_l1; + Uint8 analog_trigger_r2; + Uint8 analog_trigger_l2; + + /* Touch data (2 points max) */ + Uint8 touch1_active; + Uint8 touch1_id; + Uint16 touch1_x; + Uint16 touch1_y; + + Uint8 touch2_active; + Uint8 touch2_id; + Uint16 touch2_x; + Uint16 touch2_y; + + /* Motion data (optional) */ + Uint64 motion_timestamp; /* Microseconds */ + float accel_x; /* In g units */ + float accel_y; + float accel_z; + float gyro_pitch; /* In degrees/second */ + float gyro_yaw; + float gyro_roll; +} DSU_ControllerData; + +#pragma pack(pop) + +/* Button masks for button_states_1 */ +#define DSU_BUTTON_SHARE 0x01 +#define DSU_BUTTON_L3 0x02 +#define DSU_BUTTON_R3 0x04 +#define DSU_BUTTON_OPTIONS 0x08 +#define DSU_BUTTON_DPAD_UP 0x10 +#define DSU_BUTTON_DPAD_RIGHT 0x20 +#define DSU_BUTTON_DPAD_DOWN 0x40 +#define DSU_BUTTON_DPAD_LEFT 0x80 + +/* Button masks for button_states_2 */ +#define DSU_BUTTON_L2 0x01 +#define DSU_BUTTON_R2 0x02 +#define DSU_BUTTON_L1 0x04 +#define DSU_BUTTON_R1 0x08 +#define DSU_BUTTON_TRIANGLE 0x10 +#define DSU_BUTTON_CIRCLE 0x20 +#define DSU_BUTTON_CROSS 0x40 +#define DSU_BUTTON_SQUARE 0x80 + +/* Maximum number of DSU slots per server */ +#define DSU_MAX_SLOTS 4 + +/* We can support up to 8 controllers by using 2 server connections */ +#define DSU_MAX_CONTROLLERS 8 + +#endif /* SDL_dsuprotocol_h_ */ + +/* vi: set ts=4 sw=4 expandtab: */