From c419f627238501168bbec34552abd33f1362eb23 Mon Sep 17 00:00:00 2001 From: Christopher Allen Lane Date: Mon, 16 Mar 2026 17:52:30 -0400 Subject: [PATCH] feat: expose controller port device API for EmulatorJS Add two new WASM-exported functions to allow EmulatorJS to query available controller device types per port and change them at runtime: - ejs_get_controller_port_info(): serializes the controller info reported by cores via RETRO_ENVIRONMENT_SET_CONTROLLER_INFO into a string that JavaScript can parse. - ejs_set_controller_port_device(port, device): thin wrapper around core_set_controller_port_device() to allow changing a port's device type (e.g. switching from Joypad to Super Scope) from JavaScript. Also adds RETRO_DEVICE_LIGHTGUN handling to the EmulatorJS input driver, mapping mouse position to lightgun screen coordinates and mouse buttons to lightgun buttons (trigger, cursor, turbo, start, select). This enables lightgun support for SNES Super Scope, Justifier, NES Zapper, PSX GunCon, and other lightgun peripherals across all supported cores. Closes EmulatorJS/EmulatorJS#677 Related: EmulatorJS/EmulatorJS#272, EmulatorJS/EmulatorJS#816, EmulatorJS/EmulatorJS#1117 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile.emulatorjs | 3 +- input/drivers/emulatorjs_input.c | 39 +++++++++++++++++++++++ runloop.c | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Makefile.emulatorjs b/Makefile.emulatorjs index a6267060d4aa..ff7dc2aaf3b5 100644 --- a/Makefile.emulatorjs +++ b/Makefile.emulatorjs @@ -126,7 +126,8 @@ EXPORTED_FUNCTIONS = _main,_malloc,_free,_load_state, \ _system_restart,_cmd_savefiles,_get_core_options,_cmd_save_state,_supports_states,_reset_cheat,_toggleMainLoop, \ _save_file_path,_get_disk_count,_set_current_disk,_get_current_disk,_refresh_save_files,_toggle_fastforward,_set_ff_ratio, \ _toggle_slow_motion,_set_sm_ratio,_toggle_rewind,_set_rewind_granularity,_get_current_frame_count,_ejs_set_keyboard_enabled, \ - _set_vsync,_set_video_rotation,_get_video_dimensions,EmscriptenSendCommand,EmscriptenReceiveCommandReply,_get_memory_data + _set_vsync,_set_video_rotation,_get_video_dimensions,EmscriptenSendCommand,EmscriptenReceiveCommandReply,_get_memory_data, \ + _ejs_set_controller_port_device,_ejs_get_controller_port_info EXPORTS := Browser,callMain,cwrap,getValue,HEAPU8,FS,PATH,ERRNO_CODES,AL,abort,ENV,stringToNewUTF8,UTF8ToString,Browser,EmscriptenSendCommand,EmscriptenReceiveCommandReply,EmulatorJSGetState,EmulatorJSGetMemoryData diff --git a/input/drivers/emulatorjs_input.c b/input/drivers/emulatorjs_input.c index f96061c42f0e..564fee5158f8 100644 --- a/input/drivers/emulatorjs_input.c +++ b/input/drivers/emulatorjs_input.c @@ -801,6 +801,44 @@ static int16_t rwebinput_input_state( case RETRO_DEVICE_MOUSE: case RARCH_DEVICE_MOUSE_SCREEN: return rwebinput_mouse_state(&rwebinput->mouse, id, device == RARCH_DEVICE_MOUSE_SCREEN); + case RETRO_DEVICE_LIGHTGUN: + { + struct video_viewport vp = {0}; + rwebinput_mouse_state_t + *mouse = &rwebinput->mouse; + int16_t res_x = 0; + int16_t res_y = 0; + int16_t res_screen_x = 0; + int16_t res_screen_y = 0; + + if (!(video_driver_translate_coord_viewport_confined_wrap( + &vp, mouse->x, mouse->y, + &res_x, &res_y, &res_screen_x, &res_screen_y))) + return 0; + + switch (id) + { + case RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X: + return res_x; + case RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y: + return res_y; + case RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN: + return input_driver_pointer_is_offscreen(res_x, res_y); + case RETRO_DEVICE_ID_LIGHTGUN_TRIGGER: + return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNL)); + case RETRO_DEVICE_ID_LIGHTGUN_AUX_A: + return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNR)); + case RETRO_DEVICE_ID_LIGHTGUN_AUX_B: + return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNM)); + case RETRO_DEVICE_ID_LIGHTGUN_START: + return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTN4)); + case RETRO_DEVICE_ID_LIGHTGUN_SELECT: + return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTN5)); + default: + break; + } + } + break; case RETRO_DEVICE_POINTER: case RARCH_DEVICE_POINTER_SCREEN: { @@ -1046,6 +1084,7 @@ static uint64_t rwebinput_get_capabilities(void *data) | (1 << RETRO_DEVICE_ANALOG) | (1 << RETRO_DEVICE_KEYBOARD) | (1 << RETRO_DEVICE_MOUSE) + | (1 << RETRO_DEVICE_LIGHTGUN) | (1 << RETRO_DEVICE_POINTER); } diff --git a/runloop.c b/runloop.c index 92d1a96d6985..6b9fd09f7a18 100644 --- a/runloop.c +++ b/runloop.c @@ -8670,4 +8670,57 @@ float get_video_dimensions(const char *key) return -1.0f; } } + +void ejs_set_controller_port_device(unsigned port, unsigned device) +{ + retro_ctx_controller_info_t pad; + pad.port = port; + pad.device = device; + core_set_controller_port_device(&pad); +} + +char* ejs_get_controller_port_info(void) +{ + runloop_state_t *runloop_st = &runloop_state; + rarch_system_info_t *sys_info = &runloop_st->system; + + if (!sys_info || !sys_info->ports.data || sys_info->ports.size == 0) + return ""; + + /* Calculate required buffer size */ + size_t size = 1; + for (size_t i = 0; i < sys_info->ports.size; i++) + { + const struct retro_controller_info *port = &sys_info->ports.data[i]; + for (size_t j = 0; j < port->num_types; j++) + { + if (!port->types[j].desc) + continue; + size += snprintf(NULL, 0, "%zu:%u:%s\n", + i, port->types[j].id, port->types[j].desc); + } + } + + static char *rv = NULL; + free(rv); + rv = (char*)calloc(size, 1); + if (!rv) + return ""; + + for (size_t i = 0; i < sys_info->ports.size; i++) + { + const struct retro_controller_info *port = &sys_info->ports.data[i]; + for (size_t j = 0; j < port->num_types; j++) + { + if (!port->types[j].desc) + continue; + char line[256]; + snprintf(line, sizeof(line), "%zu:%u:%s\n", + i, port->types[j].id, port->types[j].desc); + strcat(rv, line); + } + } + + return rv; +} #endif