From df289d564ae91cfd8ca90a4e8a88ecb838ac063f Mon Sep 17 00:00:00 2001 From: danprice142 Date: Sat, 25 Oct 2025 19:50:52 +0100 Subject: [PATCH 1/3] Add DSU joystick driver Introduces DSU client joystick support for SDL, enabling connection to DSU servers (such as DS4Windows and BetterJoy) to receive controller data over UDP, including motion sensors and touchpad data. Adds build system options, configuration hints, protocol implementation, and driver integration for Windows and other platforms. --- CMakeLists.txt | 12 + include/SDL_config.h.cmake | 1 + include/SDL_config_windows.h | 1 + include/SDL_hints.h | 34 ++ src/joystick/SDL_joystick.c | 7 + src/joystick/dsu/SDL_dsujoystick.c | 449 ++++++++++++++++ src/joystick/dsu/SDL_dsujoystick_c.h | 108 ++++ src/joystick/dsu/SDL_dsujoystick_driver.c | 615 ++++++++++++++++++++++ src/joystick/dsu/SDL_dsuprotocol.h | 201 +++++++ 9 files changed, 1428 insertions(+) create mode 100644 src/joystick/dsu/SDL_dsujoystick.c create mode 100644 src/joystick/dsu/SDL_dsujoystick_c.h create mode 100644 src/joystick/dsu/SDL_dsujoystick_driver.c create mode 100644 src/joystick/dsu/SDL_dsuprotocol.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d609224783019..b015981969918 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1245,6 +1245,14 @@ 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}) + endif() endif() if(SDL_VIDEO) @@ -1984,6 +1992,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) 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..0ab209d50013c --- /dev/null +++ b/src/joystick/dsu/SDL_dsujoystick.c @@ -0,0 +1,449 @@ +/* + 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; + + 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 non-blocking */ +#ifdef _WIN32 + u_long mode = 1; + ioctlsocket(sock, FIONBIO, &mode); +#else + int 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) { + /* New controller connected */ + slot->instance_id = SDL_GetNextJoystickInstanceID(); + + /* Create GUID */ + Uint16 vendor = 0x054C; /* Sony vendor ID */ + Uint16 product = (slot->model == DSU_MODEL_FULL_GYRO) ? 0x09CC : 0x05C4; + 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) { + /* Validate CRC32 */ + Uint32 received_crc = SDL_SwapLE32(header->crc32); + header->crc32 = 0; + Uint32 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)) { + /* Parse port info */ + Uint8 *data_ptr = buffer + sizeof(DSU_Header); + Uint8 slot_id = data_ptr[0]; + Uint8 slot_state = data_ptr[1]; + Uint8 device_model = data_ptr[2]; + Uint8 connection_type = data_ptr[3]; + + /* 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: */ From 3ecb594b2ca951019ef19d52dfb28ca4c79b36e4 Mon Sep 17 00:00:00 2001 From: danprice142 Date: Sun, 26 Oct 2025 00:00:17 +0100 Subject: [PATCH 2/3] Fix C90 compatibility issues in DSU joystick driver for Haiku build --- src/joystick/dsu/SDL_dsujoystick.c | 42 +++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/joystick/dsu/SDL_dsujoystick.c b/src/joystick/dsu/SDL_dsujoystick.c index 0ab209d50013c..27aed4b9827a0 100644 --- a/src/joystick/dsu/SDL_dsujoystick.c +++ b/src/joystick/dsu/SDL_dsujoystick.c @@ -92,6 +92,11 @@ 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) { @@ -101,12 +106,11 @@ int DSU_CreateSocket(Uint16 port) /* Allow address reuse */ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)); - /* Set non-blocking */ + /* Set socket to non-blocking */ #ifdef _WIN32 - u_long mode = 1; ioctlsocket(sock, FIONBIO, &mode); #else - int flags = fcntl(sock, F_GETFL, 0); + flags = fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, flags | O_NONBLOCK); #endif @@ -338,12 +342,18 @@ void DSU_ProcessControllerData(DSU_Context *ctx, DSU_ControllerData *data) /* Handle connection state changes */ if (!was_connected && slot->connected) { + Uint16 vendor; + Uint16 product; + /* New controller connected */ slot->instance_id = SDL_GetNextJoystickInstanceID(); - /* Create GUID */ - Uint16 vendor = 0x054C; /* Sony vendor ID */ - Uint16 product = (slot->model == DSU_MODEL_FULL_GYRO) ? 0x09CC : 0x05C4; + /* 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); @@ -379,10 +389,13 @@ int DSU_ReceiverThread(void *data) /* Validate magic */ if (SDL_memcmp(header->magic, DSU_MAGIC_SERVER, 4) == 0) { + Uint32 received_crc; + Uint32 calculated_crc; + /* Validate CRC32 */ - Uint32 received_crc = SDL_SwapLE32(header->crc32); + received_crc = SDL_SwapLE32(header->crc32); header->crc32 = 0; - Uint32 calculated_crc = DSU_CalculateCRC32(buffer, received); + calculated_crc = DSU_CalculateCRC32(buffer, received); if (received_crc == calculated_crc) { Uint32 msg_type = SDL_SwapLE32(header->message_type); @@ -395,12 +408,15 @@ int DSU_ReceiverThread(void *data) 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 */ - Uint8 *data_ptr = buffer + sizeof(DSU_Header); - Uint8 slot_id = data_ptr[0]; - Uint8 slot_state = data_ptr[1]; - Uint8 device_model = data_ptr[2]; - Uint8 connection_type = data_ptr[3]; + 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) { From b35d4bd9b7daf27c2814ebf42f7ae9fa76ff815d Mon Sep 17 00:00:00 2001 From: danprice142 Date: Sun, 26 Oct 2025 00:26:39 +0100 Subject: [PATCH 3/3] Add DSU joystick support with proper network library linking for Haiku and Unix systems --- CMakeLists.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b015981969918..065d483518a54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1252,6 +1252,27 @@ if(SDL_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() @@ -2480,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)