From d5277c187eef81879577b4d8e6f7f884e4c899ef Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Wed, 24 Dec 2025 19:20:59 +0500 Subject: [PATCH] SDL joystick input for web This commit also adds joystick vibration feature to the web platform in browsers that support this feature. --- doc/classes/Input.xml | 10 +- drivers/SCsub | 4 +- drivers/sdl/SCsub | 17 + drivers/sdl/SDL_build_config_private.h | 15 + drivers/sdl/joypad_sdl.cpp | 30 + drivers/sdl/joypad_sdl.h | 1 + platform/web/detect.py | 7 + platform/web/display_server_web.cpp | 67 -- platform/web/display_server_web.h | 5 - platform/web/godot_js.h | 5 - platform/web/js/libs/library_godot_input.js | 180 +---- platform/web/os_web.cpp | 22 + platform/web/os_web.h | 6 + thirdparty/sdl/haptic/dummy/SDL_syshaptic.c | 151 ++++ thirdparty/sdl/joystick/SDL_gamepad_db.h | 2 +- .../sdl/joystick/emscripten/SDL_sysjoystick.c | 644 ++++++++++++++++++ .../joystick/emscripten/SDL_sysjoystick_c.h | 55 ++ thirdparty/sdl/loadso/dummy/SDL_sysloadso.c | 45 ++ .../patches/0007-emscripten-joystick.patch | 341 ++++++++++ thirdparty/sdl/thread/generic/SDL_sysmutex.c | 132 ++++ .../sdl/thread/generic/SDL_sysmutex_c.h | 21 + thirdparty/sdl/thread/generic/SDL_systhread.c | 57 ++ thirdparty/sdl/thread/generic/SDL_systls.c | 44 ++ thirdparty/sdl/update-sdl.sh | 12 +- 24 files changed, 1602 insertions(+), 271 deletions(-) create mode 100644 thirdparty/sdl/haptic/dummy/SDL_syshaptic.c create mode 100644 thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c create mode 100644 thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h create mode 100644 thirdparty/sdl/loadso/dummy/SDL_sysloadso.c create mode 100644 thirdparty/sdl/patches/0007-emscripten-joystick.patch create mode 100644 thirdparty/sdl/thread/generic/SDL_sysmutex.c create mode 100644 thirdparty/sdl/thread/generic/SDL_sysmutex_c.h create mode 100644 thirdparty/sdl/thread/generic/SDL_systhread.c create mode 100644 thirdparty/sdl/thread/generic/SDL_systls.c diff --git a/doc/classes/Input.xml b/doc/classes/Input.xml index 805576f17014..fbbc75db674f 100644 --- a/doc/classes/Input.xml +++ b/doc/classes/Input.xml @@ -130,14 +130,14 @@ Returns a dictionary with extra platform-specific information about the device, e.g. the raw gamepad name from the OS or the Steam Input index. - On Windows, Linux, and macOS, the dictionary contains the following fields: + On Windows, Linux, macOS, and Web, the dictionary contains the following fields: [code]raw_name[/code]: The name of the controller as it came from the OS, before getting renamed by the controller database. [code]vendor_id[/code]: The USB vendor ID of the device. [code]product_id[/code]: The USB product ID of the device. - [code]steam_input_index[/code]: The Steam Input gamepad index, if the device is not a Steam Input device this key won't be present. - On Windows, the dictionary can have an additional field: - [code]xinput_index[/code]: The index of the controller in the XInput system. This key won't be present for devices not handled by XInput. - [b]Note:[/b] The returned dictionary is always empty on Android, iOS, visionOS, and Web. + The dictionary can also include the following fields under selected platforms: + [code]steam_input_index[/code]: The Steam Input gamepad index (Windows, Linux, and macOS only). If the device is not a Steam Input device this key won't be present. + [code]xinput_index[/code]: The index of the controller in the XInput system (Windows only). This key won't be present for devices not handled by XInput. + [b]Note:[/b] The returned dictionary is always empty on Android, iOS, and visionOS. diff --git a/drivers/SCsub b/drivers/SCsub index a5a5e6d8de65..6bd5fa6e3f48 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -61,8 +61,8 @@ if env["metal"]: SConscript("metal/SCsub") # Input drivers -if env["sdl"] and env["platform"] in ["linuxbsd", "macos", "windows"]: - # TODO: Evaluate support for Android, iOS, and Web. +if env["sdl"] and env["platform"] in ["linuxbsd", "macos", "windows", "web"]: + # TODO: Evaluate support for Android, iOS, and visionOS. SConscript("sdl/SCsub") # Core dependencies diff --git a/drivers/sdl/SCsub b/drivers/sdl/SCsub index a3631fedd6d1..c53dc0205a4d 100644 --- a/drivers/sdl/SCsub +++ b/drivers/sdl/SCsub @@ -188,6 +188,23 @@ if env["builtin_sdl"]: "timer/windows/SDL_systimer.c", ] + elif env["platform"] == "web": + env_sdl.Append(CPPDEFINES=["SDL_PLATFORM_EMSCRIPTEN"]) + thirdparty_sources += [ + "core/unix/SDL_appid.c", + "core/unix/SDL_poll.c", + "haptic/dummy/SDL_syshaptic.c", + "joystick/emscripten/SDL_sysjoystick.c", + "loadso/dummy/SDL_sysloadso.c", + "thread/generic/SDL_syscond.c", + "thread/generic/SDL_sysmutex.c", + "thread/generic/SDL_sysrwlock.c", + "thread/generic/SDL_syssem.c", + "thread/generic/SDL_systhread.c", + "thread/generic/SDL_systls.c", + "timer/unix/SDL_systimer.c", + ] + thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources] env_thirdparty = env_sdl.Clone() diff --git a/drivers/sdl/SDL_build_config_private.h b/drivers/sdl/SDL_build_config_private.h index d6e6cb72cc87..0e956de60f78 100644 --- a/drivers/sdl/SDL_build_config_private.h +++ b/drivers/sdl/SDL_build_config_private.h @@ -132,6 +132,21 @@ #define SDL_THREAD_PTHREAD 1 #define SDL_THREAD_PTHREAD_RECURSIVE_MUTEX 1 +// Emscripten (web) defines +#elif defined(SDL_PLATFORM_EMSCRIPTEN) + +#define SDL_PLATFORM_PRIVATE_NAME "Emscripten" +#define SDL_PLATFORM_UNIX 1 +#define HAVE_STDIO_H 1 +#define HAVE_LIBC 1 + +#define SDL_HAPTIC_DUMMY 1 +#define SDL_JOYSTICK_EMSCRIPTEN 1 + +#define SDL_LOADSO_DUMMY 1 +#define SDL_THREADS_DISABLED 1 +#define SDL_TIMER_UNIX 1 + // Other platforms are not supported (for now) #else #error "No SDL build config was found for this platform. Setup one before compiling the engine." diff --git a/drivers/sdl/joypad_sdl.cpp b/drivers/sdl/joypad_sdl.cpp index 8e808a885a70..2fc2999e92f9 100644 --- a/drivers/sdl/joypad_sdl.cpp +++ b/drivers/sdl/joypad_sdl.cpp @@ -43,6 +43,10 @@ #include #include +#ifdef WEB_ENABLED +#include +#endif + JoypadSDL *JoypadSDL::singleton = nullptr; // Macro to skip the SDL joystick event handling if the device is an SDL gamepad, because @@ -172,6 +176,11 @@ void JoypadSDL::process_events() { sdl_instance_id_to_joypad_id.insert(sdl_event.jdevice.which, joy_id); + if (should_ignore_joypad(sdl_event.jdevice.which)) { + close_joypad(joy_id); + continue; + } + Dictionary joypad_info; // Skip Godot's mapping system if SDL already handles the joypad's mapping. joypad_info["mapping_handled"] = SDL_IsGamepad(sdl_event.jdevice.which); @@ -179,6 +188,8 @@ void JoypadSDL::process_events() { joypad_info["vendor_id"] = itos(SDL_GetJoystickVendor(joy)); joypad_info["product_id"] = itos(SDL_GetJoystickProduct(joy)); +#if defined(WINDOWS_ENABLED) || defined(LINUXBSD_ENABLED) || defined(MACOS_ENABLED) + // These properties only make sense on desktop platforms. const uint64_t steam_handle = SDL_GetGamepadSteamHandle(gamepad); if (steam_handle != 0) { joypad_info["steam_input_index"] = itos(steam_handle); @@ -189,6 +200,7 @@ void JoypadSDL::process_events() { // For XInput controllers SDL_GetJoystickPlayerIndex returns the XInput user index. joypad_info["xinput_index"] = itos(player_index); } +#endif Input::get_singleton()->joy_connection_changed( joy_id, @@ -197,7 +209,9 @@ void JoypadSDL::process_events() { joypads[joy_id].guid, joypad_info); +#ifndef WEB_ENABLED // Joypad features are not supported on the web. Input::get_singleton()->set_joy_features(joy_id, &joypads[joy_id]); +#endif } // An event for an attached joypad } else if (sdl_event.type >= SDL_EVENT_JOYSTICK_AXIS_MOTION && sdl_event.type < SDL_EVENT_FINGER_DOWN && sdl_instance_id_to_joypad_id.has(sdl_event.jdevice.which)) { @@ -310,4 +324,20 @@ SDL_Gamepad *JoypadSDL::Joypad::get_sdl_gamepad() const { return SDL_GetGamepadFromID(sdl_instance_idx); } +bool JoypadSDL::should_ignore_joypad(SDL_JoystickID p_joy_id) { + SDL_Joystick *joy = SDL_GetJoystickFromID(p_joy_id); + String joy_name_lower = String(SDL_GetJoystickName(joy)).to_lower(); + +#ifdef WEB_ENABLED + // DualSense on Firefox works very badly (input lag, no dpad, wrong face buttons, no vibration), + // I'm not sure it's fixable in Godot, so we just ignore it. + bool is_firefox = EM_ASM_INT({ return navigator.userAgent.toLowerCase().includes('firefox') }); + if (is_firefox && joy_name_lower.contains("dualsense")) { + return true; + } +#endif + + return false; +} + #endif // SDL_ENABLED diff --git a/drivers/sdl/joypad_sdl.h b/drivers/sdl/joypad_sdl.h index 7010822675d4..d498a68f7683 100644 --- a/drivers/sdl/joypad_sdl.h +++ b/drivers/sdl/joypad_sdl.h @@ -71,4 +71,5 @@ class JoypadSDL { HashMap sdl_instance_id_to_joypad_id; void close_joypad(int p_pad_idx); + bool should_ignore_joypad(SDL_JoystickID p_joy_id); }; diff --git a/platform/web/detect.py b/platform/web/detect.py index 49dc2c50f529..e9a7c4dd17b7 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -309,6 +309,13 @@ def configure(env: "SConsEnvironment"): if env["wasm_simd"]: env.Append(CCFLAGS=["-msimd128"]) + if env["sdl"]: + if env["builtin_sdl"]: + env.Append(CPPDEFINES=["SDL_ENABLED"]) + else: + print_warning("`builtin_sdl` was explicitly disabled. Disabling SDL input driver support.") + env["sdl"] = False + # Reduce code size by generating less support code (e.g. skip NodeJS support). env.Append(LINKFLAGS=["-sENVIRONMENT=web,worker"]) diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index eda004f908d4..88c7da9c7542 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -837,36 +837,6 @@ void DisplayServerWeb::_window_blur_callback() { Input::get_singleton()->release_pressed_events(); } -// Gamepad -void DisplayServerWeb::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) { - String id = p_id; - String guid = p_guid; - -#ifdef PROXY_TO_PTHREAD_ENABLED - if (!Thread::is_main_thread()) { - callable_mp_static(DisplayServerWeb::_gamepad_callback).call_deferred(p_index, p_connected, id, guid); - return; - } -#endif - - _gamepad_callback(p_index, p_connected, id, guid); -} - -void DisplayServerWeb::_gamepad_callback(int p_index, int p_connected, const String &p_id, const String &p_guid) { - if (p_connected) { - DisplayServerWeb::get_singleton()->gamepad_count += 1; - } else { - DisplayServerWeb::get_singleton()->gamepad_count -= 1; - } - - Input *input = Input::get_singleton(); - if (p_connected) { - input->joy_connection_changed(p_index, true, p_id, p_guid); - } else { - input->joy_connection_changed(p_index, false, ""); - } -} - // IME. void DisplayServerWeb::ime_callback(int p_type, const char *p_text) { String text = String::utf8(p_text); @@ -964,36 +934,6 @@ String DisplayServerWeb::ime_get_text() const { return ime_text; } -void DisplayServerWeb::process_joypads() { - Input *input = Input::get_singleton(); - int32_t pads = godot_js_input_gamepad_sample_count(); - int32_t s_btns_num = 0; - int32_t s_axes_num = 0; - int32_t s_standard = 0; - float s_btns[16]; - float s_axes[10]; - for (int idx = 0; idx < pads; idx++) { - int err = godot_js_input_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard); - if (err) { - continue; - } - for (int b = 0; b < s_btns_num; b++) { - // Buttons 6 and 7 in the standard mapping need to be - // axis to be handled as JoyAxis::TRIGGER by Godot. - if (s_standard && (b == 6)) { - input->joy_axis(idx, JoyAxis::TRIGGER_LEFT, s_btns[b]); - } else if (s_standard && (b == 7)) { - input->joy_axis(idx, JoyAxis::TRIGGER_RIGHT, s_btns[b]); - } else { - input->joy_button(idx, (JoyButton)b, s_btns[b]); - } - } - for (int a = 0; a < s_axes_num; a++) { - input->joy_axis(idx, (JoyAxis)a, s_axes[a]); - } - } -} - Vector DisplayServerWeb::get_rendering_drivers_func() { Vector drivers; #ifdef GLES3_ENABLED @@ -1162,7 +1102,6 @@ DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode godot_js_input_key_cb(&DisplayServerWeb::key_callback, key_event.code, key_event.key); godot_js_input_paste_cb(&DisplayServerWeb::update_clipboard_callback); godot_js_input_drop_files_cb(&DisplayServerWeb::drop_files_js_callback); - godot_js_input_gamepad_cb(&DisplayServerWeb::gamepad_callback); godot_js_set_ime_cb(&DisplayServerWeb::ime_callback, &DisplayServerWeb::key_callback, key_event.code, key_event.key); // JS Display interface (js/libs/library_godot_display.js) @@ -1462,12 +1401,6 @@ DisplayServer::VSyncMode DisplayServerWeb::window_get_vsync_mode(WindowID p_vsyn void DisplayServerWeb::process_events() { process_keys(); Input::get_singleton()->flush_buffered_events(); - - if (gamepad_count > 0) { - if (godot_js_input_gamepad_sample() == OK) { - process_joypads(); - } - } } void DisplayServerWeb::process_keys() { diff --git a/platform/web/display_server_web.h b/platform/web/display_server_web.h index d78bd9809a06..246636f6d331 100644 --- a/platform/web/display_server_web.h +++ b/platform/web/display_server_web.h @@ -104,8 +104,6 @@ class DisplayServerWeb : public DisplayServer { bool swap_cancel_ok = false; NativeMenu *native_menu = nullptr; - int gamepad_count = 0; - MouseMode mouse_mode_base = MOUSE_MODE_VISIBLE; MouseMode mouse_mode_override = MOUSE_MODE_VISIBLE; bool mouse_mode_override_enabled = false; @@ -130,8 +128,6 @@ class DisplayServerWeb : public DisplayServer { static void _key_callback(const String &p_key_event_code, const String &p_key_event_key, int p_pressed, int p_repeat, int p_modifiers); WASM_EXPORT static void vk_input_text_callback(const char *p_text, int p_cursor); static void _vk_input_text_callback(const String &p_text, int p_cursor); - WASM_EXPORT static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); - static void _gamepad_callback(int p_index, int p_connected, const String &p_id, const String &p_guid); WASM_EXPORT static void js_utterance_callback(int p_event, int64_t p_id, int p_pos); static void _js_utterance_callback(int p_event, int64_t p_id, int p_pos); WASM_EXPORT static void ime_callback(int p_type, const char *p_text); @@ -149,7 +145,6 @@ class DisplayServerWeb : public DisplayServer { WASM_EXPORT static void drop_files_js_callback(const char **p_filev, int p_filec); static void _drop_files_js_callback(const Vector &p_files); - void process_joypads(); void process_keys(); static Vector get_rendering_drivers_func(); diff --git a/platform/web/godot_js.h b/platform/web/godot_js.h index 29b291d566a4..ad6475f8a711 100644 --- a/platform/web/godot_js.h +++ b/platform/web/godot_js.h @@ -71,11 +71,6 @@ extern void godot_js_set_ime_position(int p_x, int p_y); extern void godot_js_set_ime_cb(void (*p_input)(int p_type, const char *p_text), void (*p_callback)(int p_type, int p_repeat, int p_modifiers), char r_code[32], char r_key[32]); extern int godot_js_is_ime_focused(); -// Input gamepad -extern void godot_js_input_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); -extern int godot_js_input_gamepad_sample(); -extern int godot_js_input_gamepad_sample_count(); -extern int godot_js_input_gamepad_sample_get(int p_idx, float r_btns[16], int32_t *r_btns_num, float r_axes[10], int32_t *r_axes_num, int32_t *r_standard); extern void godot_js_input_paste_cb(void (*p_callback)(const char *p_text)); extern void godot_js_input_drop_files_cb(void (*p_callback)(const char **p_filev, int p_filec)); diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js index 576d1a7e2196..323bde496ccc 100644 --- a/platform/web/js/libs/library_godot_input.js +++ b/platform/web/js/libs/library_godot_input.js @@ -170,137 +170,6 @@ const GodotIME = { }; mergeInto(LibraryManager.library, GodotIME); -/* - * Gamepad API helper. - */ -const GodotInputGamepads = { - $GodotInputGamepads__deps: ['$GodotRuntime', '$GodotEventListeners'], - $GodotInputGamepads: { - samples: [], - - get_pads: function () { - try { - // Will throw in iframe when permission is denied. - // Will throw/warn in the future for insecure contexts. - // See https://github.com/w3c/gamepad/pull/120 - const pads = navigator.getGamepads(); - if (pads) { - return pads; - } - return []; - } catch (e) { - return []; - } - }, - - get_samples: function () { - return GodotInputGamepads.samples; - }, - - get_sample: function (index) { - const samples = GodotInputGamepads.samples; - return index < samples.length ? samples[index] : null; - }, - - sample: function () { - const pads = GodotInputGamepads.get_pads(); - const samples = []; - for (let i = 0; i < pads.length; i++) { - const pad = pads[i]; - if (!pad) { - samples.push(null); - continue; - } - const s = { - standard: pad.mapping === 'standard', - buttons: [], - axes: [], - connected: pad.connected, - }; - for (let b = 0; b < pad.buttons.length; b++) { - s.buttons.push(pad.buttons[b].value); - } - for (let a = 0; a < pad.axes.length; a++) { - s.axes.push(pad.axes[a]); - } - samples.push(s); - } - GodotInputGamepads.samples = samples; - }, - - init: function (onchange) { - GodotInputGamepads.samples = []; - function add(pad) { - const guid = GodotInputGamepads.get_guid(pad); - const c_id = GodotRuntime.allocString(pad.id); - const c_guid = GodotRuntime.allocString(guid); - onchange(pad.index, 1, c_id, c_guid); - GodotRuntime.free(c_id); - GodotRuntime.free(c_guid); - } - const pads = GodotInputGamepads.get_pads(); - for (let i = 0; i < pads.length; i++) { - // Might be reserved space. - if (pads[i]) { - add(pads[i]); - } - } - GodotEventListeners.add(window, 'gamepadconnected', function (evt) { - if (evt.gamepad) { - add(evt.gamepad); - } - }, false); - GodotEventListeners.add(window, 'gamepaddisconnected', function (evt) { - if (evt.gamepad) { - onchange(evt.gamepad.index, 0); - } - }, false); - }, - - get_guid: function (pad) { - if (pad.mapping) { - return pad.mapping; - } - const ua = navigator.userAgent; - let os = 'Unknown'; - if (ua.indexOf('Android') >= 0) { - os = 'Android'; - } else if (ua.indexOf('Linux') >= 0) { - os = 'Linux'; - } else if (ua.indexOf('iPhone') >= 0) { - os = 'iOS'; - } else if (ua.indexOf('Macintosh') >= 0) { - // Updated iPads will fall into this category. - os = 'MacOSX'; - } else if (ua.indexOf('Windows') >= 0) { - os = 'Windows'; - } - - const id = pad.id; - // Chrom* style: NAME (Vendor: xxxx Product: xxxx). - const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; - // Firefox/Safari style (Safari may remove leading zeroes). - const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; - let vendor = ''; - let product = ''; - if (exp1.test(id)) { - const match = exp1.exec(id); - vendor = match[1].padStart(4, '0'); - product = match[2].padStart(4, '0'); - } else if (exp2.test(id)) { - const match = exp2.exec(id); - vendor = match[1].padStart(4, '0'); - product = match[2].padStart(4, '0'); - } - if (!vendor || !product) { - return `${os}Unknown`; - } - return os + vendor + product; - }, - }, -}; -mergeInto(LibraryManager.library, GodotInputGamepads); - /* * Drag and drop helper. * This is pretty big, but basically detect dropped files on GodotConfig.canvas, @@ -480,7 +349,7 @@ mergeInto(LibraryManager.library, GodotInputDragDrop); * Godot exposed input functions. */ const GodotInput = { - $GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop', '$GodotIME'], + $GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputDragDrop', '$GodotIME'], $GodotInput: { getModifiers: function (evt) { return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3); @@ -632,53 +501,6 @@ const GodotInput = { return GodotIME.active; }, - /* - * Gamepad API - */ - godot_js_input_gamepad_cb__proxy: 'sync', - godot_js_input_gamepad_cb__sig: 'vi', - godot_js_input_gamepad_cb: function (change_cb) { - const onchange = GodotRuntime.get_func(change_cb); - GodotInputGamepads.init(onchange); - }, - - godot_js_input_gamepad_sample_count__proxy: 'sync', - godot_js_input_gamepad_sample_count__sig: 'i', - godot_js_input_gamepad_sample_count: function () { - return GodotInputGamepads.get_samples().length; - }, - - godot_js_input_gamepad_sample__proxy: 'sync', - godot_js_input_gamepad_sample__sig: 'i', - godot_js_input_gamepad_sample: function () { - GodotInputGamepads.sample(); - return 0; - }, - - godot_js_input_gamepad_sample_get__proxy: 'sync', - godot_js_input_gamepad_sample_get__sig: 'iiiiiii', - godot_js_input_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { - const sample = GodotInputGamepads.get_sample(p_index); - if (!sample || !sample.connected) { - return 1; - } - const btns = sample.buttons; - const btns_len = btns.length < 16 ? btns.length : 16; - for (let i = 0; i < btns_len; i++) { - GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); - } - GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); - const axes = sample.axes; - const axes_len = axes.length < 10 ? axes.length : 10; - for (let i = 0; i < axes_len; i++) { - GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); - } - GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); - const is_standard = sample.standard ? 1 : 0; - GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); - return 0; - }, - /* * Drag/Drop API */ diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp index 9666bd8882e7..f2d5dc0199ac 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -41,6 +41,9 @@ #include "core/io/file_access.h" #include "core/os/main_loop.h" #include "core/profiling/profiling.h" +#ifdef SDL_ENABLED +#include "drivers/sdl/joypad_sdl.h" +#endif #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" @@ -89,6 +92,11 @@ bool OS_Web::main_loop_iterate() { } DisplayServer::get_singleton()->process_events(); +#ifdef SDL_ENABLED + if (joypad_sdl) { + joypad_sdl->process_events(); + } +#endif return Main::iteration(); } @@ -106,6 +114,12 @@ void OS_Web::finalize() { memdelete(driver); } audio_drivers.clear(); + +#ifdef SDL_ENABLED + if (joypad_sdl) { + memdelete(joypad_sdl); + } +#endif } // Miscellaneous @@ -295,6 +309,14 @@ OS_Web *OS_Web::get_singleton() { } void OS_Web::initialize_joypads() { +#ifdef SDL_ENABLED + joypad_sdl = memnew(JoypadSDL()); + if (joypad_sdl->initialize() != OK) { + ERR_PRINT("Couldn't initialize SDL joypad input driver."); + memdelete(joypad_sdl); + joypad_sdl = nullptr; + } +#endif } OS_Web::OS_Web() { diff --git a/platform/web/os_web.h b/platform/web/os_web.h index 43bcfa6cab8f..c56cb885b2b5 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -41,10 +41,16 @@ #include +class JoypadSDL; + class OS_Web : public OS_Unix { MainLoop *main_loop = nullptr; List audio_drivers; +#ifdef SDL_ENABLED + JoypadSDL *joypad_sdl = nullptr; +#endif + MIDIDriverWebMidi midi_driver; bool idb_is_syncing = false; diff --git a/thirdparty/sdl/haptic/dummy/SDL_syshaptic.c b/thirdparty/sdl/haptic/dummy/SDL_syshaptic.c new file mode 100644 index 000000000000..81f711f02241 --- /dev/null +++ b/thirdparty/sdl/haptic/dummy/SDL_syshaptic.c @@ -0,0 +1,151 @@ +/* + 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" + +#if defined(SDL_HAPTIC_DUMMY) || defined(SDL_HAPTIC_DISABLED) + +#include "../SDL_syshaptic.h" + +static bool SDL_SYS_LogicError(void) +{ + return SDL_SetError("Logic error: No haptic devices available."); +} + +bool SDL_SYS_HapticInit(void) +{ + return true; +} + +int SDL_SYS_NumHaptics(void) +{ + return 0; +} + +SDL_HapticID SDL_SYS_HapticInstanceID(int index) +{ + SDL_SYS_LogicError(); + return 0; +} + +const char *SDL_SYS_HapticName(int index) +{ + SDL_SYS_LogicError(); + return NULL; +} + +bool SDL_SYS_HapticOpen(SDL_Haptic *haptic) +{ + return SDL_SYS_LogicError(); +} + +int SDL_SYS_HapticMouse(void) +{ + return -1; +} + +bool SDL_SYS_JoystickIsHaptic(SDL_Joystick *joystick) +{ + return false; +} + +bool SDL_SYS_HapticOpenFromJoystick(SDL_Haptic *haptic, SDL_Joystick *joystick) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_JoystickSameHaptic(SDL_Haptic *haptic, SDL_Joystick *joystick) +{ + return false; +} + +void SDL_SYS_HapticClose(SDL_Haptic *haptic) +{ + return; +} + +void SDL_SYS_HapticQuit(void) +{ + return; +} + +bool SDL_SYS_HapticNewEffect(SDL_Haptic *haptic, + struct haptic_effect *effect, const SDL_HapticEffect *base) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticUpdateEffect(SDL_Haptic *haptic, + struct haptic_effect *effect, + const SDL_HapticEffect *data) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticRunEffect(SDL_Haptic *haptic, struct haptic_effect *effect, + Uint32 iterations) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticStopEffect(SDL_Haptic *haptic, struct haptic_effect *effect) +{ + return SDL_SYS_LogicError(); +} + +void SDL_SYS_HapticDestroyEffect(SDL_Haptic *haptic, struct haptic_effect *effect) +{ + SDL_SYS_LogicError(); + return; +} + +int SDL_SYS_HapticGetEffectStatus(SDL_Haptic *haptic, + struct haptic_effect *effect) +{ + SDL_SYS_LogicError(); + return -1; +} + +bool SDL_SYS_HapticSetGain(SDL_Haptic *haptic, int gain) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticSetAutocenter(SDL_Haptic *haptic, int autocenter) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticPause(SDL_Haptic *haptic) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticResume(SDL_Haptic *haptic) +{ + return SDL_SYS_LogicError(); +} + +bool SDL_SYS_HapticStopAll(SDL_Haptic *haptic) +{ + return SDL_SYS_LogicError(); +} + +#endif // SDL_HAPTIC_DUMMY || SDL_HAPTIC_DISABLED diff --git a/thirdparty/sdl/joystick/SDL_gamepad_db.h b/thirdparty/sdl/joystick/SDL_gamepad_db.h index d5c04384d728..1e78e62a2f5b 100644 --- a/thirdparty/sdl/joystick/SDL_gamepad_db.h +++ b/thirdparty/sdl/joystick/SDL_gamepad_db.h @@ -889,7 +889,7 @@ static const char *s_GamepadMappings[] = { "050000005e040000e0020000ff070000,Xbox Wireless Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,", #endif #ifdef SDL_JOYSTICK_EMSCRIPTEN - "default,Standard Gamepad,a:b0,b:b1,back:b8,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b16,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,", + "default,*,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b7,x:b2,y:b3,", #endif #ifdef SDL_JOYSTICK_PS2 "0000000050533220436f6e74726f6c00,PS2 Controller,crc:ed87,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,", diff --git a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c new file mode 100644 index 000000000000..dbe16fd77d80 --- /dev/null +++ b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c @@ -0,0 +1,644 @@ +/* + 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_EMSCRIPTEN + +#include // For the definition of NULL + +#include "SDL_sysjoystick_c.h" +#include "../SDL_joystick_c.h" +#include "../usb_ids.h" + +static SDL_joylist_item *JoystickByIndex(int index); + +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), { + // Let's assume that if we're calling these function then the gamepad object definitely exists + let gamepad = navigator['getGamepads']()[device_index]; + + // 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); + } + + return 0; +}); + +EM_JS(int, SDL_GetEmscriptenJoystickProduct, (int device_index), { + let gamepad = navigator['getGamepads']()[device_index]; + + // 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); + } + + return 0; +}); + +EM_JS(int, SDL_IsEmscriptenJoystickXInput, (int device_index), { + let gamepad = navigator['getGamepads']()[device_index]; + + // Chrome, Edge, Opera: Xbox 360 Controller (XInput STANDARD GAMEPAD) + // Firefox: xinput + // TODO: Safari + return gamepad['id']['toLowerCase']()['indexOf']('xinput') >= 0; +}); + +static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) +{ + SDL_joylist_item *item; + int i; + Uint16 vendor, product; + bool is_xinput; + + SDL_LockJoysticks(); + + if (JoystickByIndex(gamepadEvent->index) != NULL) { + goto done; + } + + item = (SDL_joylist_item *)SDL_malloc(sizeof(SDL_joylist_item)); + if (!item) { + goto done; + } + + SDL_zerop(item); + item->index = gamepadEvent->index; + + vendor = SDL_GetEmscriptenJoystickVendor(gamepadEvent->index); + product = SDL_GetEmscriptenJoystickProduct(gamepadEvent->index); + is_xinput = SDL_IsEmscriptenJoystickXInput(gamepadEvent->index); + + // Use a generic VID/PID representing an XInput controller + if (!vendor && !product && is_xinput) { + vendor = USB_VENDOR_MICROSOFT; + product = USB_PRODUCT_XBOX360_XUSB_CONTROLLER; + } + + item->name = SDL_CreateJoystickName(vendor, product, NULL, gamepadEvent->id); + if (!item->name) { + SDL_free(item); + goto done; + } + + if (vendor && product) { + item->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_UNKNOWN, vendor, product, 0, NULL, item->name, 0, 0); + } else { + item->guid = SDL_CreateJoystickGUIDForName(item->name); + } + + if (is_xinput) { + item->guid.data[14] = 'x'; // See SDL_IsJoystickXInput + } + + item->mapping = SDL_strdup(gamepadEvent->mapping); + if (!item->mapping) { + SDL_free(item->name); + SDL_free(item); + goto done; + } + + const int real_button_count = gamepadEvent->numButtons; + const int real_axis_count = gamepadEvent->numAxes; + int first_trigger_button = -1; + int first_hat_button = -1; + int num_buttons = gamepadEvent->numButtons; + int num_axes = gamepadEvent->numAxes; + bool triggers_are_buttons = false; + if ((SDL_strcmp(gamepadEvent->mapping, "standard") == 0) && (num_buttons >= 16)) { // maps to a game console gamepad layout, turn the d-pad into a hat, treat triggers as analog. + num_buttons -= 4; // 4 dpad buttons become a hat. + first_hat_button = 12; + + if (num_axes == 4) { // Chrome gives the triggers analog button values, Firefox exposes them as extra axes. Both have the digital buttons. + num_axes += 2; // the two trigger "buttons" + triggers_are_buttons = true; + } + + // dump the digital trigger buttons in any case. + first_trigger_button = 6; + num_buttons -= 2; + } + + item->first_hat_button = first_hat_button; + item->first_trigger_button = first_trigger_button; + item->triggers_are_buttons = triggers_are_buttons; + item->nhats = (first_hat_button >= 0) ? 1 : 0; + item->naxes = num_axes; + item->nbuttons = num_buttons; + item->device_instance = SDL_GetNextObjectID(); + + item->timestamp = gamepadEvent->timestamp; + + int buttonidx = 0; + for (i = 0; i < real_button_count; i++, buttonidx++) { + if (buttonidx == first_hat_button) { + buttonidx += 4; // skip these buttons, we're treating them as hat input. + } else if (buttonidx == first_trigger_button) { + buttonidx += 2; // skip these buttons, we're treating them as axes. + } + item->analogButton[i] = gamepadEvent->analogButton[buttonidx]; + item->digitalButton[i] = gamepadEvent->digitalButton[buttonidx]; + } + + for (i = 0; i < real_axis_count; i++) { + item->axis[i] = gamepadEvent->axis[i]; + } + + if (item->triggers_are_buttons) { + item->axis[real_axis_count] = (gamepadEvent->analogButton[first_trigger_button] * 2.0f) - 1.0f; + item->axis[real_axis_count+1] = (gamepadEvent->analogButton[first_trigger_button+1] * 2.0f) - 1.0f; + } + + SDL_assert(item->nhats <= 1); // there is (currently) only ever one of these, faked from the d-pad buttons. + if (first_hat_button != -1) { + Uint8 value = SDL_HAT_CENTERED; + // this currently expects the first button to be up, then down, then left, then right. + if (gamepadEvent->digitalButton[first_hat_button + 0]) { + value |= SDL_HAT_UP; + } + if (gamepadEvent->digitalButton[first_hat_button + 1]) { + value |= SDL_HAT_DOWN; + } + if (gamepadEvent->digitalButton[first_hat_button + 2]) { + value |= SDL_HAT_LEFT; + } + if (gamepadEvent->digitalButton[first_hat_button + 3]) { + value |= SDL_HAT_RIGHT; + } + item->hat = value; + } + + if (!SDL_joylist_tail) { + SDL_joylist = SDL_joylist_tail = item; + } else { + SDL_joylist_tail->next = item; + SDL_joylist_tail = item; + } + + ++numjoysticks; + + SDL_PrivateJoystickAdded(item->device_instance); + +#ifdef DEBUG_JOYSTICK + SDL_Log("Number of joysticks is %d", numjoysticks); +#endif +#ifdef DEBUG_JOYSTICK + SDL_Log("Added joystick with index %d", item->index); +#endif + +done: + SDL_UnlockJoysticks(); + + return 1; +} + +static EM_BOOL Emscripten_JoyStickDisconnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) +{ + SDL_joylist_item *item = SDL_joylist; + SDL_joylist_item *prev = NULL; + + SDL_LockJoysticks(); + + while (item) { + if (item->index == gamepadEvent->index) { + break; + } + prev = item; + item = item->next; + } + + if (!item) { + goto done; + } + + if (item->joystick) { + item->joystick->hwdata = NULL; + } + + if (prev) { + prev->next = item->next; + } else { + SDL_assert(SDL_joylist == item); + SDL_joylist = item->next; + } + if (item == SDL_joylist_tail) { + SDL_joylist_tail = prev; + } + + // Need to decrement the joystick count before we post the event + --numjoysticks; + + SDL_PrivateJoystickRemoved(item->device_instance); + +#ifdef DEBUG_JOYSTICK + SDL_Log("Removed joystick with id %d", item->device_instance); +#endif + SDL_free(item->name); + SDL_free(item->mapping); + SDL_free(item); + +done: + SDL_UnlockJoysticks(); + + return 1; +} + +// Function to perform any system-specific joystick related cleanup +static void EMSCRIPTEN_JoystickQuit(void) +{ + SDL_joylist_item *item = NULL; + SDL_joylist_item *next = NULL; + + for (item = SDL_joylist; item; item = next) { + next = item->next; + SDL_free(item->mapping); + SDL_free(item->name); + SDL_free(item); + } + + SDL_joylist = SDL_joylist_tail = NULL; + + numjoysticks = 0; + + emscripten_set_gamepadconnected_callback(NULL, 0, NULL); + emscripten_set_gamepaddisconnected_callback(NULL, 0, NULL); +} + +// Function to scan the system for joysticks. +static bool EMSCRIPTEN_JoystickInit(void) +{ + int rc, i, numjs; + EmscriptenGamepadEvent gamepadState; + + numjoysticks = 0; + + rc = emscripten_sample_gamepad_data(); + + // Check if gamepad is supported by browser + if (rc == EMSCRIPTEN_RESULT_NOT_SUPPORTED) { + return SDL_SetError("Gamepads not supported"); + } + + numjs = emscripten_get_num_gamepads(); + + // handle already connected gamepads + if (numjs > 0) { + for (i = 0; i < numjs; i++) { + rc = emscripten_get_gamepad_status(i, &gamepadState); + if (rc == EMSCRIPTEN_RESULT_SUCCESS) { + Emscripten_JoyStickConnected(EMSCRIPTEN_EVENT_GAMEPADCONNECTED, + &gamepadState, + NULL); + } + } + } + + rc = emscripten_set_gamepadconnected_callback(NULL, + 0, + Emscripten_JoyStickConnected); + + if (rc != EMSCRIPTEN_RESULT_SUCCESS) { + EMSCRIPTEN_JoystickQuit(); + return SDL_SetError("Could not set gamepad connect callback"); + } + + rc = emscripten_set_gamepaddisconnected_callback(NULL, + 0, + Emscripten_JoyStickDisconnected); + if (rc != EMSCRIPTEN_RESULT_SUCCESS) { + EMSCRIPTEN_JoystickQuit(); + return SDL_SetError("Could not set gamepad disconnect callback"); + } + + return true; +} + +// Returns item matching given SDL device index. +static SDL_joylist_item *JoystickByDeviceIndex(int device_index) +{ + SDL_joylist_item *item = SDL_joylist; + + while (0 < device_index) { + --device_index; + item = item->next; + } + + return item; +} + +// Returns item matching given HTML gamepad index. +static SDL_joylist_item *JoystickByIndex(int index) +{ + SDL_joylist_item *item = SDL_joylist; + + if (index < 0) { + return NULL; + } + + while (item) { + if (item->index == index) { + break; + } + item = item->next; + } + + return item; +} + +static int EMSCRIPTEN_JoystickGetCount(void) +{ + return numjoysticks; +} + +static void EMSCRIPTEN_JoystickDetect(void) +{ +} + +static bool EMSCRIPTEN_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name) +{ + // We don't override any other drivers + return false; +} + +static const char *EMSCRIPTEN_JoystickGetDeviceName(int device_index) +{ + return JoystickByDeviceIndex(device_index)->name; +} + +static const char *EMSCRIPTEN_JoystickGetDevicePath(int device_index) +{ + return NULL; +} + +static int EMSCRIPTEN_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index) +{ + return -1; +} + +static int EMSCRIPTEN_JoystickGetDevicePlayerIndex(int device_index) +{ + return -1; +} + +static void EMSCRIPTEN_JoystickSetDevicePlayerIndex(int device_index, int player_index) +{ +} + +static SDL_JoystickID EMSCRIPTEN_JoystickGetDeviceInstanceID(int device_index) +{ + return JoystickByDeviceIndex(device_index)->device_instance; +} + +static bool EMSCRIPTEN_JoystickOpen(SDL_Joystick *joystick, int device_index) +{ + SDL_joylist_item *item = JoystickByDeviceIndex(device_index); + bool rumble_available = false; + + if (!item) { + return SDL_SetError("No such device"); + } + + if (item->joystick) { + return SDL_SetError("Joystick already opened"); + } + + joystick->hwdata = (struct joystick_hwdata *)item; + item->joystick = joystick; + + // HTML5 Gamepad API doesn't offer hats, but we can fake it from the d-pad buttons on the "standard" mapping. + joystick->nhats = item->nhats; + joystick->nbuttons = item->nbuttons; + joystick->naxes = item->naxes; + + rumble_available = EM_ASM_INT({ + let gamepads = navigator['getGamepads'](); + if (!gamepads) { + return 0; + } + let gamepad = gamepads[$0]; + if (!gamepad || !gamepad['vibrationActuator']) { + return 0; + } + return 1; + }, item->index); + + if (rumble_available) { + SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, true); + } + + return true; +} + +/* Function to update the state of a joystick - called as a device poll. + * This function shouldn't update the joystick structure directly, + * but instead should call SDL_PrivateJoystick*() to deliver events + * and update joystick device state. + */ +static void EMSCRIPTEN_JoystickUpdate(SDL_Joystick *joystick) +{ + EmscriptenGamepadEvent gamepadState; + SDL_joylist_item *item = (SDL_joylist_item *)joystick->hwdata; + int i, result; + Uint64 timestamp = SDL_GetTicksNS(); + + emscripten_sample_gamepad_data(); + + if (item) { + result = emscripten_get_gamepad_status(item->index, &gamepadState); + if (result == EMSCRIPTEN_RESULT_SUCCESS) { + if (gamepadState.timestamp == 0 || gamepadState.timestamp != item->timestamp) { + const int first_hat_button = item->first_hat_button; + const int first_trigger_button = item->first_trigger_button; + const int real_button_count = gamepadState.numButtons; + const int real_axis_count = gamepadState.numAxes; + + int buttonidx = 0; + for (i = 0; i < real_button_count; i++, buttonidx++) { + if (buttonidx == first_hat_button) { + buttonidx += 4; // skip these buttons, we're treating them as hat input. + } else if (buttonidx == first_trigger_button) { + buttonidx += 2; // skip these buttons, we're treating them as axes. + } + if (item->digitalButton[i] != gamepadState.digitalButton[buttonidx]) { + const bool down = (gamepadState.digitalButton[buttonidx] != 0); + SDL_SendJoystickButton(timestamp, item->joystick, i, down); + } + + // store values to compare them in the next update + item->analogButton[i] = gamepadState.analogButton[buttonidx]; + item->digitalButton[i] = gamepadState.digitalButton[buttonidx]; + } + + for (i = 0; i < real_axis_count; i++) { + if (item->axis[i] != gamepadState.axis[i]) { + SDL_SendJoystickAxis(timestamp, item->joystick, i, (Sint16)(32767.0f * gamepadState.axis[i])); + item->axis[i] = gamepadState.axis[i]; + } + } + + if (item->triggers_are_buttons) { + for (i = 0; i < 2; i++) { + if (item->axis[real_axis_count+i] != gamepadState.analogButton[first_trigger_button+i]) { + SDL_SendJoystickAxis(timestamp, item->joystick, real_axis_count+i, (Sint16)(32767.0f * ((gamepadState.analogButton[first_trigger_button+i] * 2.0f) - 1.0f))); + item->axis[real_axis_count+i] = gamepadState.analogButton[first_trigger_button+i]; + } + } + } + + SDL_assert(item->nhats <= 1); // there is (currently) only ever one of these, faked from the d-pad buttons. + if (item->nhats) { + Uint8 value = SDL_HAT_CENTERED; + // this currently expects the first button to be up, then down, then left, then right. + if (gamepadState.digitalButton[first_hat_button + 0]) { + value |= SDL_HAT_UP; + } else if (gamepadState.digitalButton[first_hat_button + 1]) { + value |= SDL_HAT_DOWN; + } + if (gamepadState.digitalButton[first_hat_button + 2]) { + value |= SDL_HAT_LEFT; + } else if (gamepadState.digitalButton[first_hat_button + 3]) { + value |= SDL_HAT_RIGHT; + } + if (item->hat != value) { + item->hat = value; + SDL_SendJoystickHat(timestamp, item->joystick, 0, value); + } + } + + + item->timestamp = gamepadState.timestamp; + } + } + } +} + +// Function to close a joystick after use +static void EMSCRIPTEN_JoystickClose(SDL_Joystick *joystick) +{ + SDL_joylist_item *item = (SDL_joylist_item *)joystick->hwdata; + if (item) { + item->joystick = NULL; + } +} + +static SDL_GUID EMSCRIPTEN_JoystickGetDeviceGUID(int device_index) +{ + return JoystickByDeviceIndex(device_index)->guid; +} + +static bool EMSCRIPTEN_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) +{ + SDL_joylist_item *item = (SDL_joylist_item *)joystick->hwdata; + + // clang-format off + bool result = EM_ASM_INT({ + let gamepads = navigator['getGamepads'](); + if (!gamepads) { + return 0; + } + let gamepad = gamepads[$0]; + if (!gamepad || !gamepad['vibrationActuator']) { + return 0; + } + + gamepad['vibrationActuator']['playEffect']('dual-rumble', { + 'startDelay': 0, + 'duration': 3000, + 'weakMagnitude': $2 / 0xFFFF, + 'strongMagnitude': $1 / 0xFFFF, + }); + return 1; + }, item->index, low_frequency_rumble, high_frequency_rumble); + + return result; +} + +static bool EMSCRIPTEN_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) +{ + return SDL_Unsupported(); +} + +static bool EMSCRIPTEN_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) +{ + return false; +} + +static bool EMSCRIPTEN_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) +{ + return SDL_Unsupported(); +} + +static bool EMSCRIPTEN_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size) +{ + return SDL_Unsupported(); +} + +static bool EMSCRIPTEN_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled) +{ + return SDL_Unsupported(); +} + +SDL_JoystickDriver SDL_EMSCRIPTEN_JoystickDriver = { + EMSCRIPTEN_JoystickInit, + EMSCRIPTEN_JoystickGetCount, + EMSCRIPTEN_JoystickDetect, + EMSCRIPTEN_JoystickIsDevicePresent, + EMSCRIPTEN_JoystickGetDeviceName, + EMSCRIPTEN_JoystickGetDevicePath, + EMSCRIPTEN_JoystickGetDeviceSteamVirtualGamepadSlot, + EMSCRIPTEN_JoystickGetDevicePlayerIndex, + EMSCRIPTEN_JoystickSetDevicePlayerIndex, + EMSCRIPTEN_JoystickGetDeviceGUID, + EMSCRIPTEN_JoystickGetDeviceInstanceID, + EMSCRIPTEN_JoystickOpen, + EMSCRIPTEN_JoystickRumble, + EMSCRIPTEN_JoystickRumbleTriggers, + EMSCRIPTEN_JoystickSetLED, + EMSCRIPTEN_JoystickSendEffect, + EMSCRIPTEN_JoystickSetSensorsEnabled, + EMSCRIPTEN_JoystickUpdate, + EMSCRIPTEN_JoystickClose, + EMSCRIPTEN_JoystickQuit, + EMSCRIPTEN_JoystickGetGamepadMapping +}; + +#endif // SDL_JOYSTICK_EMSCRIPTEN diff --git a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h new file mode 100644 index 000000000000..a9b312aebe21 --- /dev/null +++ b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h @@ -0,0 +1,55 @@ +/* + 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_EMSCRIPTEN +#include "../SDL_sysjoystick.h" + +#include + +// A linked list of available joysticks +typedef struct SDL_joylist_item +{ + int index; + char *name; + char *mapping; + SDL_JoystickID device_instance; + SDL_Joystick *joystick; + int first_hat_button; + int first_trigger_button; + bool triggers_are_buttons; + int nhats; + SDL_GUID guid; + int nbuttons; + int naxes; + double timestamp; + double axis[64]; // !!! FIXME: don't hardcode 64 on all of these. + double analogButton[64]; + EM_BOOL digitalButton[64]; + Uint8 hat; // there is (currently) only ever one of these, faked from the d-pad buttons. + + struct SDL_joylist_item *next; +} SDL_joylist_item; + +typedef SDL_joylist_item joystick_hwdata; + +#endif // SDL_JOYSTICK_EMSCRIPTEN diff --git a/thirdparty/sdl/loadso/dummy/SDL_sysloadso.c b/thirdparty/sdl/loadso/dummy/SDL_sysloadso.c new file mode 100644 index 000000000000..733386baf4c1 --- /dev/null +++ b/thirdparty/sdl/loadso/dummy/SDL_sysloadso.c @@ -0,0 +1,45 @@ +/* + 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" + +#if defined(SDL_LOADSO_DUMMY) + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +// System dependent library loading routines + +SDL_SharedObject *SDL_LoadObject(const char *sofile) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_FunctionPointer SDL_LoadFunction(SDL_SharedObject *handle, const char *name) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_UnloadObject(SDL_SharedObject *handle) +{ + // no-op. +} + +#endif // SDL_LOADSO_DUMMY diff --git a/thirdparty/sdl/patches/0007-emscripten-joystick.patch b/thirdparty/sdl/patches/0007-emscripten-joystick.patch new file mode 100644 index 000000000000..c25c0f830052 --- /dev/null +++ b/thirdparty/sdl/patches/0007-emscripten-joystick.patch @@ -0,0 +1,341 @@ +diff --git a/thirdparty/sdl/joystick/SDL_gamepad_db.h b/thirdparty/sdl/joystick/SDL_gamepad_db.h +index d5c04384d7..1e78e62a2f 100644 +--- a/thirdparty/sdl/joystick/SDL_gamepad_db.h ++++ b/thirdparty/sdl/joystick/SDL_gamepad_db.h +@@ -889,7 +889,7 @@ static const char *s_GamepadMappings[] = { + "050000005e040000e0020000ff070000,Xbox Wireless Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,", + #endif + #ifdef SDL_JOYSTICK_EMSCRIPTEN +- "default,Standard Gamepad,a:b0,b:b1,back:b8,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b16,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,", ++ "default,*,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b7,x:b2,y:b3,", + #endif + #ifdef SDL_JOYSTICK_PS2 + "0000000050533220436f6e74726f6c00,PS2 Controller,crc:ed87,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,", +diff --git a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c +index cddd975a71..fc02eb6172 100644 +--- a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c ++++ b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c +@@ -27,6 +27,7 @@ + + #include "SDL_sysjoystick_c.h" + #include "../SDL_joystick_c.h" ++#include "../usb_ids.h" + + static SDL_joylist_item *JoystickByIndex(int index); + +@@ -34,10 +35,60 @@ 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), { ++ // Let's assume that if we're calling these function then the gamepad object definitely exists ++ let gamepad = navigator['getGamepads']()[device_index]; ++ ++ // 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); ++ } ++ ++ return 0; ++}); ++ ++EM_JS(int, SDL_GetEmscriptenJoystickProduct, (int device_index), { ++ let gamepad = navigator['getGamepads']()[device_index]; ++ ++ // 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); ++ } ++ ++ return 0; ++}); ++ ++EM_JS(int, SDL_IsEmscriptenJoystickXInput, (int device_index), { ++ let gamepad = navigator['getGamepads']()[device_index]; ++ ++ // Chrome, Edge, Opera: Xbox 360 Controller (XInput STANDARD GAMEPAD) ++ // Firefox: xinput ++ // TODO: Safari ++ return gamepad['id']['toLowerCase']()['indexOf']('xinput') >= 0; ++}); ++ + static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) + { + SDL_joylist_item *item; + int i; ++ Uint16 vendor, product; ++ bool is_xinput; + + SDL_LockJoysticks(); + +@@ -53,12 +104,32 @@ static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamep + SDL_zerop(item); + item->index = gamepadEvent->index; + +- item->name = SDL_CreateJoystickName(0, 0, NULL, gamepadEvent->id); ++ vendor = SDL_GetEmscriptenJoystickVendor(gamepadEvent->index); ++ product = SDL_GetEmscriptenJoystickProduct(gamepadEvent->index); ++ is_xinput = SDL_IsEmscriptenJoystickXInput(gamepadEvent->index); ++ ++ // Use a generic VID/PID representing an XInput controller ++ if (!vendor && !product && is_xinput) { ++ vendor = USB_VENDOR_MICROSOFT; ++ product = USB_PRODUCT_XBOX360_XUSB_CONTROLLER; ++ } ++ ++ item->name = SDL_CreateJoystickName(vendor, product, NULL, gamepadEvent->id); + if (!item->name) { + SDL_free(item); + goto done; + } + ++ if (vendor && product) { ++ item->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_UNKNOWN, vendor, product, 0, NULL, item->name, 0, 0); ++ } else { ++ item->guid = SDL_CreateJoystickGUIDForName(item->name); ++ } ++ ++ if (is_xinput) { ++ item->guid.data[14] = 'x'; // See SDL_IsJoystickXInput ++ } ++ + item->mapping = SDL_strdup(gamepadEvent->mapping); + if (!item->mapping) { + SDL_free(item->name); +@@ -66,19 +137,74 @@ static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamep + goto done; + } + +- item->naxes = gamepadEvent->numAxes; +- item->nbuttons = gamepadEvent->numButtons; ++ const int real_button_count = gamepadEvent->numButtons; ++ const int real_axis_count = gamepadEvent->numAxes; ++ int first_trigger_button = -1; ++ int first_hat_button = -1; ++ int num_buttons = gamepadEvent->numButtons; ++ int num_axes = gamepadEvent->numAxes; ++ bool triggers_are_buttons = false; ++ if ((SDL_strcmp(gamepadEvent->mapping, "standard") == 0) && (num_buttons >= 16)) { // maps to a game console gamepad layout, turn the d-pad into a hat, treat triggers as analog. ++ num_buttons -= 4; // 4 dpad buttons become a hat. ++ first_hat_button = 12; ++ ++ if (num_axes == 4) { // Chrome gives the triggers analog button values, Firefox exposes them as extra axes. Both have the digital buttons. ++ num_axes += 2; // the two trigger "buttons" ++ triggers_are_buttons = true; ++ } ++ ++ // dump the digital trigger buttons in any case. ++ first_trigger_button = 6; ++ num_buttons -= 2; ++ } ++ ++ item->first_hat_button = first_hat_button; ++ item->first_trigger_button = first_trigger_button; ++ item->triggers_are_buttons = triggers_are_buttons; ++ item->nhats = (first_hat_button >= 0) ? 1 : 0; ++ item->naxes = num_axes; ++ item->nbuttons = num_buttons; + item->device_instance = SDL_GetNextObjectID(); + + item->timestamp = gamepadEvent->timestamp; + +- for (i = 0; i < item->naxes; i++) { ++ int buttonidx = 0; ++ for (i = 0; i < real_button_count; i++, buttonidx++) { ++ if (buttonidx == first_hat_button) { ++ buttonidx += 4; // skip these buttons, we're treating them as hat input. ++ } else if (buttonidx == first_trigger_button) { ++ buttonidx += 2; // skip these buttons, we're treating them as axes. ++ } ++ item->analogButton[i] = gamepadEvent->analogButton[buttonidx]; ++ item->digitalButton[i] = gamepadEvent->digitalButton[buttonidx]; ++ } ++ ++ for (i = 0; i < real_axis_count; i++) { + item->axis[i] = gamepadEvent->axis[i]; + } + +- for (i = 0; i < item->nbuttons; i++) { +- item->analogButton[i] = gamepadEvent->analogButton[i]; +- item->digitalButton[i] = gamepadEvent->digitalButton[i]; ++ if (item->triggers_are_buttons) { ++ item->axis[real_axis_count] = (gamepadEvent->analogButton[first_trigger_button] * 2.0f) - 1.0f; ++ item->axis[real_axis_count+1] = (gamepadEvent->analogButton[first_trigger_button+1] * 2.0f) - 1.0f; ++ } ++ ++ SDL_assert(item->nhats <= 1); // there is (currently) only ever one of these, faked from the d-pad buttons. ++ if (first_hat_button != -1) { ++ Uint8 value = SDL_HAT_CENTERED; ++ // this currently expects the first button to be up, then down, then left, then right. ++ if (gamepadEvent->digitalButton[first_hat_button + 0]) { ++ value |= SDL_HAT_UP; ++ } ++ if (gamepadEvent->digitalButton[first_hat_button + 1]) { ++ value |= SDL_HAT_DOWN; ++ } ++ if (gamepadEvent->digitalButton[first_hat_button + 2]) { ++ value |= SDL_HAT_LEFT; ++ } ++ if (gamepadEvent->digitalButton[first_hat_button + 3]) { ++ value |= SDL_HAT_RIGHT; ++ } ++ item->hat = value; + } + + if (!SDL_joylist_tail) { +@@ -318,9 +444,8 @@ static bool EMSCRIPTEN_JoystickOpen(SDL_Joystick *joystick, int device_index) + joystick->hwdata = (struct joystick_hwdata *)item; + item->joystick = joystick; + +- // HTML5 Gamepad API doesn't say anything about these +- joystick->nhats = 0; +- ++ // HTML5 Gamepad API doesn't offer hats, but we can fake it from the d-pad buttons on the "standard" mapping. ++ joystick->nhats = item->nhats; + joystick->nbuttons = item->nbuttons; + joystick->naxes = item->naxes; + +@@ -361,28 +486,65 @@ static void EMSCRIPTEN_JoystickUpdate(SDL_Joystick *joystick) + result = emscripten_get_gamepad_status(item->index, &gamepadState); + if (result == EMSCRIPTEN_RESULT_SUCCESS) { + if (gamepadState.timestamp == 0 || gamepadState.timestamp != item->timestamp) { +- for (i = 0; i < item->nbuttons; i++) { +- if (item->digitalButton[i] != gamepadState.digitalButton[i]) { +- bool down = (gamepadState.digitalButton[i] != 0); ++ const int first_hat_button = item->first_hat_button; ++ const int first_trigger_button = item->first_trigger_button; ++ const int real_button_count = gamepadState.numButtons; ++ const int real_axis_count = gamepadState.numAxes; ++ ++ int buttonidx = 0; ++ for (i = 0; i < real_button_count; i++, buttonidx++) { ++ if (buttonidx == first_hat_button) { ++ buttonidx += 4; // skip these buttons, we're treating them as hat input. ++ } else if (buttonidx == first_trigger_button) { ++ buttonidx += 2; // skip these buttons, we're treating them as axes. ++ } ++ if (item->digitalButton[i] != gamepadState.digitalButton[buttonidx]) { ++ const bool down = (gamepadState.digitalButton[buttonidx] != 0); + SDL_SendJoystickButton(timestamp, item->joystick, i, down); + } + + // store values to compare them in the next update +- item->analogButton[i] = gamepadState.analogButton[i]; +- item->digitalButton[i] = gamepadState.digitalButton[i]; ++ item->analogButton[i] = gamepadState.analogButton[buttonidx]; ++ item->digitalButton[i] = gamepadState.digitalButton[buttonidx]; + } + +- for (i = 0; i < item->naxes; i++) { ++ for (i = 0; i < real_axis_count; i++) { + if (item->axis[i] != gamepadState.axis[i]) { +- // do we need to do conversion? +- SDL_SendJoystickAxis(timestamp, item->joystick, i, +- (Sint16)(32767. * gamepadState.axis[i])); ++ SDL_SendJoystickAxis(timestamp, item->joystick, i, (Sint16)(32767.0f * gamepadState.axis[i])); ++ item->axis[i] = gamepadState.axis[i]; + } ++ } + +- // store to compare in next update +- item->axis[i] = gamepadState.axis[i]; ++ if (item->triggers_are_buttons) { ++ for (i = 0; i < 2; i++) { ++ if (item->axis[real_axis_count+i] != gamepadState.analogButton[first_trigger_button+i]) { ++ SDL_SendJoystickAxis(timestamp, item->joystick, real_axis_count+i, (Sint16)(32767.0f * ((gamepadState.analogButton[first_trigger_button+i] * 2.0f) - 1.0f))); ++ item->axis[real_axis_count+i] = gamepadState.analogButton[first_trigger_button+i]; ++ } ++ } + } + ++ SDL_assert(item->nhats <= 1); // there is (currently) only ever one of these, faked from the d-pad buttons. ++ if (item->nhats) { ++ Uint8 value = SDL_HAT_CENTERED; ++ // this currently expects the first button to be up, then down, then left, then right. ++ if (gamepadState.digitalButton[first_hat_button + 0]) { ++ value |= SDL_HAT_UP; ++ } else if (gamepadState.digitalButton[first_hat_button + 1]) { ++ value |= SDL_HAT_DOWN; ++ } ++ if (gamepadState.digitalButton[first_hat_button + 2]) { ++ value |= SDL_HAT_LEFT; ++ } else if (gamepadState.digitalButton[first_hat_button + 3]) { ++ value |= SDL_HAT_RIGHT; ++ } ++ if (item->hat != value) { ++ item->hat = value; ++ SDL_SendJoystickHat(timestamp, item->joystick, 0, value); ++ } ++ } ++ ++ + item->timestamp = gamepadState.timestamp; + } + } +@@ -400,9 +562,7 @@ static void EMSCRIPTEN_JoystickClose(SDL_Joystick *joystick) + + static SDL_GUID EMSCRIPTEN_JoystickGetDeviceGUID(int device_index) + { +- // the GUID is just the name for now +- const char *name = EMSCRIPTEN_JoystickGetDeviceName(device_index); +- return SDL_CreateJoystickGUIDForName(name); ++ return JoystickByDeviceIndex(device_index)->guid; + } + + static bool EMSCRIPTEN_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) +diff --git a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h +index e03a27c41f..a9b312aebe 100644 +--- a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h ++++ b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick_c.h +@@ -34,12 +34,18 @@ typedef struct SDL_joylist_item + char *mapping; + SDL_JoystickID device_instance; + SDL_Joystick *joystick; ++ int first_hat_button; ++ int first_trigger_button; ++ bool triggers_are_buttons; ++ int nhats; ++ SDL_GUID guid; + int nbuttons; + int naxes; + double timestamp; +- double axis[64]; ++ double axis[64]; // !!! FIXME: don't hardcode 64 on all of these. + double analogButton[64]; + EM_BOOL digitalButton[64]; ++ Uint8 hat; // there is (currently) only ever one of these, faked from the d-pad buttons. + + struct SDL_joylist_item *next; + } SDL_joylist_item; + +diff --git a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c +index fc02eb6172..dbe16fd77d 100644 +--- a/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c ++++ b/thirdparty/sdl/joystick/emscripten/SDL_sysjoystick.c +@@ -583,8 +583,8 @@ static bool EMSCRIPTEN_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequen + gamepad['vibrationActuator']['playEffect']('dual-rumble', { + 'startDelay': 0, + 'duration': 3000, +- 'weakMagnitude': $1 / 0xFFFF, +- 'strongMagnitude': $2 / 0xFFFF, ++ 'weakMagnitude': $2 / 0xFFFF, ++ 'strongMagnitude': $1 / 0xFFFF, + }); + return 1; + }, item->index, low_frequency_rumble, high_frequency_rumble); diff --git a/thirdparty/sdl/thread/generic/SDL_sysmutex.c b/thirdparty/sdl/thread/generic/SDL_sysmutex.c new file mode 100644 index 000000000000..e58da67bc84d --- /dev/null +++ b/thirdparty/sdl/thread/generic/SDL_sysmutex.c @@ -0,0 +1,132 @@ +/* + 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" + +// An implementation of mutexes using semaphores + +#include "SDL_systhread_c.h" + +struct SDL_Mutex +{ + int recursive; + SDL_ThreadID owner; + SDL_Semaphore *sem; +}; + +SDL_Mutex *SDL_CreateMutex(void) +{ + SDL_Mutex *mutex = (SDL_Mutex *)SDL_calloc(1, sizeof(*mutex)); + +#ifndef SDL_THREADS_DISABLED + if (mutex) { + // Create the mutex semaphore, with initial value 1 + mutex->sem = SDL_CreateSemaphore(1); + mutex->recursive = 0; + mutex->owner = 0; + if (!mutex->sem) { + SDL_free(mutex); + mutex = NULL; + } + } +#endif // !SDL_THREADS_DISABLED + + return mutex; +} + +void SDL_DestroyMutex(SDL_Mutex *mutex) +{ + if (mutex) { + if (mutex->sem) { + SDL_DestroySemaphore(mutex->sem); + } + SDL_free(mutex); + } +} + +void SDL_LockMutex(SDL_Mutex *mutex) SDL_NO_THREAD_SAFETY_ANALYSIS // clang doesn't know about NULL mutexes +{ +#ifndef SDL_THREADS_DISABLED + if (mutex != NULL) { + SDL_ThreadID this_thread = SDL_GetCurrentThreadID(); + if (mutex->owner == this_thread) { + ++mutex->recursive; + } else { + /* The order of operations is important. + We set the locking thread id after we obtain the lock + so unlocks from other threads will fail. + */ + SDL_WaitSemaphore(mutex->sem); + mutex->owner = this_thread; + mutex->recursive = 0; + } + } +#endif // SDL_THREADS_DISABLED +} + +bool SDL_TryLockMutex(SDL_Mutex *mutex) +{ + bool result = true; +#ifndef SDL_THREADS_DISABLED + if (mutex) { + SDL_ThreadID this_thread = SDL_GetCurrentThreadID(); + if (mutex->owner == this_thread) { + ++mutex->recursive; + } else { + /* The order of operations is important. + We set the locking thread id after we obtain the lock + so unlocks from other threads will fail. + */ + result = SDL_TryWaitSemaphore(mutex->sem); + if (result) { + mutex->owner = this_thread; + mutex->recursive = 0; + } + } + } +#endif // SDL_THREADS_DISABLED + return result; +} + +void SDL_UnlockMutex(SDL_Mutex *mutex) SDL_NO_THREAD_SAFETY_ANALYSIS // clang doesn't know about NULL mutexes +{ +#ifndef SDL_THREADS_DISABLED + if (mutex != NULL) { + // If we don't own the mutex, we can't unlock it + if (SDL_GetCurrentThreadID() != mutex->owner) { + SDL_assert(!"Tried to unlock a mutex we don't own!"); + return; // (undefined behavior!) SDL_SetError("mutex not owned by this thread"); + } + + if (mutex->recursive) { + --mutex->recursive; + } else { + /* The order of operations is important. + First reset the owner so another thread doesn't lock + the mutex and set the ownership before we reset it, + then release the lock semaphore. + */ + mutex->owner = 0; + SDL_SignalSemaphore(mutex->sem); + } + } +#endif // SDL_THREADS_DISABLED +} + diff --git a/thirdparty/sdl/thread/generic/SDL_sysmutex_c.h b/thirdparty/sdl/thread/generic/SDL_sysmutex_c.h new file mode 100644 index 000000000000..4b0c6f8fcc83 --- /dev/null +++ b/thirdparty/sdl/thread/generic/SDL_sysmutex_c.h @@ -0,0 +1,21 @@ +/* + 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" diff --git a/thirdparty/sdl/thread/generic/SDL_systhread.c b/thirdparty/sdl/thread/generic/SDL_systhread.c new file mode 100644 index 000000000000..ecfa4e13ef30 --- /dev/null +++ b/thirdparty/sdl/thread/generic/SDL_systhread.c @@ -0,0 +1,57 @@ +/* + 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" + +// Thread management routines for SDL + +#include "../SDL_systhread.h" + +bool SDL_SYS_CreateThread(SDL_Thread *thread, + SDL_FunctionPointer pfnBeginThread, + SDL_FunctionPointer pfnEndThread) +{ + return SDL_SetError("Threads are not supported on this platform"); +} + +void SDL_SYS_SetupThread(const char *name) +{ + return; +} + +SDL_ThreadID SDL_GetCurrentThreadID(void) +{ + return 0; +} + +bool SDL_SYS_SetThreadPriority(SDL_ThreadPriority priority) +{ + return true; +} + +void SDL_SYS_WaitThread(SDL_Thread *thread) +{ + return; +} + +void SDL_SYS_DetachThread(SDL_Thread *thread) +{ + return; +} diff --git a/thirdparty/sdl/thread/generic/SDL_systls.c b/thirdparty/sdl/thread/generic/SDL_systls.c new file mode 100644 index 000000000000..b8ebafe8a8fc --- /dev/null +++ b/thirdparty/sdl/thread/generic/SDL_systls.c @@ -0,0 +1,44 @@ +/* + 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" +#include "../SDL_thread_c.h" + +void SDL_SYS_InitTLSData(void) +{ + SDL_Generic_InitTLSData(); +} + +SDL_TLSData *SDL_SYS_GetTLSData(void) +{ + return SDL_Generic_GetTLSData(); +} + +bool SDL_SYS_SetTLSData(SDL_TLSData *data) +{ + return SDL_Generic_SetTLSData(data); +} + +void SDL_SYS_QuitTLSData(void) +{ + SDL_Generic_QuitTLSData(); +} + diff --git a/thirdparty/sdl/update-sdl.sh b/thirdparty/sdl/update-sdl.sh index 6c68a4159872..f6533ae390b0 100755 --- a/thirdparty/sdl/update-sdl.sh +++ b/thirdparty/sdl/update-sdl.sh @@ -52,24 +52,22 @@ rm -f $target/core/windows/version.rc rm -f $target/core/linux/SDL_{fcitx,ibus,ime,system_theme}.* mkdir $target/haptic -cp -rv haptic/{*.{c,h},darwin,linux,windows} $target/haptic +cp -rv haptic/{*.{c,h},darwin,dummy,linux,windows} $target/haptic mkdir $target/joystick -cp -rv joystick/{*.{c,h},apple,darwin,hidapi,linux,windows} $target/joystick +cp -rv joystick/{*.{c,h},apple,darwin,emscripten,hidapi,linux,windows} $target/joystick mkdir $target/loadso -cp -rv loadso/dlopen $target/loadso +cp -rv loadso/{dlopen,dummy} $target/loadso mkdir $target/sensor cp -rv sensor/{*.{c,h},dummy,windows} $target/sensor -mkdir $target/thread -cp -rv thread/{*.{c,h},pthread,windows} $target/thread # Despite being 'generic', syssem.c is included in the Unix driver for macOS, # and syscond/sysrwlock are used by the Windows driver. # systhread_c.h is included by all these, but we should NOT compile the matching .c file. -mkdir $target/thread/generic -cp -v thread/generic/SDL_{syssem.c,{syscond,sysrwlock}*.{c,h},systhread_c.h} $target/thread/generic +mkdir $target/thread +cp -rv thread/{*.{c,h},generic,pthread,windows} $target/thread mkdir $target/timer cp -rv timer/{*.{c,h},unix,windows} $target/timer