diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7472137a2d006..80fa3463ec3a0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -380,6 +380,7 @@ dep_option(SDL_HIDAPI_LIBUSB "Use libusb for low level joystick drivers" O
dep_option(SDL_HIDAPI_LIBUSB_SHARED "Dynamically load libusb support" ON "SDL_HIDAPI_LIBUSB;SDL_DEPS_SHARED" OFF)
dep_option(SDL_HIDAPI_JOYSTICK "Use HIDAPI for low level joystick drivers" ON SDL_HIDAPI OFF)
dep_option(SDL_VIRTUAL_JOYSTICK "Enable the virtual-joystick driver" ON SDL_HIDAPI OFF)
+option(SDL_DSU_JOYSTICK "Enable DSU client joystick support" ON)
set_option(SDL_LIBUDEV "Enable libudev support" ON)
set_option(SDL_ASAN "Use AddressSanitizer to detect memory errors" OFF)
set_option(SDL_CCACHE "Use Ccache to speed up build" OFF)
@@ -1374,6 +1375,27 @@ if(SDL_JOYSTICK)
"${SDL3_SOURCE_DIR}/src/joystick/virtual/*.h"
)
endif()
+
+ # DSU (DualShock UDP) client support
+ # Supported on platforms with UDP socket support (BSD sockets or WinSock)
+ # Disabled on platforms without UDP support or where not yet implemented
+ if(SDL_DSU_JOYSTICK AND NOT (EMSCRIPTEN OR VITA OR PSP OR PS2 OR N3DS))
+ set(SDL_JOYSTICK_DSU 1)
+ sdl_glob_sources(
+ "${SDL3_SOURCE_DIR}/src/joystick/dsu/*.c"
+ "${SDL3_SOURCE_DIR}/src/joystick/dsu/*.h"
+ )
+
+ # DSU requires network libraries
+ if(WIN32)
+ sdl_link_dependency(dsu LIBS ws2_32)
+ elseif(HAIKU)
+ sdl_link_dependency(dsu LIBS network)
+ elseif(QNX)
+ # QNX needs socket library
+ sdl_link_dependency(dsu LIBS socket)
+ endif()
+ endif()
endif()
if(SDL_VIDEO)
diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index 5f7ac766792b9..d0bcb7ccbd2d4 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -475,6 +475,8 @@
+
+
@@ -746,6 +748,7 @@
+
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index 7af7132b29a66..5d6484c3ecf4e 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -389,6 +389,8 @@
+
+
@@ -637,6 +639,7 @@
+
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 972e0b7e782a1..fb7295d928022 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -2319,6 +2319,56 @@ extern "C" {
*/
#define SDL_HINT_JOYSTICK_WGI "SDL_JOYSTICK_WGI"
+/**
+ * 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.
+ *
+ * This hint should be set before SDL is initialized.
+ *
+ * \since This hint is available since SDL 3.2.0.
+ */
+#define SDL_HINT_JOYSTICK_DSU "SDL_JOYSTICK_DSU"
+
+/**
+ * A variable controlling the DSU server address.
+ *
+ * The default value is "127.0.0.1"
+ *
+ * This hint should be set before SDL is initialized.
+ *
+ * \since This hint is available since SDL 3.2.0.
+ */
+#define SDL_HINT_DSU_SERVER "SDL_DSU_SERVER"
+
+/**
+ * A variable controlling the DSU server port.
+ *
+ * The default value is "26760"
+ *
+ * This hint should be set before SDL is initialized.
+ *
+ * \since This hint is available since SDL 3.2.0.
+ */
+#define SDL_HINT_DSU_SERVER_PORT "SDL_DSU_SERVER_PORT"
+
+/**
+ * A variable controlling the DSU client port.
+ *
+ * The default value is "0" (auto-select)
+ *
+ * This hint should be set before SDL is initialized.
+ *
+ * \since This hint is available since SDL 3.2.0.
+ */
+#define SDL_HINT_DSU_CLIENT_PORT "SDL_DSU_CLIENT_PORT"
+
/**
* A variable containing a list of wheel style controllers.
*
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 35560da940507..7a8ad4cacea56 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -313,6 +313,7 @@
#cmakedefine SDL_JOYSTICK_RAWINPUT 1
#cmakedefine SDL_JOYSTICK_USBHID 1
#cmakedefine SDL_JOYSTICK_VIRTUAL 1
+#cmakedefine SDL_JOYSTICK_DSU 1
#cmakedefine SDL_JOYSTICK_VITA 1
#cmakedefine SDL_JOYSTICK_WGI 1
#cmakedefine SDL_JOYSTICK_XINPUT 1
diff --git a/include/build_config/SDL_build_config_windows.h b/include/build_config/SDL_build_config_windows.h
index 7872d2a92869c..62e646934263c 100644
--- a/include/build_config/SDL_build_config_windows.h
+++ b/include/build_config/SDL_build_config_windows.h
@@ -231,6 +231,7 @@ typedef unsigned int uintptr_t;
#define SDL_JOYSTICK_HIDAPI 1
#define SDL_JOYSTICK_RAWINPUT 1
#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/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index 1841341831ef1..fcf0777ff247a 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -48,6 +48,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 // Highest priority driver for supported devices
&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/SDL_sysjoystick.h b/src/joystick/SDL_sysjoystick.h
index 041ebc3b50903..450b8454b7107 100644
--- a/src/joystick/SDL_sysjoystick.h
+++ b/src/joystick/SDL_sysjoystick.h
@@ -255,6 +255,7 @@ extern SDL_JoystickDriver SDL_RAWINPUT_JoystickDriver;
extern SDL_JoystickDriver SDL_IOS_JoystickDriver;
extern SDL_JoystickDriver SDL_LINUX_JoystickDriver;
extern SDL_JoystickDriver SDL_VIRTUAL_JoystickDriver;
+extern SDL_JoystickDriver SDL_DSU_JoystickDriver;
extern SDL_JoystickDriver SDL_WGI_JoystickDriver;
extern SDL_JoystickDriver SDL_WINDOWS_JoystickDriver;
extern SDL_JoystickDriver SDL_WINMM_JoystickDriver;
diff --git a/src/joystick/dsu/SDL_dsujoystick.c b/src/joystick/dsu/SDL_dsujoystick.c
new file mode 100644
index 0000000000000..f88d615ad3a26
--- /dev/null
+++ b/src/joystick/dsu/SDL_dsujoystick.c
@@ -0,0 +1,1244 @@
+/*
+ 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 system joystick headers */
+#include "../SDL_sysjoystick.h"
+#include "../SDL_joystick_c.h"
+
+/* Define this before including our header to get internal definitions */
+#define IN_JOYSTICK_DSU_
+
+/* Include our header to get structure definitions */
+#include "SDL_dsujoystick_c.h"
+
+/* Additional Windows headers */
+#ifdef _WIN32
+#include
+#ifdef _MSC_VER
+#pragma comment(lib, "ws2_32.lib")
+#endif
+/* Define socklen_t for MinGW/other Windows compilers if needed */
+#ifndef _MSC_VER
+#ifndef socklen_t
+typedef int socklen_t;
+#endif
+#endif
+#endif
+
+/* Ensure timer prototypes are visible */
+#include
+
+/* Platform-specific socket includes */
+#ifndef _WIN32
+ /* iOS/tvOS/watchOS/visionOS - Same as macOS */
+ #if defined(__APPLE__)
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ /* QNX */
+ #elif defined(__QNXNTO__)
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ /* RISC OS */
+ #elif defined(__riscos__)
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ /* Standard Unix/Linux */
+ #else
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #ifdef HAVE_SYS_IOCTL_H
+ #include
+ #endif
+ #ifdef __sun
+ #include
+ #endif
+ #define closesocket close
+ #endif
+
+ /* Default closesocket if not defined */
+ #ifndef closesocket
+ #define closesocket close
+ #endif
+#endif
+
+#include
+
+/* Constants */
+#define SERVER_REREGISTER_INTERVAL 1000 /* ms */
+#define SERVER_TIMEOUT_INTERVAL 2000 /* ms */
+#define GRAVITY_ACCELERATION 9.80665f /* m/s² */
+
+/* Internal DSU helper macros and functions */
+#ifdef _WIN32
+ #define DSU_htons(x) htons(x)
+ #define DSU_htonl(x) htonl(x)
+ #define DSU_SOCKET_ERROR SOCKET_ERROR
+ #define DSU_INVALID_SOCKET INVALID_SOCKET
+#else
+ #define DSU_htons(x) htons(x)
+ #define DSU_htonl(x) htonl(x)
+ #define DSU_SOCKET_ERROR (-1)
+ #define DSU_INVALID_SOCKET (-1)
+#endif
+
+/* Helper function to convert IP address string to network byte order */
+static unsigned long DSU_ipv4_addr(const char *str)
+{
+ struct sockaddr_in addr;
+ SDL_memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+
+#ifdef _WIN32
+ /* Use getaddrinfo on Windows keeping compatibility with older systems (e.g. Windows XP). */
+ {
+ struct addrinfo hints;
+ struct addrinfo *result = NULL;
+
+ SDL_memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET;
+
+ if (getaddrinfo(str, NULL, &hints, &result) == 0 && result && result->ai_addr) {
+ const struct sockaddr_in *ipv4 = (const struct sockaddr_in *)result->ai_addr;
+ addr.sin_addr = ipv4->sin_addr;
+ freeaddrinfo(result);
+ return addr.sin_addr.s_addr;
+ }
+
+ if (result) {
+ freeaddrinfo(result);
+ }
+ }
+#else
+ /* Use inet_pton on Unix-like systems */
+ if (inet_pton(AF_INET, str, &addr.sin_addr) == 1) {
+ return addr.sin_addr.s_addr;
+ }
+#endif
+
+ /* Return INADDR_NONE on error (same as inet_addr would) */
+ return (unsigned long)(-1);
+}
+
+/* Global DSU context pointer */
+struct DSU_Context_t *s_dsu_ctx = NULL;
+
+/* Use the DSU_Context type from the shared header */
+
+/* Forward declarations */
+void DSU_RequestControllerInfo(DSU_Context *ctx, Uint8 slot);
+void DSU_RequestControllerData(DSU_Context *ctx, Uint8 slot);
+
+/* Platform-specific network function wrappers */
+/* Standard sendto/recvfrom for all supported platforms */
+#define DSU_sendto sendto
+#define DSU_recvfrom recvfrom
+#define DSU_GetLastError() errno
+
+/* Socket helpers implementation */
+int DSU_InitSockets(void)
+{
+#ifdef _WIN32
+ WSADATA wsaData;
+ return WSAStartup(MAKEWORD(2, 2), &wsaData);
+#else
+ /* Unix/Linux - no initialization needed */
+ return 0;
+#endif
+}
+
+void DSU_CleanupSockets(void)
+{
+#ifdef _WIN32
+ WSACleanup();
+#else
+ /* Unix/Linux - no cleanup needed */
+#endif
+}
+
+dsu_socket_t DSU_CreateSocket(Uint16 port)
+{
+ dsu_socket_t sock;
+ struct sockaddr_in addr;
+ int reuse = 1;
+#ifdef _WIN32
+ u_long mode = 1;
+#else
+ int flags;
+#if defined(FIONBIO)
+ int nonblock = 1;
+#endif
+#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
+#if defined(FIONBIO)
+ if (ioctl(sock, FIONBIO, &nonblock) < 0) {
+ flags = fcntl(sock, F_GETFL, 0);
+ if (flags != -1) {
+ fcntl(sock, F_SETFL, flags | O_NONBLOCK);
+ }
+ }
+#else
+ flags = fcntl(sock, F_GETFL, 0);
+ if (flags != -1) {
+ fcntl(sock, F_SETFL, flags | O_NONBLOCK);
+ }
+#endif
+#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(dsu_socket_t socket)
+{
+ if (socket != DSU_INVALID_SOCKET) {
+ closesocket(socket);
+ }
+}
+
+/* Send a packet to the DSU server */
+static int DSU_SendPacket(DSU_Context *ctx, void *packet, size_t size)
+{
+ DSU_Header *header;
+ struct sockaddr_in server;
+ int result;
+
+ header = (DSU_Header *)packet;
+
+ /* Fill header */
+ SDL_memcpy(header->magic, DSU_MAGIC_CLIENT, 4);
+ header->version = SDL_Swap16LE(DSU_PROTOCOL_VERSION);
+ header->length = SDL_Swap16LE((Uint16)(size - sizeof(DSU_Header)));
+ header->client_id = SDL_Swap32LE(ctx->client_id);
+ header->crc32 = 0;
+
+ /* Calculate and store CRC32 */
+ header->crc32 = SDL_Swap32LE(SDL_crc32(0, packet, size));
+
+ /* Send to server */
+ SDL_memset(&server, 0, sizeof(server));
+ server.sin_family = AF_INET;
+ server.sin_port = DSU_htons(ctx->server_port);
+ server.sin_addr.s_addr = DSU_ipv4_addr(ctx->server_address);
+
+ result = DSU_sendto(ctx->socket, (const char*)packet, (int)size, 0,
+ (struct sockaddr *)&server, (int)sizeof(server));
+
+ if (result < 0) {
+#ifdef _WIN32
+ int err = WSAGetLastError();
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "DSU: sendto failed with error %d", err);
+#else
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "DSU: sendto failed with errno %d", DSU_GetLastError());
+#endif
+ }
+
+ return result;
+}
+
+/* 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_Swap32LE(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_Swap32LE(DSU_MSG_DATA);
+ request.flags = 0; /* Subscribe to data */
+ request.slot_id = slot;
+
+ DSU_SendPacket(ctx, &request, sizeof(request));
+}
+
+/* Process incoming controller data */
+static void DSU_ProcessControllerData(DSU_Context *ctx, DSU_ControllerData *data)
+{
+ DSU_ControllerSlot *slot;
+ int slot_id;
+ bool was_connected;
+
+ /* Validate context */
+ if (!ctx || !ctx->slots_mutex) {
+ return;
+ }
+
+ /* Get slot ID */
+ slot_id = data->info.slot;
+ if (slot_id >= DSU_MAX_SLOTS) {
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "DSU: Invalid slot_id %d in data packet", slot_id);
+ return;
+ }
+
+ SDL_LockMutex(ctx->slots_mutex);
+ slot = &ctx->slots[slot_id];
+
+ /* If already connected to SDL, just update data without changing state */
+ if (slot->connected) {
+ was_connected = true;
+ } else {
+ /* Update connection state */
+ was_connected = slot->detected;
+ slot->detected = (data->info.slot_state == DSU_STATE_CONNECTED);
+ }
+
+ if (slot->detected || 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 = (Uint8)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 = true;
+ slot->has_accel = true;
+ slot->motion_timestamp = SDL_Swap64LE(data->motion_timestamp);
+
+ /* Convert gyro from deg/s to rad/s (handling endianness) */
+ slot->gyro[0] = SDL_SwapFloatLE(data->gyro_pitch) * (SDL_PI_F / 180.0f);
+ slot->gyro[1] = SDL_SwapFloatLE(data->gyro_yaw) * (SDL_PI_F / 180.0f);
+ slot->gyro[2] = SDL_SwapFloatLE(data->gyro_roll) * (SDL_PI_F / 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_GetTicks();
+
+ /* Touch data */
+ slot->has_touchpad = 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_Swap16LE(data->touch1_x);
+ slot->touch1_y = SDL_Swap16LE(data->touch1_y);
+ slot->touch2_x = SDL_Swap16LE(data->touch2_x);
+ slot->touch2_y = SDL_Swap16LE(data->touch2_y);
+
+ /* Update timing */
+ slot->last_packet_time = SDL_GetTicks();
+ slot->packet_number = SDL_Swap32LE(data->packet_number);
+ }
+
+ /* Handle connection state changes (before unlock) */
+ if (!was_connected && slot->detected) {
+ Uint16 vendor;
+ Uint16 product;
+
+ /* New controller connected */
+ slot->instance_id = SDL_GetNextObjectID();
+
+ /* 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);
+
+ /* Mark as connected before notifying SDL */
+ slot->connected = true;
+
+ /* Unlock our mutex before taking the joystick lock to avoid deadlock */
+ SDL_UnlockMutex(ctx->slots_mutex);
+
+ /* Add the joystick with proper locking */
+ SDL_LockJoysticks();
+ SDL_PrivateJoystickAdded(slot->instance_id);
+ SDL_UnlockJoysticks();
+
+ /* Re-lock our mutex to continue */
+ SDL_LockMutex(ctx->slots_mutex);
+ }
+
+ SDL_UnlockMutex(ctx->slots_mutex);
+
+ /* Subscribe to controller data updates if just detected */
+ if (!was_connected && slot->detected) {
+ DSU_RequestControllerData(ctx, (Uint8)slot_id);
+ }
+}
+
+/* Receiver thread implementation */
+int SDLCALL 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_SetCurrentThreadPriority(SDL_THREAD_PRIORITY_HIGH);
+
+ /* Main receive loop */
+ while (SDL_GetAtomicInt(&ctx->running)) {
+ /* Double-check context is still valid */
+ if (!ctx->slots_mutex) {
+ break;
+ }
+ received = DSU_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_Swap32LE(header->crc32);
+ header->crc32 = 0;
+ calculated_crc = SDL_crc32(0, buffer, (size_t)received);
+
+ if (received_crc == calculated_crc) {
+ Uint32 msg_type = SDL_Swap32LE(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_ControllerData *packet = (DSU_ControllerData *)buffer;
+ DSU_ProcessControllerData(ctx, packet);
+ }
+ break;
+
+ default:
+ /* Unknown message type */
+ break;
+ }
+ };
+ }
+ } else if (received < 0) {
+ /* Check for real errors (not just EWOULDBLOCK) */
+#ifdef _WIN32
+ int error = WSAGetLastError();
+ if (error == WSAENOTSOCK || error == WSAEBADF) {
+ /* Socket closed, exit gracefully */
+ break;
+ }
+ if (error != WSAEWOULDBLOCK && error != WSAEINTR && error != WSAECONNRESET) {
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "DSU: recvfrom error %d", error);
+ SDL_Delay(100); /* Back off on errors */
+ }
+#else
+ int err = DSU_GetLastError();
+ if (err == EBADF) {
+ /* Socket closed, exit gracefully */
+ break;
+ }
+ if (err != EWOULDBLOCK && err != EAGAIN && err != EINTR) {
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "DSU: recvfrom errno %d", err);
+ SDL_Delay(100);
+ }
+#endif
+ }
+
+ /* Small delay to prevent CPU spinning */
+ SDL_Delay(1);
+ }
+
+ return 0;
+}
+
+/* Driver functions - merged from SDL_dsujoystick_driver.c */
+static bool DSU_JoystickInit(void)
+{
+ const char *enabled;
+ const char *server;
+ const char *server_port;
+ const char *client_port;
+ struct DSU_Context_t *ctx;
+
+ /* Check if DSU is enabled */
+ enabled = SDL_GetHint(SDL_HINT_JOYSTICK_DSU);
+ if (enabled && SDL_atoi(enabled) == 0) {
+ return true; /* DSU disabled */
+ }
+
+ /* Allocate context */
+ ctx = (struct DSU_Context_t *)SDL_calloc(1, sizeof(struct DSU_Context_t));
+ if (!ctx) {
+ SDL_OutOfMemory();
+ return false;
+ }
+
+ /* Get configuration from hints with fallbacks */
+ server = SDL_GetHint(SDL_HINT_DSU_SERVER);
+ if (!server || !*server) {
+ server = DSU_SERVER_ADDRESS_DEFAULT;
+ }
+ SDL_strlcpy(ctx->server_address, server,
+ sizeof(ctx->server_address));
+
+ server_port = SDL_GetHint(SDL_HINT_DSU_SERVER_PORT);
+ if (server_port && *server_port) {
+ ctx->server_port = (Uint16)SDL_atoi(server_port);
+ } else {
+ ctx->server_port = DSU_SERVER_PORT_DEFAULT;
+ }
+
+ client_port = SDL_GetHint(SDL_HINT_DSU_CLIENT_PORT);
+ if (client_port && *client_port) {
+ ctx->client_port = (Uint16)SDL_atoi(client_port);
+ } else {
+ ctx->client_port = DSU_CLIENT_PORT_DEFAULT;
+ }
+
+ ctx->client_id = (Uint32)SDL_GetTicks();
+
+ /* Initialize sockets */
+ if (DSU_InitSockets() != 0) {
+ SDL_free(ctx);
+ return false;
+ }
+
+ /* Create UDP socket */
+ ctx->socket = DSU_CreateSocket(ctx->client_port);
+ if (ctx->socket == DSU_INVALID_SOCKET) {
+ DSU_CleanupSockets();
+ SDL_free(ctx);
+ return false;
+ }
+
+ /* Create mutex */
+ ctx->slots_mutex = SDL_CreateMutex();
+ if (!ctx->slots_mutex) {
+ DSU_CloseSocket(ctx->socket);
+ DSU_CleanupSockets();
+ SDL_free(ctx);
+ SDL_OutOfMemory();
+ return false;
+ }
+
+ /* Start receiver thread */
+ SDL_SetAtomicInt(&ctx->running, 1);
+ ctx->receiver_thread = SDL_CreateThread(
+ DSU_ReceiverThread, "DSU_Receiver", ctx);
+ if (!ctx->receiver_thread) {
+ SDL_DestroyMutex(ctx->slots_mutex);
+ DSU_CloseSocket(ctx->socket);
+ DSU_CleanupSockets();
+ SDL_free(ctx);
+ SDL_SetError("Failed to create DSU receiver thread");
+ return false;
+ }
+
+ /* Store context globally */
+ s_dsu_ctx = ctx;
+
+ /* Request controller info from all slots */
+ DSU_RequestControllerInfo(ctx, 0xFF);
+
+ return true;
+}
+
+static int DSU_JoystickGetCount(void)
+{
+ int count = 0;
+ int i;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return 0;
+ }
+
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if (ctx->slots[i].connected) {
+ count++;
+ }
+ }
+ SDL_UnlockMutex(mutex);
+
+ return count;
+}
+
+static void DSU_JoystickDetect(void)
+{
+ Uint64 now;
+ int i;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return;
+ }
+
+ /* Periodically request controller info and re-subscribe to data */
+ now = SDL_GetTicks();
+ if (now - ctx->last_request_time >= 500) { /* Request more frequently */
+ DSU_RequestControllerInfo(ctx, (Uint8)0xFF);
+
+ /* Re-subscribe to data for detected controllers */
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if (ctx->slots[i].detected || ctx->slots[i].connected) {
+ DSU_RequestControllerData(ctx, (Uint8)i);
+ }
+ }
+
+ ctx->last_request_time = now;
+ }
+
+ /* Check for timeouts */
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if ((ctx->slots[i].detected || ctx->slots[i].connected) &&
+ now - ctx->slots[i].last_packet_time > 5000) { /* Increased timeout */
+ /* Controller timed out - notify SDL if it was connected */
+ if (ctx->slots[i].connected && ctx->slots[i].instance_id != 0) {
+ SDL_JoystickID removed_id = ctx->slots[i].instance_id;
+
+ /* Clear state before notifying to avoid race conditions */
+ ctx->slots[i].detected = false;
+ ctx->slots[i].connected = false;
+ ctx->slots[i].instance_id = 0;
+
+ /* Unlock our mutex before taking the joystick lock to avoid deadlock */
+ SDL_UnlockMutex(mutex);
+
+ SDL_LockJoysticks();
+ SDL_PrivateJoystickRemoved(removed_id);
+ SDL_UnlockJoysticks();
+
+ /* Re-lock our mutex to continue the loop */
+ SDL_LockMutex(mutex);
+ } else {
+ /* Clear all state flags */
+ ctx->slots[i].detected = false;
+ ctx->slots[i].connected = false;
+ ctx->slots[i].instance_id = 0;
+ }
+ }
+ }
+ SDL_UnlockMutex(mutex);
+}
+
+static const char *DSU_JoystickGetDeviceName(int device_index)
+{
+ int i, count = 0;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return NULL;
+ }
+
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if (ctx->slots[i].connected) {
+ if (count == device_index) {
+ SDL_UnlockMutex(mutex);
+ return ctx->slots[i].name;
+ }
+ count++;
+ }
+ }
+ SDL_UnlockMutex(mutex);
+
+ return NULL;
+}
+
+static bool DSU_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)
+{
+ /* DSU devices are network-based, not USB, so we don't match by VID/PID */
+ return false;
+}
+
+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;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return -1;
+ }
+
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if (ctx->slots[i].connected) {
+ if (count == device_index) {
+ SDL_UnlockMutex(mutex);
+ return i; /* Return slot ID as player index */
+ }
+ count++;
+ }
+ }
+ SDL_UnlockMutex(mutex);
+
+ return -1;
+}
+
+static void DSU_JoystickSetDevicePlayerIndex(int device_index, int player_index)
+{
+ /* DSU controllers have fixed slots, can't change */
+}
+
+static SDL_GUID DSU_JoystickGetDeviceGUID(int device_index)
+{
+ SDL_GUID guid;
+ int i, count = 0;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ SDL_zero(guid);
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return guid;
+ }
+
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if (ctx->slots[i].connected) {
+ if (count == device_index) {
+ guid = ctx->slots[i].guid;
+ SDL_UnlockMutex(mutex);
+ return guid;
+ }
+ count++;
+ }
+ }
+ SDL_UnlockMutex(mutex);
+
+ return guid;
+}
+
+static SDL_JoystickID DSU_JoystickGetDeviceInstanceID(int device_index)
+{
+ int i, count = 0;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return 0;
+ }
+
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ if (ctx->slots[i].connected) {
+ if (count == device_index) {
+ SDL_JoystickID id = ctx->slots[i].instance_id;
+ SDL_UnlockMutex(mutex);
+ return id;
+ }
+ count++;
+ }
+ }
+ SDL_UnlockMutex(mutex);
+
+ return 0;
+}
+
+static bool DSU_JoystickOpen(SDL_Joystick *joystick, int device_index)
+{
+ DSU_ControllerSlot *slot = NULL;
+ int i, count = 0;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ SDL_SetError("DSU not initialized");
+ return false;
+ }
+
+ if (!joystick) {
+ SDL_SetError("DSU: NULL joystick pointer");
+ return false;
+ }
+
+ /* Find the slot for this device - check detected controllers */
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+ for (i = 0; i < DSU_MAX_SLOTS; i++) {
+ /* Look for detected controllers that are about to be connected */
+ if (ctx->slots[i].detected && ctx->slots[i].instance_id != 0) {
+ if (count == device_index) {
+ slot = &ctx->slots[i];
+ break;
+ }
+ count++;
+ }
+ }
+ SDL_UnlockMutex(mutex);
+
+ if (!slot) {
+ SDL_SetError("Invalid DSU device index");
+ return false;
+ }
+
+ 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 = 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 = true;
+ }
+
+ return true;
+}
+
+static bool 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;
+ struct DSU_Context_t *ctx;
+
+ ctx = s_dsu_ctx;
+ if (!ctx || !slot || !slot->connected) {
+ SDL_SetError("DSU controller not available");
+ return false;
+ }
+
+ /* Build rumble packet */
+ SDL_memset(&packet, 0, sizeof(packet));
+ SDL_memcpy(packet.header.magic, DSU_MAGIC_CLIENT, 4);
+ packet.header.version = SDL_Swap16LE(DSU_PROTOCOL_VERSION);
+ packet.header.length = SDL_Swap16LE((Uint16)(sizeof(packet) - sizeof(DSU_Header)));
+ packet.header.client_id = SDL_Swap32LE(ctx->client_id);
+ packet.header.message_type = SDL_Swap32LE(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_Swap32LE(SDL_crc32(0, &packet, sizeof(packet)));
+
+ /* Send to server */
+ SDL_memset(&server, 0, sizeof(server));
+ server.sin_family = AF_INET;
+ server.sin_port = DSU_htons(ctx->server_port);
+ server.sin_addr.s_addr = DSU_ipv4_addr(ctx->server_address);
+ if (DSU_sendto(ctx->socket, (const char*)&packet, (int)sizeof(packet), 0,
+ (struct sockaddr *)&server, (int)sizeof(server)) < 0) {
+ SDL_SetError("Failed to send rumble packet");
+ return false;
+ }
+
+ return true;
+}
+
+static bool DSU_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
+{
+ SDL_Unsupported();
+ return false;
+}
+
+static bool DSU_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
+{
+ SDL_Unsupported();
+ return false;
+}
+
+static bool DSU_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)
+{
+ SDL_Unsupported();
+ return false;
+}
+
+static bool DSU_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)
+{
+ DSU_ControllerSlot *slot = (DSU_ControllerSlot *)joystick->hwdata;
+
+ /* Sensors are always enabled if available */
+ if (!(slot->has_gyro || slot->has_accel)) {
+ SDL_Unsupported();
+ return false;
+ }
+ return true;
+}
+
+static void DSU_JoystickUpdate(SDL_Joystick *joystick)
+{
+ DSU_ControllerSlot *slot = (DSU_ControllerSlot *)joystick->hwdata;
+ struct DSU_Context_t *ctx;
+ SDL_Mutex *mutex;
+ Uint64 timestamp;
+ int i;
+
+ if (!slot || !slot->connected) {
+ return;
+ }
+
+ ctx = s_dsu_ctx;
+ if (!ctx || !ctx->slots_mutex) {
+ return;
+ }
+
+ mutex = ctx->slots_mutex;
+ SDL_LockMutex(mutex);
+
+ /* Get current timestamp */
+ timestamp = SDL_GetTicks();
+
+ /* Update buttons */
+ for (i = 0; i < 12; i++) {
+ bool pressed = (slot->buttons & (1 << i)) ? true : false;
+ SDL_SendJoystickButton(timestamp, joystick, (Uint8)i, pressed);
+ }
+
+ /* Update axes */
+ for (i = 0; i < 6; i++) {
+ SDL_SendJoystickAxis(timestamp, joystick, (Uint8)i, slot->axes[i]);
+ }
+
+ /* Update hat (D-Pad) */
+ SDL_SendJoystickHat(timestamp, 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 */
+ bool touchpad_down = slot->touch1_active;
+ 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_SendJoystickTouchpad(timestamp, joystick, 0, 0, touchpad_down,
+ touchpad_x, touchpad_y,
+ touchpad_down ? 1.0f : 0.0f);
+
+ /* Second touch point */
+ touchpad_down = slot->touch2_active;
+ 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_SendJoystickTouchpad(timestamp, joystick, 0, 1, touchpad_down,
+ touchpad_x, touchpad_y,
+ touchpad_down ? 1.0f : 0.0f);
+ }
+
+ /* Update battery level */
+ switch (slot->battery) {
+ case DSU_BATTERY_DYING:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, 10);
+ break;
+ case DSU_BATTERY_LOW:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, 25);
+ break;
+ case DSU_BATTERY_MEDIUM:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, 55);
+ break;
+ case DSU_BATTERY_HIGH:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, 85);
+ break;
+ case DSU_BATTERY_FULL:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, 100);
+ break;
+ case DSU_BATTERY_CHARGING:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_CHARGING, -1);
+ break;
+ case DSU_BATTERY_CHARGED:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_CHARGING, 100);
+ break;
+ default:
+ SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_UNKNOWN, -1);
+ break;
+ }
+
+ /* Update sensors if available */
+ if (slot->has_gyro) {
+ SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO,
+ slot->motion_timestamp, slot->gyro, 3);
+ }
+ if (slot->has_accel) {
+ SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL,
+ slot->motion_timestamp, slot->accel, 3);
+ }
+
+ SDL_UnlockMutex(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)
+{
+ struct DSU_Context_t *ctx;
+
+ ctx = s_dsu_ctx;
+ if (!ctx) {
+ return;
+ }
+
+ /* Clear the global pointer first to prevent access during shutdown */
+ s_dsu_ctx = NULL;
+
+ /* Stop receiver thread */
+ if (SDL_GetAtomicInt(&ctx->running) != 0) {
+ SDL_SetAtomicInt(&ctx->running, 0);
+ }
+
+ /* Close socket to interrupt any blocking recvfrom */
+ if (ctx->socket != DSU_INVALID_SOCKET) {
+ DSU_CloseSocket(ctx->socket);
+ ctx->socket = DSU_INVALID_SOCKET;
+ }
+
+ /* Now wait for thread to finish */
+ if (ctx->receiver_thread) {
+ SDL_WaitThread(ctx->receiver_thread, NULL);
+ ctx->receiver_thread = NULL;
+ }
+
+ /* Clean up sockets */
+ DSU_CleanupSockets();
+
+ /* Clean up mutex */
+ if (ctx->slots_mutex) {
+ SDL_DestroyMutex(ctx->slots_mutex);
+ }
+
+ /* Free context */
+ SDL_free(ctx);
+}
+
+static bool DSU_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
+{
+ /* DSU controllers map well to standard gamepad layout */
+ return false; /* Use default mapping */
+}
+
+/* Export the driver */
+SDL_JoystickDriver SDL_DSU_JoystickDriver = {
+ DSU_JoystickInit,
+ DSU_JoystickGetCount,
+ DSU_JoystickDetect,
+ DSU_JoystickIsDevicePresent,
+ DSU_JoystickGetDeviceName,
+ DSU_JoystickGetDevicePath,
+ NULL, /* GetDeviceSteamVirtualGamepadSlot */
+ DSU_JoystickGetDevicePlayerIndex,
+ DSU_JoystickSetDevicePlayerIndex,
+ DSU_JoystickGetDeviceGUID,
+ DSU_JoystickGetDeviceInstanceID,
+ DSU_JoystickOpen,
+ DSU_JoystickRumble,
+ DSU_JoystickRumbleTriggers,
+ 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_dsujoystick_c.h b/src/joystick/dsu/SDL_dsujoystick_c.h
new file mode 100644
index 0000000000000..8c56c2f1ef850
--- /dev/null
+++ b/src/joystick/dsu/SDL_dsujoystick_c.h
@@ -0,0 +1,116 @@
+/*
+ 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
+#include
+#include
+#include
+#include "../SDL_sysjoystick.h"
+#include "SDL_dsuprotocol.h"
+
+/* DSU Joystick driver */
+extern SDL_JoystickDriver SDL_DSU_JoystickDriver;
+
+/* Socket type definitions - move these out to ensure visibility */
+#ifdef _WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+#include
+typedef SOCKET dsu_socket_t;
+#else
+typedef int dsu_socket_t;
+#endif
+
+/* Internal structures - always visible */
+typedef struct DSU_ControllerSlot {
+ /* Connection state */
+ bool detected; /* Controller detected by DSU but not yet added to SDL */
+ bool connected; /* Controller added to SDL and visible */
+ SDL_JoystickID instance_id;
+ SDL_GUID 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 */
+ bool has_gyro;
+ 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 */
+ bool has_touchpad;
+ bool touch1_active;
+ 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_t {
+ /* Network */
+ dsu_socket_t socket;
+ SDL_Thread *receiver_thread;
+ SDL_AtomicInt 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;
+
+#endif /* SDL_JOYSTICK_DSU */
+
+#endif /* SDL_dsujoystick_c_h_ */
+
+/* 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..952deaa8e06e7
--- /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: */