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