From 138683e64546582b9ba53e6cb8a39a759abbc115 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Wed, 13 May 2026 16:34:03 +0500 Subject: [PATCH 01/13] Web HIDAPI --- .github/workflows/create-test-plan.py | 2 +- CMakeLists.txt | 4 + src/hidapi/SDL_hidapi.c | 2 + src/hidapi/SDL_hidapi_emscripten.h | 25 ++ src/hidapi/emscripten/hid.c | 495 +++++++++++++++++++++ src/joystick/emscripten/SDL_sysjoystick.c | 21 + src/joystick/hidapi/SDL_hidapi_ps4.c | 5 + src/joystick/hidapi/SDL_hidapijoystick.c | 2 +- src/joystick/hidapi/SDL_hidapijoystick_c.h | 2 + 9 files changed, 556 insertions(+), 2 deletions(-) create mode 100644 src/hidapi/SDL_hidapi_emscripten.h create mode 100644 src/hidapi/emscripten/hid.c diff --git a/.github/workflows/create-test-plan.py b/.github/workflows/create-test-plan.py index c0e847c05f..15d3860b7f 100755 --- a/.github/workflows/create-test-plan.py +++ b/.github/workflows/create-test-plan.py @@ -621,7 +621,7 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool, ctest_args "-ffile-prefix-map=${PWD}=/SDL", )) job.ldflags.extend(( - "--source-map-base", "/", + "--source-map-base", "/", "-s", "ASYNCIFY", )) pretest_cmd.extend(( "# Start local HTTP server", diff --git a/CMakeLists.txt b/CMakeLists.txt index 851e11add9..1c7eb0809e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1772,6 +1772,10 @@ elseif(EMSCRIPTEN) ) endif() + if(SDL_HIDAPI) + CheckHIDAPI() + endif() + if(SDL_JOYSTICK) set(SDL_JOYSTICK_EMSCRIPTEN 1) sdl_glob_sources( diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c index 0dda4d4ba1..7e1ed944d1 100644 --- a/src/hidapi/SDL_hidapi.c +++ b/src/hidapi/SDL_hidapi.c @@ -595,6 +595,8 @@ typedef struct PLATFORM_hid_device_ PLATFORM_hid_device; #include "SDL_hidapi_android.h" #elif defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) #include "SDL_hidapi_ios.h" +#elif defined(SDL_PLATFORM_EMSCRIPTEN) +#include "SDL_hidapi_emscripten.h" #endif #undef api_version diff --git a/src/hidapi/SDL_hidapi_emscripten.h b/src/hidapi/SDL_hidapi_emscripten.h new file mode 100644 index 0000000000..dbf6f1e1fa --- /dev/null +++ b/src/hidapi/SDL_hidapi_emscripten.h @@ -0,0 +1,25 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 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. +*/ + +#undef HIDAPI_H__ +#include "emscripten/hid.c" +#define HAVE_PLATFORM_BACKEND 1 +#define udev_ctx 1 diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c new file mode 100644 index 0000000000..481d918515 --- /dev/null +++ b/src/hidapi/emscripten/hid.c @@ -0,0 +1,495 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#include "../hidapi/hidapi.h" +#include +#include +#include + +EM_JS_DEPS(hidapi, "$dynCall"); + +struct hid_device_ { + int device_id; + unsigned char *last_report; + size_t last_report_length; + int last_report_read; + struct hid_device_info* device_info; +}; + +static struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH +}; + +static hid_device *new_hid_device(void) +{ + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + if (dev == NULL) { + return NULL; + } + + dev->device_id = -1; + dev->last_report = NULL; + dev->last_report_length = 0; + dev->last_report_read = 0; + + return dev; +} + +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void) +{ + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) +{ + return HID_API_VERSION_STR; +} + +// static void test(hid_device *dev, unsigned char *data, size_t length) +// static void test(unsigned char *data) +// { +// // printf("Test function, device_id=%d\n", dev->device_id); +// printf("Test function\n"); +// for (int i = 0; i < 3; i++) { +// printf("Test function, data[%d]=%d\n", i, data[i]); +// } +// free(data); +// } + +int HID_API_EXPORT hid_init(void) +{ + // MAIN_THREAD_EM_ASM({ + // const typedArray = Uint8Array.from([1,2,3]); + // const heapPointer = _malloc(typedArray.length * typedArray.BYTES_PER_ELEMENT); + // HEAPU8.set(typedArray, heapPointer); + // dynCall('vi', $0, [heapPointer]); + // }, &test); + + return MAIN_THREAD_EM_ASM_INT({ + return "hid" in navigator ? 0 : -1; + }); +} + +int HID_API_EXPORT hid_exit(void) +{ + return 0; +} + +static struct hid_device_info * create_device_info_for_device(int device_id) +{ + struct hid_device_info *root = NULL; + struct hid_device_info *cur_dev = NULL; + char path[16]; + /*wchar_t product_string[128];*/ + int ignore = 0; + int input_report_count; + + ignore = MAIN_THREAD_EM_ASM_INT({ + let device = window._hidDeviceList[$0]; + if (!device) { + return true; + } + return false; + }, device_id); + + if (ignore) + goto end; + + /* Create the record. */ + root = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (!root) + goto end; + + cur_dev = root; + + snprintf(path, sizeof(path), "hid%d", device_id); + cur_dev->path = strdup(path); + + cur_dev->vendor_id = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].vendorId; + }, device_id); + cur_dev->product_id = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].productId; + }, device_id); + + cur_dev->serial_number = NULL; + cur_dev->release_number = 0; + cur_dev->manufacturer_string = NULL; + + /* I'm too tired to make stringToUTF32 work correctly here, cmake doesn't want to include it in the build */ + /*MAIN_THREAD_EM_ASM({ + stringToUTF32(window._hidDeviceList[$0].productName, $1, Number($2)); + }, device_id, product_string, sizeof(product_string)); + + cur_dev->product_string = wcsdup(product_string);*/ + + cur_dev->product_string = NULL; + + cur_dev->usage_page = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].collections[0].usagePage; + }, device_id); + cur_dev->usage = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].collections[0].usage; + }, device_id); + cur_dev->interface_number = 0; + + input_report_count = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].collections[0].inputReports.length; + }, device_id); + + /* WebHID doesn't provide the bus type, so we have to guess it ourselves */ + if (input_report_count >= 5) { + cur_dev->bus_type = HID_API_BUS_BLUETOOTH; + } else { + cur_dev->bus_type = HID_API_BUS_USB; + } + + cur_dev->next = NULL; + + printf("HIDAPI: New device found: %d %d\n", cur_dev->vendor_id, cur_dev->product_id); +end: + return root; +} + +// TODO: remove all EM_ASYNC_JS +EM_ASYNC_JS(int, hid_js_get_device_count, (), { + window._hidDeviceList = await navigator.hid.getDevices(); + return window._hidDeviceList.length; +}); + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + int device_count; + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + + hid_init(); + + device_count = hid_js_get_device_count(); + + printf("hid_enumerate, device_count=%d\n", device_count); + + int device_id; + for (device_id = 0; device_id < device_count; device_id++) { + struct hid_device_info * tmp; + /* TODO: handle vendor_id and product_id */ + tmp = create_device_info_for_device(device_id); + + if (tmp) { + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + /* move the pointer to the tail of returned list */ + while (cur_dev->next != NULL) { + cur_dev = cur_dev->next; + } + } + } + + return root; +} + +void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) +{ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + /*free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string);*/ + free(d); + d = next; + } +} + +hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + /* register_global_error: global error is reset by hid_enumerate/hid_init */ + devs = hid_enumerate(vendor_id, product_id); + if (devs == NULL) { + /* register_global_error: global error is already set by hid_enumerate */ + return NULL; + } + + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +typedef void(*SetByteCallback)(unsigned char *data, size_t length, int byte, size_t offset); +typedef void(*SetReportCallback)(hid_device *dev, unsigned char *data, size_t length); + +static void set_byte(unsigned char *data, size_t length, int byte, size_t offset) +{ + if (offset >= length) + return; + data[offset] = (unsigned char)byte; + // printf("set_byte: offset=%d byte=%d\n", (int)offset, byte); +} + +static void set_report(hid_device *dev, unsigned char *data, size_t length) +{ + if (dev->last_report) { + free(dev->last_report); + } + dev->last_report = data; + dev->last_report_length = length; + dev->last_report_read = 0; +} + +EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetByteCallback callback, SetReportCallback set_report_callback), { + let device = window._hidDeviceList[device_id]; + console.log("Opening device1 " + device_id); + if (device) { + console.log("Opening device2 " + device_id); + await device.open(); + device.addEventListener("inputreport", function (event) { + const { data, device, reportId } = event; + + let dataLength = data['byteLength']+1; + let pointer = _malloc(dataLength); + dynCall("viiii", callback, [pointer, dataLength, report_id, 0]); + for (let i = 0; i < data['byteLength']; i++) { + dynCall("viiii", callback, [pointer, dataLength, data['getUint8'](i), i+1]); + } + dynCall("viii", set_report_callback, [dev, pointer, dataLength]); + }); + } +}); + +hid_device * HID_API_EXPORT hid_open_path(const char *path) +{ + hid_device *dev = NULL; + int device_id = 0; + + hid_init(); + /* register_global_error: global error is reset by hid_init */ + + printf("hid_open_path: %s\n", path); + dev = new_hid_device(); + if (!dev) { + printf("hid_open_path: no memory\n"); + return NULL; + } + + if (sscanf(path, "hid%d", &device_id) != 1) { + free(dev); + printf("hid_open_path: invalid path\n"); + return NULL; + } + + dev->device_id = device_id; + hid_js_open(device_id, dev, set_byte, set_report); + + return dev; +} + +EM_ASYNC_JS(int, hid_js_write, (int device_id, int report_id, const unsigned char *data, size_t length), { + let device = window._hidDeviceList[device_id]; + if (device) { + let dataArray = new Uint8Array(length); + for (let i = 0; i < length; i++) { + // console.log("hid_write: offset=" + i + " byte=" + HEAPU8[data+i]); + dataArray[i] = HEAPU8[data+i]; + } + await device.sendReport(report_id, dataArray); + } +}); + +int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + if (length < 1) + return 0; + hid_js_write(dev->device_id, data[0], data+1, length-1); + return length; +} + +EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_t length, SetByteCallback callback), { + let device = window._hidDeviceList[device_id]; + if (device) { + let [report_id, dataView] = await new Promise(function(resolve, reject) { + device.addEventListener("inputreport", function (event) { + const { data, device, reportId } = event; + resolve([reportId, data]); // done + }, {once: true}); + }); + dynCall("viiii", callback, [data, length, report_id, 0]); + for (let i = 0; i < dataView['byteLength']; i++) { + dynCall("viiii", callback, [data, length, dataView['getUint8'](i), i+1]); + } + return dataView['byteLength']+1; + } + return 0; +}); + +int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + /* TODO: timeout */ + if (length < 1) + return -1; + if (milliseconds == 0) { + if (dev->last_report && !dev->last_report_read) { + size_t return_size = length; + if (dev->last_report_length < length) { + return_size = dev->last_report_length; + } + memcpy(data, dev->last_report, return_size); + dev->last_report_read = 1; + return return_size; + } else { + return 0; + } + } + return hid_js_read_timeout(dev->device_id, data, length, &set_byte); +} + +int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + /* TODO: blocking */ + return hid_read_timeout(dev, data, length, -1); +} + +int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) +{ + /* TODO */ + return 0; +} + +int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + return 0; +} + +EM_ASYNC_JS(void, hid_js_get_feature_report, (int device_id, int report_id, unsigned char *data, size_t length, SetByteCallback callback), { + let device = window._hidDeviceList[device_id]; + if (device) { + let dataView = await device['receiveFeatureReport'](report_id); + for (let i = 0; i < dataView['byteLength']; i++) { + dynCall("viiii", callback, [data, length, dataView['getUint8'](i), i]); + } + } +}); + +int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + if (length < 1) + return -1; + int report_id = (int)data[0]; + hid_js_get_feature_report(dev->device_id, report_id, data, length, &set_byte); + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + return 0; +} + +EM_ASYNC_JS(int, hid_js_close, (int device_id), { + let device = window._hidDeviceList[device_id]; + if (device) { + await device.close(); + } +}); + +void HID_API_EXPORT hid_close(hid_device *dev) +{ + if (!dev) + return; + + if (dev->last_report) { + free(dev->last_report); + } + + hid_js_close(dev->device_id); + hid_free_enumeration(dev->device_info); + + free(dev); +} + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return 0; +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return 0; +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return 0; +} + +HID_API_EXPORT struct hid_device_info *HID_API_CALL hid_get_device_info(hid_device *dev) +{ + return NULL; +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + return -1; +} + +int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size) +{ + return 0; +} + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + return L""; +} \ No newline at end of file diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index adf471a199..c7318ea98e 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -28,6 +28,7 @@ #include "SDL_sysjoystick_c.h" #include "../SDL_joystick_c.h" #include "../usb_ids.h" +#include "../hidapi/SDL_hidapijoystick_c.h" static SDL_joylist_item *JoystickByIndex(int index); @@ -120,6 +121,17 @@ static int SDL_GetEmscriptenOSID() }); } +#ifdef SDL_JOYSTICK_HIDAPI +static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product) +{ + MAIN_THREAD_EM_ASM({ + if ("hid" in navigator) { + navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); + } + }, vendor, product); +} +#endif + static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) { SDL_joylist_item *item; @@ -148,6 +160,15 @@ static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamep product = SDL_GetEmscriptenJoystickProduct(gamepadEvent->index); is_xinput = SDL_IsEmscriptenJoystickXInput(gamepadEvent->index); +#ifdef SDL_JOYSTICK_HIDAPI + if (HIDAPI_IsDeviceSupported(vendor, product, 0, "")) { + SDL_RequestWebHIDDevice(vendor, product); + printf("HIDAPI_IsDeviceSupported\n"); + SDL_free(item); + goto done; + } +#endif + // Use a generic VID/PID representing an XInput controller if (!vendor && !product && is_xinput) { vendor = USB_VENDOR_MICROSOFT; diff --git a/src/joystick/hidapi/SDL_hidapi_ps4.c b/src/joystick/hidapi/SDL_hidapi_ps4.c index 62c9ff6273..dbdcdae3f2 100644 --- a/src/joystick/hidapi/SDL_hidapi_ps4.c +++ b/src/joystick/hidapi/SDL_hidapi_ps4.c @@ -1034,6 +1034,7 @@ static void HIDAPI_DriverPS4_HandleStatePacket(SDL_Joystick *joystick, SDL_hid_d SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, touchpad_down, touchpad_x * TOUCHPAD_SCALEX, touchpad_y * TOUCHPAD_SCALEY, touchpad_down ? 1.0f : 0.0f); } + printf("PS4 Packet usb1 %d\n", packet->rgucButtonsHatAndCounter[0]); if (ctx->last_state.rgucButtonsHatAndCounter[0] != packet->rgucButtonsHatAndCounter[0]) { { Uint8 data = (packet->rgucButtonsHatAndCounter[0] >> 4); @@ -1295,7 +1296,9 @@ static bool HIDAPI_DriverPS4_UpdateDevice(SDL_HIDAPI_Device *device) #ifdef DEBUG_PS4_PROTOCOL HIDAPI_DumpPacket("PS4 packet: size = %d", data, size); #endif + printf("PS4 Packet start\n"); if (!HIDAPI_DriverPS4_IsPacketValid(ctx, data, size)) { + printf("PS4 Packet invalid\n"); continue; } @@ -1303,11 +1306,13 @@ static bool HIDAPI_DriverPS4_UpdateDevice(SDL_HIDAPI_Device *device) ctx->last_packet = now; if (!joystick) { + printf("PS4 Packet no joystick\n"); continue; } switch (data[0]) { case k_EPS4ReportIdUsbState: + printf("PS4 Packet usb, packet[1]=%d\n", +data[1]); HIDAPI_DriverPS4_HandleStatePacket(joystick, device->dev, ctx, (PS4StatePacket_t *)&data[1], size - 1); break; case k_EPS4ReportIdBluetoothState1: diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index 81587b0a33..8fa0fc66e9 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -342,7 +342,7 @@ static SDL_GamepadType SDL_GetJoystickGameControllerProtocol(const char *name, U return type; } -static bool HIDAPI_IsDeviceSupported(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name) +bool HIDAPI_IsDeviceSupported(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name) { int i; SDL_GamepadType type = SDL_GetJoystickGameControllerProtocol(name, vendor_id, product_id, -1, 0, 0, 0); diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h index 59c62f6fe5..9e7be455de 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick_c.h +++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h @@ -180,6 +180,8 @@ extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverZUIKI; (((Uint32)(C)) << 16) | \ (((Uint32)(D)) << 24)) +extern bool HIDAPI_IsDeviceSupported(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name); + // Return true if a HID device is present and supported as a joystick of the given type extern bool HIDAPI_IsDeviceTypePresent(SDL_GamepadType type); From 6577fc722ef2e2aab9e3855ab941d55866949164 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 14 May 2026 16:27:55 +0500 Subject: [PATCH 02/13] WebHID fixes --- src/hidapi/emscripten/hid.c | 47 +++++++++++------------- src/joystick/hidapi/SDL_hidapi_ps4.c | 5 --- src/joystick/hidapi/SDL_hidapi_rumble.c | 16 ++++++++ src/joystick/hidapi/SDL_hidapijoystick.c | 12 ++++++ 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index 481d918515..b66d672517 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -27,6 +27,10 @@ EM_JS_DEPS(hidapi, "$dynCall"); +#if 0 +#define HIDAPI_WEBHID_DEBUG +#endif + struct hid_device_ { int device_id; unsigned char *last_report; @@ -66,26 +70,8 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } -// static void test(hid_device *dev, unsigned char *data, size_t length) -// static void test(unsigned char *data) -// { -// // printf("Test function, device_id=%d\n", dev->device_id); -// printf("Test function\n"); -// for (int i = 0; i < 3; i++) { -// printf("Test function, data[%d]=%d\n", i, data[i]); -// } -// free(data); -// } - int HID_API_EXPORT hid_init(void) { - // MAIN_THREAD_EM_ASM({ - // const typedArray = Uint8Array.from([1,2,3]); - // const heapPointer = _malloc(typedArray.length * typedArray.BYTES_PER_ELEMENT); - // HEAPU8.set(typedArray, heapPointer); - // dynCall('vi', $0, [heapPointer]); - // }, &test); - return MAIN_THREAD_EM_ASM_INT({ return "hid" in navigator ? 0 : -1; }); @@ -167,12 +153,14 @@ static struct hid_device_info * create_device_info_for_device(int device_id) cur_dev->next = NULL; +#ifdef HIDAPI_WEBHID_DEBUG printf("HIDAPI: New device found: %d %d\n", cur_dev->vendor_id, cur_dev->product_id); +#endif end: return root; } -// TODO: remove all EM_ASYNC_JS +// TODO: remove all EM_ASYNC_JS for non-blocking operations EM_ASYNC_JS(int, hid_js_get_device_count, (), { window._hidDeviceList = await navigator.hid.getDevices(); return window._hidDeviceList.length; @@ -188,7 +176,9 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u device_count = hid_js_get_device_count(); +#ifdef HIDAPI_WEBHID_DEBUG printf("hid_enumerate, device_count=%d\n", device_count); +#endif int device_id; for (device_id = 0; device_id < device_count; device_id++) { @@ -277,7 +267,9 @@ static void set_byte(unsigned char *data, size_t length, int byte, size_t offset if (offset >= length) return; data[offset] = (unsigned char)byte; +#ifdef HIDAPI_WEBHID_DEBUG // printf("set_byte: offset=%d byte=%d\n", (int)offset, byte); +#endif } static void set_report(hid_device *dev, unsigned char *data, size_t length) @@ -292,16 +284,14 @@ static void set_report(hid_device *dev, unsigned char *data, size_t length) EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetByteCallback callback, SetReportCallback set_report_callback), { let device = window._hidDeviceList[device_id]; - console.log("Opening device1 " + device_id); if (device) { - console.log("Opening device2 " + device_id); await device.open(); device.addEventListener("inputreport", function (event) { const { data, device, reportId } = event; - + let dataLength = data['byteLength']+1; let pointer = _malloc(dataLength); - dynCall("viiii", callback, [pointer, dataLength, report_id, 0]); + dynCall("viiii", callback, [pointer, dataLength, reportId, 0]); for (let i = 0; i < data['byteLength']; i++) { dynCall("viiii", callback, [pointer, dataLength, data['getUint8'](i), i+1]); } @@ -318,16 +308,22 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) hid_init(); /* register_global_error: global error is reset by hid_init */ +#ifdef HIDAPI_WEBHID_DEBUG printf("hid_open_path: %s\n", path); +#endif dev = new_hid_device(); if (!dev) { +#ifdef HIDAPI_WEBHID_DEBUG printf("hid_open_path: no memory\n"); +#endif return NULL; } if (sscanf(path, "hid%d", &device_id) != 1) { free(dev); +#ifdef HIDAPI_WEBHID_DEBUG printf("hid_open_path: invalid path\n"); +#endif return NULL; } @@ -342,7 +338,6 @@ EM_ASYNC_JS(int, hid_js_write, (int device_id, int report_id, const unsigned cha if (device) { let dataArray = new Uint8Array(length); for (let i = 0; i < length; i++) { - // console.log("hid_write: offset=" + i + " byte=" + HEAPU8[data+i]); dataArray[i] = HEAPU8[data+i]; } await device.sendReport(report_id, dataArray); @@ -362,8 +357,8 @@ EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_ if (device) { let [report_id, dataView] = await new Promise(function(resolve, reject) { device.addEventListener("inputreport", function (event) { - const { data, device, reportId } = event; - resolve([reportId, data]); // done + const { data, device, report_id } = event; + resolve([report_id, data]); // done }, {once: true}); }); dynCall("viiii", callback, [data, length, report_id, 0]); diff --git a/src/joystick/hidapi/SDL_hidapi_ps4.c b/src/joystick/hidapi/SDL_hidapi_ps4.c index dbdcdae3f2..62c9ff6273 100644 --- a/src/joystick/hidapi/SDL_hidapi_ps4.c +++ b/src/joystick/hidapi/SDL_hidapi_ps4.c @@ -1034,7 +1034,6 @@ static void HIDAPI_DriverPS4_HandleStatePacket(SDL_Joystick *joystick, SDL_hid_d SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, touchpad_down, touchpad_x * TOUCHPAD_SCALEX, touchpad_y * TOUCHPAD_SCALEY, touchpad_down ? 1.0f : 0.0f); } - printf("PS4 Packet usb1 %d\n", packet->rgucButtonsHatAndCounter[0]); if (ctx->last_state.rgucButtonsHatAndCounter[0] != packet->rgucButtonsHatAndCounter[0]) { { Uint8 data = (packet->rgucButtonsHatAndCounter[0] >> 4); @@ -1296,9 +1295,7 @@ static bool HIDAPI_DriverPS4_UpdateDevice(SDL_HIDAPI_Device *device) #ifdef DEBUG_PS4_PROTOCOL HIDAPI_DumpPacket("PS4 packet: size = %d", data, size); #endif - printf("PS4 Packet start\n"); if (!HIDAPI_DriverPS4_IsPacketValid(ctx, data, size)) { - printf("PS4 Packet invalid\n"); continue; } @@ -1306,13 +1303,11 @@ static bool HIDAPI_DriverPS4_UpdateDevice(SDL_HIDAPI_Device *device) ctx->last_packet = now; if (!joystick) { - printf("PS4 Packet no joystick\n"); continue; } switch (data[0]) { case k_EPS4ReportIdUsbState: - printf("PS4 Packet usb, packet[1]=%d\n", +data[1]); HIDAPI_DriverPS4_HandleStatePacket(joystick, device->dev, ctx, (PS4StatePacket_t *)&data[1], size - 1); break; case k_EPS4ReportIdBluetoothState1: diff --git a/src/joystick/hidapi/SDL_hidapi_rumble.c b/src/joystick/hidapi/SDL_hidapi_rumble.c index 7747b9dd4b..68ec667f41 100644 --- a/src/joystick/hidapi/SDL_hidapi_rumble.c +++ b/src/joystick/hidapi/SDL_hidapi_rumble.c @@ -49,12 +49,15 @@ typedef struct SDL_HIDAPI_RumbleContext SDL_HIDAPI_RumbleRequest *requests_tail; } SDL_HIDAPI_RumbleContext; +#ifndef SDL_PLATFORM_EMSCRIPTEN #ifndef SDL_THREAD_SAFETY_ANALYSIS static #endif SDL_Mutex *SDL_HIDAPI_rumble_lock; +#endif static SDL_HIDAPI_RumbleContext rumble_context SDL_GUARDED_BY(SDL_HIDAPI_rumble_lock); +#ifndef SDL_PLATFORM_EMSCRIPTEN // Threads are not used for the web static int SDLCALL SDL_HIDAPI_RumbleThread(void *data) { SDL_HIDAPI_RumbleContext *ctx = (SDL_HIDAPI_RumbleContext *)data; @@ -161,9 +164,11 @@ static bool SDL_HIDAPI_StartRumbleThread(SDL_HIDAPI_RumbleContext *ctx) } return true; } +#endif bool SDL_HIDAPI_LockRumble(void) { +#ifndef SDL_PLATFORM_EMSCRIPTEN SDL_HIDAPI_RumbleContext *ctx = &rumble_context; if (SDL_CompareAndSwapAtomicInt(&ctx->initialized, false, true)) { @@ -173,11 +178,13 @@ bool SDL_HIDAPI_LockRumble(void) } SDL_LockMutex(SDL_HIDAPI_rumble_lock); +#endif return true; } bool SDL_HIDAPI_GetPendingRumbleLocked(SDL_HIDAPI_Device *device, Uint8 **data, int **size, int *maximum_size) { +#ifndef SDL_PLATFORM_EMSCRIPTEN SDL_HIDAPI_RumbleContext *ctx = &rumble_context; SDL_HIDAPI_RumbleRequest *request, *found; @@ -193,6 +200,7 @@ bool SDL_HIDAPI_GetPendingRumbleLocked(SDL_HIDAPI_Device *device, Uint8 **data, *maximum_size = sizeof(found->data); return true; } +#endif return false; } @@ -212,6 +220,7 @@ int SDL_HIDAPI_SendRumbleWithCallbackAndUnlock(SDL_HIDAPI_Device *device, const return -1; } +#ifndef SDL_PLATFORM_EMSCRIPTEN request = (SDL_HIDAPI_RumbleRequest *)SDL_calloc(1, sizeof(*request)); if (!request) { SDL_HIDAPI_UnlockRumble(); @@ -234,6 +243,9 @@ int SDL_HIDAPI_SendRumbleWithCallbackAndUnlock(SDL_HIDAPI_Device *device, const // Make sure we unlock before posting the semaphore so the rumble thread can run immediately SDL_HIDAPI_UnlockRumble(); +#else + SDL_hid_write(device->dev, data, size); +#endif SDL_SignalSemaphore(ctx->request_sem); @@ -242,7 +254,9 @@ int SDL_HIDAPI_SendRumbleWithCallbackAndUnlock(SDL_HIDAPI_Device *device, const void SDL_HIDAPI_UnlockRumble(void) { +#ifndef SDL_PLATFORM_EMSCRIPTEN SDL_UnlockMutex(SDL_HIDAPI_rumble_lock); +#endif } int SDL_HIDAPI_SendRumble(SDL_HIDAPI_Device *device, const Uint8 *data, int size) @@ -273,11 +287,13 @@ int SDL_HIDAPI_SendRumble(SDL_HIDAPI_Device *device, const Uint8 *data, int size void SDL_HIDAPI_QuitRumble(void) { +#ifndef SDL_PLATFORM_EMSCRIPTEN SDL_HIDAPI_RumbleContext *ctx = &rumble_context; if (SDL_GetAtomicInt(&ctx->running)) { SDL_HIDAPI_StopRumbleThread(ctx); } +#endif } #endif // SDL_JOYSTICK_HIDAPI diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index 8fa0fc66e9..a45c64c63f 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -642,6 +642,7 @@ void HIDAPI_SetDeviceProduct(SDL_HIDAPI_Device *device, Uint16 vendor_id, Uint16 device->guid = SDL_CreateJoystickGUID(device->guid.data[0], vendor_id, product_id, device->version, device->manufacturer_string, device->product_string, 'h', 0); } +#ifndef SDL_PLATFORM_EMSCRIPTEN static void HIDAPI_UpdateJoystickSerial(SDL_HIDAPI_Device *device) { int i; @@ -656,6 +657,7 @@ static void HIDAPI_UpdateJoystickSerial(SDL_HIDAPI_Device *device) } } } +#endif static bool HIDAPI_SerialIsEmpty(SDL_HIDAPI_Device *device) { @@ -675,13 +677,18 @@ static bool HIDAPI_SerialIsEmpty(SDL_HIDAPI_Device *device) void HIDAPI_SetDeviceSerial(SDL_HIDAPI_Device *device, const char *serial) { +#ifdef SDL_PLATFORM_EMSCRIPTEN + // Don't include the serial number for the web to decrease the fingerprinting surface +#else if (serial && *serial && (!device->serial || SDL_strcmp(serial, device->serial) != 0)) { SDL_free(device->serial); device->serial = SDL_strdup(serial); HIDAPI_UpdateJoystickSerial(device); } +#endif } +#ifndef SDL_PLATFORM_EMSCRIPTEN static int wcstrcmp(const wchar_t *str1, const char *str2) { int result; @@ -696,14 +703,19 @@ static int wcstrcmp(const wchar_t *str1, const char *str2) } return result; } +#endif static void HIDAPI_SetDeviceSerialW(SDL_HIDAPI_Device *device, const wchar_t *serial) { +#ifdef SDL_PLATFORM_EMSCRIPTEN + // Don't include the serial number for the web to decrease the fingerprinting surface +#else if (serial && *serial && (!device->serial || wcstrcmp(serial, device->serial) != 0)) { SDL_free(device->serial); device->serial = HIDAPI_ConvertString(serial); HIDAPI_UpdateJoystickSerial(device); } +#endif } bool HIDAPI_HasConnectedUSBDevice(const char *serial) From c52bf5188fb218010af6211d8f4793a553111647 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 21 May 2026 21:12:08 +0500 Subject: [PATCH 03/13] [skip ci] Disconnect gamepad on device access granted --- src/joystick/emscripten/SDL_sysjoystick.c | 34 +++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index c7318ea98e..d488fa7538 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -121,14 +121,35 @@ static int SDL_GetEmscriptenOSID() }); } +static EM_BOOL Emscripten_JoyStickDisconnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData); + #ifdef SDL_JOYSTICK_HIDAPI -static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product) + +static void SDL_WebHID_DisconnectEmscriptenGamepad(int device_index) +{ + int rc; + EmscriptenGamepadEvent gamepadState; + rc = emscripten_get_gamepad_status(device_index, &gamepadState); + if (rc == EMSCRIPTEN_RESULT_SUCCESS) { + Emscripten_JoyStickDisconnected(EMSCRIPTEN_EVENT_GAMEPADDISCONNECTED, + &gamepadState, + NULL); + } +} + +static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product, int device_index) { MAIN_THREAD_EM_ASM({ if ("hid" in navigator) { - navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); + async function handler() { + let devices = await navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); + if (devices) { + dynCall("vi", $2, [$3]); + } + } + handler(); } - }, vendor, product); + }, vendor, product, SDL_WebHID_DisconnectEmscriptenGamepad, device_index); } #endif @@ -162,10 +183,7 @@ static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamep #ifdef SDL_JOYSTICK_HIDAPI if (HIDAPI_IsDeviceSupported(vendor, product, 0, "")) { - SDL_RequestWebHIDDevice(vendor, product); - printf("HIDAPI_IsDeviceSupported\n"); - SDL_free(item); - goto done; + SDL_RequestWebHIDDevice(vendor, product, gamepadEvent->index); } #endif @@ -308,7 +326,7 @@ done: return 1; } -static EM_BOOL Emscripten_JoyStickDisconnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) +EM_BOOL Emscripten_JoyStickDisconnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) { SDL_joylist_item *item = SDL_joylist; SDL_joylist_item *prev = NULL; From 9ae773bcbcd0a3d6627c57e45ce95d39766636b7 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 21 May 2026 22:26:52 +0500 Subject: [PATCH 04/13] [skip ci] Wait until the user interacts with the page --- src/joystick/emscripten/SDL_sysjoystick.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index d488fa7538..ed39b31b43 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -140,11 +140,24 @@ static void SDL_WebHID_DisconnectEmscriptenGamepad(int device_index) static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product, int device_index) { MAIN_THREAD_EM_ASM({ + function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + if ("hid" in navigator) { async function handler() { - let devices = await navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); - if (devices) { - dynCall("vi", $2, [$3]); + while (true) { + try { + let devices = await navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); + if (devices) { + dynCall("vi", $2, [$3]); + } + return; + } catch(e) { + // Exception, most likely because the user hasn't interacted with the page yet. + // Let's wait until they do, hopefully. + await timeout(500); + } } } handler(); From 85fc76b089a09937844ec5e38e3aa4088203651e Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 21 May 2026 22:30:55 +0500 Subject: [PATCH 05/13] [skip ci] Disable WebHID HIDAPI by default --- src/joystick/hidapi/SDL_hidapijoystick_c.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h index 9e7be455de..c30102bea0 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick_c.h +++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h @@ -63,8 +63,12 @@ #if defined(SDL_PLATFORM_ANDROID) || \ defined(SDL_PLATFORM_IOS) || \ defined(SDL_PLATFORM_TVOS) || \ - defined(SDL_PLATFORM_VISIONOS) -// On Android, HIDAPI prompts for permissions and acquires exclusive access to the device, and on Apple mobile platforms it doesn't do anything except for handling Bluetooth Steam Controllers, so we'll leave it off by default. + defined(SDL_PLATFORM_VISIONOS) || \ + defined(SDL_PLATFORM_EMSCRIPTEN) +// On Android, HIDAPI prompts for permissions and acquires exclusive access to the device, +// on Emscripten, it also prompts for permissions and it's not available in some browsers, +// and on Apple mobile platforms it doesn't do anything except for handling Bluetooth Steam Controllers, +// so we'll leave it off by default. #define SDL_HIDAPI_DEFAULT false #else #define SDL_HIDAPI_DEFAULT true From b0823349bbb986b2d42fa8aee99a2ca7ace710fb Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 21 May 2026 23:16:53 +0500 Subject: [PATCH 06/13] [skip ci] Fix hidapi gamepad ignoring --- src/joystick/emscripten/SDL_sysjoystick.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index ed39b31b43..3707386222 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -149,7 +149,7 @@ static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product, int device_in while (true) { try { let devices = await navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); - if (devices) { + if (devices["length"]) { dynCall("vi", $2, [$3]); } return; From 5936ee00dd19bc669a11b22f66838b4238f7de80 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 21 May 2026 23:17:16 +0500 Subject: [PATCH 07/13] [skip ci] Fix joycon handling --- src/hidapi/emscripten/hid.c | 28 +++++++++++++---------- src/joystick/emscripten/SDL_sysjoystick.c | 12 +++++++++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index b66d672517..a97815edf2 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -285,18 +285,22 @@ static void set_report(hid_device *dev, unsigned char *data, size_t length) EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetByteCallback callback, SetReportCallback set_report_callback), { let device = window._hidDeviceList[device_id]; if (device) { - await device.open(); - device.addEventListener("inputreport", function (event) { - const { data, device, reportId } = event; - - let dataLength = data['byteLength']+1; - let pointer = _malloc(dataLength); - dynCall("viiii", callback, [pointer, dataLength, reportId, 0]); - for (let i = 0; i < data['byteLength']; i++) { - dynCall("viiii", callback, [pointer, dataLength, data['getUint8'](i), i+1]); - } - dynCall("viii", set_report_callback, [dev, pointer, dataLength]); - }); + try { + await device.open(); + device.addEventListener("inputreport", function (event) { + const { data, device, reportId } = event; + + let dataLength = data['byteLength']+1; + let pointer = _malloc(dataLength); + dynCall("viiii", callback, [pointer, dataLength, reportId, 0]); + for (let i = 0; i < data['byteLength']; i++) { + dynCall("viiii", callback, [pointer, dataLength, data['getUint8'](i), i+1]); + } + dynCall("viii", set_report_callback, [dev, pointer, dataLength]); + }); + } catch (e) { + // Pass? + } } }); diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index 3707386222..1d8e894ee4 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -139,6 +139,11 @@ static void SDL_WebHID_DisconnectEmscriptenGamepad(int device_index) static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product, int device_index) { + Uint16 product2 = 0; + if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_GRIP) { + product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT; + product2 = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT; + } MAIN_THREAD_EM_ASM({ function timeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -149,6 +154,11 @@ static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product, int device_in while (true) { try { let devices = await navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); + let device_length = devices["length"]; + if ($4) { // product2 + devices = await navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $4, } ]}); + device_length += devices["length"]; + } if (devices["length"]) { dynCall("vi", $2, [$3]); } @@ -162,7 +172,7 @@ static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product, int device_in } handler(); } - }, vendor, product, SDL_WebHID_DisconnectEmscriptenGamepad, device_index); + }, vendor, product, SDL_WebHID_DisconnectEmscriptenGamepad, device_index, product2); } #endif From 1f7247c9951653b7c778162209f3dffe668a3bf4 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Wed, 27 May 2026 22:49:33 +0500 Subject: [PATCH 08/13] [skip ci] Remove `set_byte` --- src/hidapi/emscripten/hid.c | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index a97815edf2..5e8b985755 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -160,7 +160,6 @@ end: return root; } -// TODO: remove all EM_ASYNC_JS for non-blocking operations EM_ASYNC_JS(int, hid_js_get_device_count, (), { window._hidDeviceList = await navigator.hid.getDevices(); return window._hidDeviceList.length; @@ -259,19 +258,8 @@ hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const return handle; } -typedef void(*SetByteCallback)(unsigned char *data, size_t length, int byte, size_t offset); typedef void(*SetReportCallback)(hid_device *dev, unsigned char *data, size_t length); -static void set_byte(unsigned char *data, size_t length, int byte, size_t offset) -{ - if (offset >= length) - return; - data[offset] = (unsigned char)byte; -#ifdef HIDAPI_WEBHID_DEBUG - // printf("set_byte: offset=%d byte=%d\n", (int)offset, byte); -#endif -} - static void set_report(hid_device *dev, unsigned char *data, size_t length) { if (dev->last_report) { @@ -282,7 +270,7 @@ static void set_report(hid_device *dev, unsigned char *data, size_t length) dev->last_report_read = 0; } -EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetByteCallback callback, SetReportCallback set_report_callback), { +EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetReportCallback set_report_callback), { let device = window._hidDeviceList[device_id]; if (device) { try { @@ -292,9 +280,9 @@ EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetByteCallback let dataLength = data['byteLength']+1; let pointer = _malloc(dataLength); - dynCall("viiii", callback, [pointer, dataLength, reportId, 0]); + HEAPU8[pointer] = reportId; for (let i = 0; i < data['byteLength']; i++) { - dynCall("viiii", callback, [pointer, dataLength, data['getUint8'](i), i+1]); + HEAPU8[pointer + i + 1] = data['getUint8'](i); } dynCall("viii", set_report_callback, [dev, pointer, dataLength]); }); @@ -332,7 +320,7 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) } dev->device_id = device_id; - hid_js_open(device_id, dev, set_byte, set_report); + hid_js_open(device_id, dev, set_report); return dev; } @@ -356,7 +344,7 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t return length; } -EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_t length, SetByteCallback callback), { +EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_t length), { let device = window._hidDeviceList[device_id]; if (device) { let [report_id, dataView] = await new Promise(function(resolve, reject) { @@ -365,9 +353,9 @@ EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_ resolve([report_id, data]); // done }, {once: true}); }); - dynCall("viiii", callback, [data, length, report_id, 0]); + HEAPU8[data] = report_id; for (let i = 0; i < dataView['byteLength']; i++) { - dynCall("viiii", callback, [data, length, dataView['getUint8'](i), i+1]); + HEAPU8[data + i + 1] = dataView['getUint8'](i); } return dataView['byteLength']+1; } @@ -392,7 +380,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t return 0; } } - return hid_js_read_timeout(dev->device_id, data, length, &set_byte); + return hid_js_read_timeout(dev->device_id, data, length); } int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) @@ -412,12 +400,12 @@ int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char return 0; } -EM_ASYNC_JS(void, hid_js_get_feature_report, (int device_id, int report_id, unsigned char *data, size_t length, SetByteCallback callback), { +EM_ASYNC_JS(void, hid_js_get_feature_report, (int device_id, int report_id, unsigned char *data, size_t length), { let device = window._hidDeviceList[device_id]; if (device) { let dataView = await device['receiveFeatureReport'](report_id); for (let i = 0; i < dataView['byteLength']; i++) { - dynCall("viiii", callback, [data, length, dataView['getUint8'](i), i]); + HEAPU8[data + i] = dataView['getUint8'](i); } } }); @@ -427,7 +415,7 @@ int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, if (length < 1) return -1; int report_id = (int)data[0]; - hid_js_get_feature_report(dev->device_id, report_id, data, length, &set_byte); + hid_js_get_feature_report(dev->device_id, report_id, data, length); return 0; } From 1b714eeb363eacf32877ec1188d6e4d7b1dd2354 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Wed, 27 May 2026 22:50:44 +0500 Subject: [PATCH 09/13] [skip ci] Remove `set_report` --- src/hidapi/emscripten/hid.c | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index 5e8b985755..f09fc721bc 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -25,7 +25,7 @@ #include #include -EM_JS_DEPS(hidapi, "$dynCall"); +// EM_JS_DEPS(hidapi, "$dynCall"); #if 0 #define HIDAPI_WEBHID_DEBUG @@ -35,7 +35,6 @@ struct hid_device_ { int device_id; unsigned char *last_report; size_t last_report_length; - int last_report_read; struct hid_device_info* device_info; }; @@ -55,7 +54,6 @@ static hid_device *new_hid_device(void) dev->device_id = -1; dev->last_report = NULL; dev->last_report_length = 0; - dev->last_report_read = 0; return dev; } @@ -258,19 +256,7 @@ hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const return handle; } -typedef void(*SetReportCallback)(hid_device *dev, unsigned char *data, size_t length); - -static void set_report(hid_device *dev, unsigned char *data, size_t length) -{ - if (dev->last_report) { - free(dev->last_report); - } - dev->last_report = data; - dev->last_report_length = length; - dev->last_report_read = 0; -} - -EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetReportCallback set_report_callback), { +EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, unsigned char **last_report_out, size_t *last_report_length_out), { let device = window._hidDeviceList[device_id]; if (device) { try { @@ -284,7 +270,12 @@ EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetReportCallbac for (let i = 0; i < data['byteLength']; i++) { HEAPU8[pointer + i + 1] = data['getUint8'](i); } - dynCall("viii", set_report_callback, [dev, pointer, dataLength]); + + if (HEAP32[last_report_out >> 2] != 0) { + _free(HEAP32[last_report_out >> 2]); + } + HEAP32[last_report_out >> 2] = pointer; + HEAP32[last_report_length_out >> 2] = dataLength; }); } catch (e) { // Pass? @@ -320,7 +311,7 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) } dev->device_id = device_id; - hid_js_open(device_id, dev, set_report); + hid_js_open(device_id, dev, &dev->last_report, &dev->last_report_length); return dev; } @@ -368,13 +359,14 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t if (length < 1) return -1; if (milliseconds == 0) { - if (dev->last_report && !dev->last_report_read) { + if (dev->last_report) { size_t return_size = length; if (dev->last_report_length < length) { return_size = dev->last_report_length; } memcpy(data, dev->last_report, return_size); - dev->last_report_read = 1; + free(dev->last_report); + dev->last_report = NULL; return return_size; } else { return 0; From f1f69612e2e6d2bc698488828b6b42ceb3f6abbe Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Wed, 27 May 2026 23:03:32 +0500 Subject: [PATCH 10/13] [skip ci] Return dummy values if WebHID is unsupported --- src/hidapi/emscripten/hid.c | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index f09fc721bc..e608f6e06c 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -44,6 +44,9 @@ static struct hid_api_version api_version = { .patch = HID_API_VERSION_PATCH }; +static int initialized = 0; +static int webhid_supported = 0; + static hid_device *new_hid_device(void) { hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); @@ -70,9 +73,12 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) int HID_API_EXPORT hid_init(void) { - return MAIN_THREAD_EM_ASM_INT({ - return "hid" in navigator ? 0 : -1; - }); + if (!initialized) { + webhid_supported = MAIN_THREAD_EM_ASM_INT({ + return "hid" in navigator; + }); + } + return webhid_supported ? 0 : -1; } int HID_API_EXPORT hid_exit(void) @@ -169,7 +175,9 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - hid_init(); + if (hid_init() < 0) { + return NULL; + } device_count = hid_js_get_device_count(); @@ -288,7 +296,9 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) hid_device *dev = NULL; int device_id = 0; - hid_init(); + if (hid_init() < 0) { + return NULL; + } /* register_global_error: global error is reset by hid_init */ #ifdef HIDAPI_WEBHID_DEBUG @@ -329,7 +339,7 @@ EM_ASYNC_JS(int, hid_js_write, (int device_id, int report_id, const unsigned cha int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) { - if (length < 1) + if (!webhid_supported || length < 1) return 0; hid_js_write(dev->device_id, data[0], data+1, length-1); return length; @@ -356,7 +366,7 @@ EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) { /* TODO: timeout */ - if (length < 1) + if (!webhid_supported || length < 1) return -1; if (milliseconds == 0) { if (dev->last_report) { @@ -404,7 +414,7 @@ EM_ASYNC_JS(void, hid_js_get_feature_report, (int device_id, int report_id, unsi int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) { - if (length < 1) + if (!webhid_supported || length < 1) return -1; int report_id = (int)data[0]; hid_js_get_feature_report(dev->device_id, report_id, data, length); @@ -468,7 +478,7 @@ int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char return 0; } -HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) { return L""; } \ No newline at end of file From 3b6569ad035ba858911da1872f82d4b2765ba540 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Wed, 27 May 2026 23:14:26 +0500 Subject: [PATCH 11/13] [skip ci] `product_string` --- src/hidapi/emscripten/hid.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index e608f6e06c..677d0c6ff0 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -25,7 +25,7 @@ #include #include -// EM_JS_DEPS(hidapi, "$dynCall"); +EM_JS_DEPS(hidapi, "$stringToUTF32"); #if 0 #define HIDAPI_WEBHID_DEBUG @@ -91,7 +91,7 @@ static struct hid_device_info * create_device_info_for_device(int device_id) struct hid_device_info *root = NULL; struct hid_device_info *cur_dev = NULL; char path[16]; - /*wchar_t product_string[128];*/ + wchar_t product_string[128]; int ignore = 0; int input_report_count; @@ -127,14 +127,11 @@ static struct hid_device_info * create_device_info_for_device(int device_id) cur_dev->release_number = 0; cur_dev->manufacturer_string = NULL; - /* I'm too tired to make stringToUTF32 work correctly here, cmake doesn't want to include it in the build */ - /*MAIN_THREAD_EM_ASM({ + MAIN_THREAD_EM_ASM({ stringToUTF32(window._hidDeviceList[$0].productName, $1, Number($2)); }, device_id, product_string, sizeof(product_string)); - cur_dev->product_string = wcsdup(product_string);*/ - - cur_dev->product_string = NULL; + cur_dev->product_string = wcsdup(product_string); cur_dev->usage_page = MAIN_THREAD_EM_ASM_INT({ return window._hidDeviceList[$0].collections[0].usagePage; @@ -216,9 +213,7 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) while (d) { struct hid_device_info *next = d->next; free(d->path); - /*free(d->serial_number); - free(d->manufacturer_string); - free(d->product_string);*/ + free(d->product_string); free(d); d = next; } From c6502d9f8fd34318943b7c6c31754f1d72b46c9d Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 28 May 2026 00:03:03 +0500 Subject: [PATCH 12/13] [skip ci] Fix `hid_js_read_timeout` crash --- src/hidapi/emscripten/hid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c index 677d0c6ff0..99466b2391 100644 --- a/src/hidapi/emscripten/hid.c +++ b/src/hidapi/emscripten/hid.c @@ -350,7 +350,7 @@ EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_ }, {once: true}); }); HEAPU8[data] = report_id; - for (let i = 0; i < dataView['byteLength']; i++) { + for (let i = 0; i < dataView['byteLength'] && i < (length-1); i++) { HEAPU8[data + i + 1] = dataView['getUint8'](i); } return dataView['byteLength']+1; From 3eb59cb7a97fe726fd177e48d3ca6ed699ba3f91 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 28 May 2026 00:03:32 +0500 Subject: [PATCH 13/13] [skip ci] Code cleanup --- src/joystick/hidapi/SDL_hidapi_rumble.c | 14 +++++++------- src/joystick/hidapi/SDL_hidapijoystick.c | 12 ++++-------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_rumble.c b/src/joystick/hidapi/SDL_hidapi_rumble.c index 68ec667f41..4afbb70394 100644 --- a/src/joystick/hidapi/SDL_hidapi_rumble.c +++ b/src/joystick/hidapi/SDL_hidapi_rumble.c @@ -49,7 +49,7 @@ typedef struct SDL_HIDAPI_RumbleContext SDL_HIDAPI_RumbleRequest *requests_tail; } SDL_HIDAPI_RumbleContext; -#ifndef SDL_PLATFORM_EMSCRIPTEN +#ifndef SDL_THREADS_DISABLED #ifndef SDL_THREAD_SAFETY_ANALYSIS static #endif @@ -57,7 +57,7 @@ SDL_Mutex *SDL_HIDAPI_rumble_lock; #endif static SDL_HIDAPI_RumbleContext rumble_context SDL_GUARDED_BY(SDL_HIDAPI_rumble_lock); -#ifndef SDL_PLATFORM_EMSCRIPTEN // Threads are not used for the web +#ifndef SDL_THREADS_DISABLED static int SDLCALL SDL_HIDAPI_RumbleThread(void *data) { SDL_HIDAPI_RumbleContext *ctx = (SDL_HIDAPI_RumbleContext *)data; @@ -168,7 +168,7 @@ static bool SDL_HIDAPI_StartRumbleThread(SDL_HIDAPI_RumbleContext *ctx) bool SDL_HIDAPI_LockRumble(void) { -#ifndef SDL_PLATFORM_EMSCRIPTEN +#ifndef SDL_THREADS_DISABLED SDL_HIDAPI_RumbleContext *ctx = &rumble_context; if (SDL_CompareAndSwapAtomicInt(&ctx->initialized, false, true)) { @@ -184,7 +184,7 @@ bool SDL_HIDAPI_LockRumble(void) bool SDL_HIDAPI_GetPendingRumbleLocked(SDL_HIDAPI_Device *device, Uint8 **data, int **size, int *maximum_size) { -#ifndef SDL_PLATFORM_EMSCRIPTEN +#ifndef SDL_THREADS_DISABLED SDL_HIDAPI_RumbleContext *ctx = &rumble_context; SDL_HIDAPI_RumbleRequest *request, *found; @@ -220,7 +220,7 @@ int SDL_HIDAPI_SendRumbleWithCallbackAndUnlock(SDL_HIDAPI_Device *device, const return -1; } -#ifndef SDL_PLATFORM_EMSCRIPTEN +#ifndef SDL_THREADS_DISABLED request = (SDL_HIDAPI_RumbleRequest *)SDL_calloc(1, sizeof(*request)); if (!request) { SDL_HIDAPI_UnlockRumble(); @@ -254,7 +254,7 @@ int SDL_HIDAPI_SendRumbleWithCallbackAndUnlock(SDL_HIDAPI_Device *device, const void SDL_HIDAPI_UnlockRumble(void) { -#ifndef SDL_PLATFORM_EMSCRIPTEN +#ifndef SDL_THREADS_DISABLED SDL_UnlockMutex(SDL_HIDAPI_rumble_lock); #endif } @@ -287,7 +287,7 @@ int SDL_HIDAPI_SendRumble(SDL_HIDAPI_Device *device, const Uint8 *data, int size void SDL_HIDAPI_QuitRumble(void) { -#ifndef SDL_PLATFORM_EMSCRIPTEN +#ifndef SDL_THREADS_DISABLED SDL_HIDAPI_RumbleContext *ctx = &rumble_context; if (SDL_GetAtomicInt(&ctx->running)) { diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index a45c64c63f..e54f72337f 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -642,7 +642,6 @@ void HIDAPI_SetDeviceProduct(SDL_HIDAPI_Device *device, Uint16 vendor_id, Uint16 device->guid = SDL_CreateJoystickGUID(device->guid.data[0], vendor_id, product_id, device->version, device->manufacturer_string, device->product_string, 'h', 0); } -#ifndef SDL_PLATFORM_EMSCRIPTEN static void HIDAPI_UpdateJoystickSerial(SDL_HIDAPI_Device *device) { int i; @@ -657,7 +656,6 @@ static void HIDAPI_UpdateJoystickSerial(SDL_HIDAPI_Device *device) } } } -#endif static bool HIDAPI_SerialIsEmpty(SDL_HIDAPI_Device *device) { @@ -679,16 +677,15 @@ void HIDAPI_SetDeviceSerial(SDL_HIDAPI_Device *device, const char *serial) { #ifdef SDL_PLATFORM_EMSCRIPTEN // Don't include the serial number for the web to decrease the fingerprinting surface -#else + serial = NULL; +#endif if (serial && *serial && (!device->serial || SDL_strcmp(serial, device->serial) != 0)) { SDL_free(device->serial); device->serial = SDL_strdup(serial); HIDAPI_UpdateJoystickSerial(device); } -#endif } -#ifndef SDL_PLATFORM_EMSCRIPTEN static int wcstrcmp(const wchar_t *str1, const char *str2) { int result; @@ -703,19 +700,18 @@ static int wcstrcmp(const wchar_t *str1, const char *str2) } return result; } -#endif static void HIDAPI_SetDeviceSerialW(SDL_HIDAPI_Device *device, const wchar_t *serial) { #ifdef SDL_PLATFORM_EMSCRIPTEN // Don't include the serial number for the web to decrease the fingerprinting surface -#else + serial = NULL; +#endif if (serial && *serial && (!device->serial || wcstrcmp(serial, device->serial) != 0)) { SDL_free(device->serial); device->serial = HIDAPI_ConvertString(serial); HIDAPI_UpdateJoystickSerial(device); } -#endif } bool HIDAPI_HasConnectedUSBDevice(const char *serial)