From 5be888591cc46ab685ea67ff065b5ff9cac51f35 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Thu, 2 Apr 2026 19:17:02 -0700 Subject: [PATCH] emscripten: Fix navigator.getGamepads crash in worker threads The three EM_JS functions (SDL_GetEmscriptenJoystickVendor, SDL_GetEmscriptenJoystickProduct, SDL_IsEmscriptenJoystickXInput) call navigator.getGamepads() which is only available on the main browser thread. With PROXY_TO_PTHREAD, the joystick callbacks are dispatched to a worker where the Gamepad API is not available, causing a TypeError. Convert these from EM_JS to static functions using MAIN_THREAD_EM_ASM_INT, which proxies the JavaScript execution to the main browser thread. This matches the pattern already used by other navigator.getGamepads() calls in the same file. (cherry picked from commit be8643f739c3a70f459d6dfb9b7bcfd09bc5fea9) --- src/joystick/emscripten/SDL_sysjoystick.c | 83 +++++++++++++---------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index 42f12b033d..156917e682 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -35,53 +35,62 @@ static SDL_joylist_item *SDL_joylist = NULL; static SDL_joylist_item *SDL_joylist_tail = NULL; static int numjoysticks = 0; -EM_JS(int, SDL_GetEmscriptenJoystickVendor, (int device_index), { +static int SDL_GetEmscriptenJoystickVendor(int device_index) +{ // Let's assume that if we're calling these function then the gamepad object definitely exists - let gamepad = navigator['getGamepads']()[device_index]; + return MAIN_THREAD_EM_ASM_INT({ + let gamepad = navigator['getGamepads']()[$0]; - // Chrome, Edge, Opera: Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc) - let vendor_str = 'Vendor: '; - if (gamepad['id']['indexOf'](vendor_str) > 0) { - let vendor_str_index = gamepad['id']['indexOf'](vendor_str) + vendor_str['length']; - return parseInt(gamepad['id']['substr'](vendor_str_index, 4), 16); - } + // Chrome, Edge, Opera: Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc) + let vendor_str = 'Vendor: '; + if (gamepad['id']['indexOf'](vendor_str) > 0) { + let vendor_str_index = gamepad['id']['indexOf'](vendor_str) + vendor_str['length']; + return parseInt(gamepad['id']['substr'](vendor_str_index, 4), 16); + } - // Firefox, Safari: 046d-c216-Logitech Dual Action (or 46d-c216-Logicool Dual Action) - let id_split = gamepad['id']['split']('-'); - if (id_split['length'] > 1 && !isNaN(parseInt(id_split[0], 16))) { - return parseInt(id_split[0], 16); - } + // Firefox, Safari: 046d-c216-Logitech Dual Action (or 46d-c216-Logicool Dual Action) + let id_split = gamepad['id']['split']('-'); + if (id_split['length'] > 1 && !isNaN(parseInt(id_split[0], 16))) { + return parseInt(id_split[0], 16); + } - return 0; -}); + return 0; + }, device_index); +} -EM_JS(int, SDL_GetEmscriptenJoystickProduct, (int device_index), { - let gamepad = navigator['getGamepads']()[device_index]; +static int SDL_GetEmscriptenJoystickProduct(int device_index) +{ + return MAIN_THREAD_EM_ASM_INT({ + let gamepad = navigator['getGamepads']()[$0]; - // Chrome, Edge, Opera: Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc) - let product_str = 'Product: '; - if (gamepad['id']['indexOf'](product_str) > 0) { - let product_str_index = gamepad['id']['indexOf'](product_str) + product_str['length']; - return parseInt(gamepad['id']['substr'](product_str_index, 4), 16); - } + // Chrome, Edge, Opera: Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc) + let product_str = 'Product: '; + if (gamepad['id']['indexOf'](product_str) > 0) { + let product_str_index = gamepad['id']['indexOf'](product_str) + product_str['length']; + return parseInt(gamepad['id']['substr'](product_str_index, 4), 16); + } - // Firefox, Safari: 046d-c216-Logitech Dual Action (or 46d-c216-Logicool Dual Action) - let id_split = gamepad['id']['split']('-'); - if (id_split['length'] > 1 && !isNaN(parseInt(id_split[1], 16))) { - return parseInt(id_split[1], 16); - } + // Firefox, Safari: 046d-c216-Logitech Dual Action (or 46d-c216-Logicool Dual Action) + let id_split = gamepad['id']['split']('-'); + if (id_split['length'] > 1 && !isNaN(parseInt(id_split[1], 16))) { + return parseInt(id_split[1], 16); + } - return 0; -}); + return 0; + }, device_index); +} -EM_JS(int, SDL_IsEmscriptenJoystickXInput, (int device_index), { - let gamepad = navigator['getGamepads']()[device_index]; +static int SDL_IsEmscriptenJoystickXInput(int device_index) +{ + return MAIN_THREAD_EM_ASM_INT({ + let gamepad = navigator['getGamepads']()[$0]; - // Chrome, Edge, Opera: Xbox 360 Controller (XInput STANDARD GAMEPAD) - // Firefox: xinput - // TODO: Safari - return gamepad['id']['toLowerCase']()['indexOf']('xinput') >= 0; -}); + // Chrome, Edge, Opera: Xbox 360 Controller (XInput STANDARD GAMEPAD) + // Firefox: xinput + // TODO: Safari + return gamepad['id']['toLowerCase']()['indexOf']('xinput') >= 0; + }, device_index); +} static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) {