From aed1fa4ac6324e76f0ad88d8b051e914286274d6 Mon Sep 17 00:00:00 2001 From: antangelo Date: Thu, 25 Dec 2025 16:54:30 -0500 Subject: [PATCH] ui: Add controller/keyboard input mapping configuration --- config_spec.yml | 86 ++++++++++++ ui/meson.build | 1 + ui/xemu-controllers.cc | 141 ++++++++++++++++++++ ui/xemu-controllers.h | 85 ++++++++++++ ui/xemu-input.c | 281 +++++++++++++++++++++++++++++----------- ui/xemu-input.h | 12 +- ui/xemu-settings.cc | 60 +++++++++ ui/xemu-settings.h | 3 + ui/xemu.c | 4 +- ui/xui/input-manager.cc | 23 ++-- ui/xui/main-menu.cc | 253 ++++++++++++++++++++++++++++++++++-- ui/xui/main-menu.hh | 16 +++ ui/xui/main.cc | 5 + 13 files changed, 874 insertions(+), 96 deletions(-) create mode 100644 ui/xemu-controllers.cc create mode 100644 ui/xemu-controllers.h diff --git a/config_spec.yml b/config_spec.yml index 1e044dfe19..12a510d882 100644 --- a/config_spec.yml +++ b/config_spec.yml @@ -55,6 +55,7 @@ input: auto_bind: type: bool default: true + # Deprecated: use 'enable_rumble' on the controller mapping settings instead allow_vibration: type: bool default: true @@ -136,6 +137,91 @@ input: rtrigger: type: integer default: 18 # w + gamepad_mappings: + type: array + items: + gamepad_id: string + enable_rumble: + type: bool + default: true + # steel_battalion_mapping: + # ... + controller_mapping: + a: + type: integer + default: 0 + b: + type: integer + default: 1 + x: + type: integer + default: 2 + y: + type: integer + default: 3 + back: + type: integer + default: 4 + guide: + type: integer + default: 5 + start: + type: integer + default: 6 + lstick_btn: + type: integer + default: 7 + rstick_btn: + type: integer + default: 8 + lshoulder: + type: integer + default: 9 + rshoulder: + type: integer + default: 10 + dpad_up: + type: integer + default: 11 + dpad_down: + type: integer + default: 12 + dpad_left: + type: integer + default: 13 + dpad_right: + type: integer + default: 14 + axis_left_x: + type: integer + default: 0 + axis_left_y: + type: integer + default: 1 + axis_right_x: + type: integer + default: 2 + axis_right_y: + type: integer + default: 3 + axis_trigger_left: + type: integer + default: 4 + axis_trigger_right: + type: integer + default: 5 + invert_axis_left_x: + type: bool + default: false + invert_axis_left_y: + type: bool + default: false + invert_axis_right_x: + type: bool + default: false + invert_axis_right_y: + type: bool + default: false display: renderer: diff --git a/ui/meson.build b/ui/meson.build index b0b19d3f79..08cfaf669a 100644 --- a/ui/meson.build +++ b/ui/meson.build @@ -28,6 +28,7 @@ xemu_ss.add(files( 'xemu-monitor.c', 'xemu-net.c', 'xemu-settings.cc', + 'xemu-controllers.cc', 'xemu.c', 'xemu-data.c', diff --git a/ui/xemu-controllers.cc b/ui/xemu-controllers.cc new file mode 100644 index 0000000000..968e9aa00d --- /dev/null +++ b/ui/xemu-controllers.cc @@ -0,0 +1,141 @@ +/* + * xemu Controller Binding Management + * + * Copyright (C) 2025 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "xemu-controllers.h" +#include "xemu-settings.h" +#include +#include +#include + +constexpr int controller_button_count = 15; +constexpr int controller_axes_count = 6; + +RebindEventResult +ControllerKeyboardRebindingMap::ConsumeRebindEvent(SDL_Event *event) +{ + // Bind on key up + // This ensures the UI does not immediately respond once the new binding is + // applied + if (event->type == SDL_KEYUP) { + *(g_keyboard_scancode_map[m_table_row]) = event->key.keysym.scancode; + return RebindEventResult::Complete; + } + + return RebindEventResult::Ignore; +} + +RebindEventResult ControllerGamepadRebindingMap::HandleButtonEvent( + SDL_ControllerButtonEvent *event) +{ + if (m_state->sdl_joystick_id != event->which) { + return RebindEventResult::Ignore; + } + + int *button_map[controller_button_count] = { + &m_state->controller_map->controller_mapping.a, + &m_state->controller_map->controller_mapping.b, + &m_state->controller_map->controller_mapping.x, + &m_state->controller_map->controller_mapping.y, + &m_state->controller_map->controller_mapping.back, + &m_state->controller_map->controller_mapping.guide, + &m_state->controller_map->controller_mapping.start, + &m_state->controller_map->controller_mapping.lstick_btn, + &m_state->controller_map->controller_mapping.rstick_btn, + &m_state->controller_map->controller_mapping.lshoulder, + &m_state->controller_map->controller_mapping.rshoulder, + &m_state->controller_map->controller_mapping.dpad_up, + &m_state->controller_map->controller_mapping.dpad_down, + &m_state->controller_map->controller_mapping.dpad_left, + &m_state->controller_map->controller_mapping.dpad_right, + }; + + // FIXME: Allow face buttons to map to axes + if (m_table_row >= controller_button_count) { + return RebindEventResult::Ignore; + } + + // If we only track up events, then we might rebind to a button + // that was already held down when the rebinding event began + if (event->type == SDL_CONTROLLERBUTTONDOWN) { + m_seen_key_down = true; + return RebindEventResult::Ignore; + } + + // Bind on controller button up + // This ensures the UI does not immediately respond once the new binding is + // applied + if (event->type != SDL_CONTROLLERBUTTONUP || !m_seen_key_down) { + return RebindEventResult::Ignore; + } + + *(button_map[m_table_row]) = event->button; + + return RebindEventResult::Complete; +} + +RebindEventResult +ControllerGamepadRebindingMap::HandleAxisEvent(SDL_ControllerAxisEvent *event) +{ + if (m_state->sdl_joystick_id != event->which) { + return RebindEventResult::Ignore; + } + + // Axis inputs cannot be bound to controller buttons + if (m_table_row < controller_button_count) { + return RebindEventResult::Ignore; + } + + // Requre that the input be sufficiently outside of any deadzone range + // before using it for rebinding + if (std::abs(event->value >> 1) <= + (std::numeric_limits::max() >> 2)) { + return RebindEventResult::Ignore; + } + + int *axis_map[controller_axes_count] = { + &m_state->controller_map->controller_mapping.axis_left_x, + &m_state->controller_map->controller_mapping.axis_left_y, + &m_state->controller_map->controller_mapping.axis_right_x, + &m_state->controller_map->controller_mapping.axis_right_y, + &m_state->controller_map->controller_mapping.axis_trigger_left, + &m_state->controller_map->controller_mapping.axis_trigger_right, + }; + + *(axis_map[m_table_row - controller_button_count]) = event->axis; + + return RebindEventResult::Complete; +} + +RebindEventResult +ControllerGamepadRebindingMap::ConsumeRebindEvent(SDL_Event *event) +{ + switch (event->type) { + case SDL_CONTROLLERDEVICEREMOVED: + return (m_state->sdl_joystick_id == event->cdevice.which) ? + RebindEventResult::Complete : + RebindEventResult::Ignore; + case SDL_CONTROLLERBUTTONUP: + case SDL_CONTROLLERBUTTONDOWN: + return HandleButtonEvent(&event->cbutton); + case SDL_CONTROLLERAXISMOTION: + return HandleAxisEvent(&event->caxis); + default: + return RebindEventResult::Ignore; + } +} diff --git a/ui/xemu-controllers.h b/ui/xemu-controllers.h new file mode 100644 index 0000000000..bbef1e90db --- /dev/null +++ b/ui/xemu-controllers.h @@ -0,0 +1,85 @@ +/* + * xemu Settings Management + * + * Copyright (C) 2025 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef XEMU_CONTROLLERS_H +#define XEMU_CONTROLLERS_H + +#include "xemu-input.h" +#include + +#ifdef __cplusplus + +enum class RebindEventResult { + Ignore, + Complete, +}; + +struct RebindingMap { +protected: + int m_table_row; + RebindingMap(int table_row) : m_table_row{ table_row } + { + } + +public: + virtual RebindEventResult ConsumeRebindEvent(SDL_Event *event) = 0; + + int GetTableRow() const + { + return m_table_row; + } + + virtual ~RebindingMap() = default; +}; + +struct ControllerKeyboardRebindingMap : public virtual RebindingMap { + RebindEventResult ConsumeRebindEvent(SDL_Event *event) override; + + ControllerKeyboardRebindingMap(int table_row) : RebindingMap(table_row) + { + } +}; + +class ControllerGamepadRebindingMap : public virtual RebindingMap { + ControllerState *m_state; + bool m_seen_key_down; + + RebindEventResult HandleButtonEvent(SDL_ControllerButtonEvent *event); + RebindEventResult HandleAxisEvent(SDL_ControllerAxisEvent *event); + +public: + RebindEventResult ConsumeRebindEvent(SDL_Event *event) override; + ControllerGamepadRebindingMap(int table_row, ControllerState *state) + : RebindingMap(table_row), m_state{ state }, m_seen_key_down{ false } + { + } +}; + +extern "C" { +#endif // __cplusplus + +extern int *g_keyboard_scancode_map[25]; + +GamepadMappings *xemu_settings_load_gamepad_mapping(const char *guid, bool reset_to_default); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // XEMU_CONTROLLERS_H diff --git a/ui/xemu-input.c b/ui/xemu-input.c index de1db08f21..9c7da5ff55 100644 --- a/ui/xemu-input.c +++ b/ui/xemu-input.c @@ -28,9 +28,12 @@ #include "qemu/timer.h" #include "qemu/config-file.h" +#include "xemu-controllers.h" #include "xemu-input.h" #include "xemu-notifications.h" #include "xemu-settings.h" +#include +#include #include "sysemu/blockdev.h" @@ -126,7 +129,90 @@ static const char **peripheral_params_settings_map[4][2] = { &g_config.input.peripherals.port4.peripheral_param_1 } }; -static int sdl_kbd_scancode_map[25]; +int *g_keyboard_scancode_map[25] = { + &g_config.input.keyboard_controller_scancode_map.a, + &g_config.input.keyboard_controller_scancode_map.b, + &g_config.input.keyboard_controller_scancode_map.x, + &g_config.input.keyboard_controller_scancode_map.y, + &g_config.input.keyboard_controller_scancode_map.back, + &g_config.input.keyboard_controller_scancode_map.guide, + &g_config.input.keyboard_controller_scancode_map.start, + &g_config.input.keyboard_controller_scancode_map.lstick_btn, + &g_config.input.keyboard_controller_scancode_map.rstick_btn, + &g_config.input.keyboard_controller_scancode_map.white, + &g_config.input.keyboard_controller_scancode_map.black, + &g_config.input.keyboard_controller_scancode_map.dpad_up, + &g_config.input.keyboard_controller_scancode_map.dpad_down, + &g_config.input.keyboard_controller_scancode_map.dpad_left, + &g_config.input.keyboard_controller_scancode_map.dpad_right, + &g_config.input.keyboard_controller_scancode_map.lstick_up, + &g_config.input.keyboard_controller_scancode_map.lstick_left, + &g_config.input.keyboard_controller_scancode_map.lstick_right, + &g_config.input.keyboard_controller_scancode_map.lstick_down, + &g_config.input.keyboard_controller_scancode_map.ltrigger, + &g_config.input.keyboard_controller_scancode_map.rstick_up, + &g_config.input.keyboard_controller_scancode_map.rstick_left, + &g_config.input.keyboard_controller_scancode_map.rstick_right, + &g_config.input.keyboard_controller_scancode_map.rstick_down, + &g_config.input.keyboard_controller_scancode_map.rtrigger, +}; + +static void check_and_reset_in_range(int *btn, int min, int max, + const char *message) +{ + if (*btn < min || *btn >= max) { + fprintf(stderr, "%s\n", message); + *btn = min; + } +} + +static void xemu_input_bindings_reload_controller_map(ControllerState *con, bool reset_to_default) +{ + assert(con->type == INPUT_DEVICE_SDL_GAMECONTROLLER); + + char guid[35] = { 0 }; + SDL_JoystickGetGUIDString(con->sdl_joystick_guid, guid, sizeof(guid)); + con->controller_map = xemu_settings_load_gamepad_mapping(guid, reset_to_default); + +#define CHECK_RESET_BUTTON(btn) \ + check_and_reset_in_range(&con->controller_map->controller_mapping.btn, \ + SDL_CONTROLLER_BUTTON_INVALID, \ + SDL_CONTROLLER_BUTTON_MAX, \ + "Invalid entry for button " #btn ", resetting") + + CHECK_RESET_BUTTON(a); + CHECK_RESET_BUTTON(b); + CHECK_RESET_BUTTON(x); + CHECK_RESET_BUTTON(y); + CHECK_RESET_BUTTON(dpad_left); + CHECK_RESET_BUTTON(dpad_up); + CHECK_RESET_BUTTON(dpad_right); + CHECK_RESET_BUTTON(dpad_down); + CHECK_RESET_BUTTON(back); + CHECK_RESET_BUTTON(start); + CHECK_RESET_BUTTON(lshoulder); + CHECK_RESET_BUTTON(rshoulder); + CHECK_RESET_BUTTON(lstick_btn); + CHECK_RESET_BUTTON(rstick_btn); + CHECK_RESET_BUTTON(guide); + +#undef CHECK_RESET_BUTTON + +#define CHECK_RESET_AXIS(axis) \ + check_and_reset_in_range(&con->controller_map->controller_mapping.axis, \ + SDL_CONTROLLER_AXIS_INVALID, \ + SDL_CONTROLLER_AXIS_MAX, \ + "Invalid entry for button " #axis ", resetting") + + CHECK_RESET_AXIS(axis_trigger_left); + CHECK_RESET_AXIS(axis_trigger_right); + CHECK_RESET_AXIS(axis_left_x); + CHECK_RESET_AXIS(axis_left_y); + CHECK_RESET_AXIS(axis_right_x); + CHECK_RESET_AXIS(axis_right_y); + +#undef CHECK_RESET_AXIS +} static const char *get_bound_driver(int port) { @@ -171,38 +257,14 @@ void xemu_input_init(void) new_con->peripherals[0] = NULL; new_con->peripherals[1] = NULL; - sdl_kbd_scancode_map[0] = g_config.input.keyboard_controller_scancode_map.a; - sdl_kbd_scancode_map[1] = g_config.input.keyboard_controller_scancode_map.b; - sdl_kbd_scancode_map[2] = g_config.input.keyboard_controller_scancode_map.x; - sdl_kbd_scancode_map[3] = g_config.input.keyboard_controller_scancode_map.y; - sdl_kbd_scancode_map[4] = g_config.input.keyboard_controller_scancode_map.dpad_left; - sdl_kbd_scancode_map[5] = g_config.input.keyboard_controller_scancode_map.dpad_up; - sdl_kbd_scancode_map[6] = g_config.input.keyboard_controller_scancode_map.dpad_right; - sdl_kbd_scancode_map[7] = g_config.input.keyboard_controller_scancode_map.dpad_down; - sdl_kbd_scancode_map[8] = g_config.input.keyboard_controller_scancode_map.back; - sdl_kbd_scancode_map[9] = g_config.input.keyboard_controller_scancode_map.start; - sdl_kbd_scancode_map[10] = g_config.input.keyboard_controller_scancode_map.white; - sdl_kbd_scancode_map[11] = g_config.input.keyboard_controller_scancode_map.black; - sdl_kbd_scancode_map[12] = g_config.input.keyboard_controller_scancode_map.lstick_btn; - sdl_kbd_scancode_map[13] = g_config.input.keyboard_controller_scancode_map.rstick_btn; - sdl_kbd_scancode_map[14] = g_config.input.keyboard_controller_scancode_map.guide; - sdl_kbd_scancode_map[15] = g_config.input.keyboard_controller_scancode_map.lstick_up; - sdl_kbd_scancode_map[16] = g_config.input.keyboard_controller_scancode_map.lstick_left; - sdl_kbd_scancode_map[17] = g_config.input.keyboard_controller_scancode_map.lstick_right; - sdl_kbd_scancode_map[18] = g_config.input.keyboard_controller_scancode_map.lstick_down; - sdl_kbd_scancode_map[19] = g_config.input.keyboard_controller_scancode_map.ltrigger; - sdl_kbd_scancode_map[20] = g_config.input.keyboard_controller_scancode_map.rstick_up; - sdl_kbd_scancode_map[21] = g_config.input.keyboard_controller_scancode_map.rstick_left; - sdl_kbd_scancode_map[22] = g_config.input.keyboard_controller_scancode_map.rstick_right; - sdl_kbd_scancode_map[23] = g_config.input.keyboard_controller_scancode_map.rstick_down; - sdl_kbd_scancode_map[24] = g_config.input.keyboard_controller_scancode_map.rtrigger; - for (int i = 0; i < 25; i++) { - if( (sdl_kbd_scancode_map[i] < SDL_SCANCODE_UNKNOWN) || - (sdl_kbd_scancode_map[i] >= SDL_NUM_SCANCODES) ) { - fprintf(stderr, "WARNING: Keyboard controller map scancode out of range (%d) : Disabled\n", sdl_kbd_scancode_map[i]); - sdl_kbd_scancode_map[i] = SDL_SCANCODE_UNKNOWN; - } + static const char *format_str = + "WARNING: Keyboard controller map scancode out of range " + "(%d) : Disabled\n"; + char buf[128]; + snprintf(buf, sizeof(buf), format_str, i); + check_and_reset_in_range(g_keyboard_scancode_map[i], + SDL_SCANCODE_UNKNOWN, SDL_NUM_SCANCODES, buf); } bound_drivers[0] = get_bound_driver(0); @@ -277,7 +339,6 @@ void xemu_input_process_sdl_events(const SDL_Event *event) memset(new_con, 0, sizeof(ControllerState)); new_con->type = INPUT_DEVICE_SDL_GAMECONTROLLER; new_con->name = SDL_GameControllerName(sdl_con); - new_con->rumble_enabled = true; new_con->sdl_gamecontroller = sdl_con; new_con->sdl_joystick = SDL_GameControllerGetJoystick(new_con->sdl_gamecontroller); new_con->sdl_joystick_id = SDL_JoystickInstanceID(new_con->sdl_joystick); @@ -292,6 +353,8 @@ void xemu_input_process_sdl_events(const SDL_Event *event) SDL_JoystickGetGUIDString(new_con->sdl_joystick_guid, guid_buf, sizeof(guid_buf)); DPRINTF("Opened %s (%s)\n", new_con->name, guid_buf); + xemu_input_bindings_reload_controller_map(new_con, /*reset_to_default=*/false); + QTAILQ_INSERT_TAIL(&available_controllers, new_con, entry); // Do not replace binding for a currently bound device. In the case that @@ -427,21 +490,48 @@ void xemu_input_update_sdl_kbd_controller_state(ControllerState *state) const uint8_t *kbd = SDL_GetKeyboardState(NULL); - for (int i = 0; i < 15; i++) { - state->buttons |= kbd[sdl_kbd_scancode_map[i]] << i; - } +#define KBD_STATE(btn) \ + (kbd[g_config.input.keyboard_controller_scancode_map.btn]) - if (kbd[sdl_kbd_scancode_map[15]]) state->axis[CONTROLLER_AXIS_LSTICK_Y] = 32767; - if (kbd[sdl_kbd_scancode_map[16]]) state->axis[CONTROLLER_AXIS_LSTICK_X] = -32768; - if (kbd[sdl_kbd_scancode_map[17]]) state->axis[CONTROLLER_AXIS_LSTICK_X] = 32767; - if (kbd[sdl_kbd_scancode_map[18]]) state->axis[CONTROLLER_AXIS_LSTICK_Y] = -32768; - if (kbd[sdl_kbd_scancode_map[19]]) state->axis[CONTROLLER_AXIS_LTRIG] = 32767; + state->buttons |= KBD_STATE(a) << 0; + state->buttons |= KBD_STATE(b) << 1; + state->buttons |= KBD_STATE(x) << 2; + state->buttons |= KBD_STATE(y) << 3; + state->buttons |= KBD_STATE(dpad_left) << 4; + state->buttons |= KBD_STATE(dpad_up) << 5; + state->buttons |= KBD_STATE(dpad_right) << 6; + state->buttons |= KBD_STATE(dpad_down) << 7; + state->buttons |= KBD_STATE(back) << 8; + state->buttons |= KBD_STATE(start) << 9; + state->buttons |= KBD_STATE(white) << 10; + state->buttons |= KBD_STATE(black) << 11; + state->buttons |= KBD_STATE(lstick_btn) << 12; + state->buttons |= KBD_STATE(rstick_btn) << 13; + state->buttons |= KBD_STATE(guide) << 14; - if (kbd[sdl_kbd_scancode_map[20]]) state->axis[CONTROLLER_AXIS_RSTICK_Y] = 32767; - if (kbd[sdl_kbd_scancode_map[21]]) state->axis[CONTROLLER_AXIS_RSTICK_X] = -32768; - if (kbd[sdl_kbd_scancode_map[22]]) state->axis[CONTROLLER_AXIS_RSTICK_X] = 32767; - if (kbd[sdl_kbd_scancode_map[23]]) state->axis[CONTROLLER_AXIS_RSTICK_Y] = -32768; - if (kbd[sdl_kbd_scancode_map[24]]) state->axis[CONTROLLER_AXIS_RTRIG] = 32767; + if (KBD_STATE(lstick_up)) + state->axis[CONTROLLER_AXIS_LSTICK_Y] = 32767; + if (KBD_STATE(lstick_left)) + state->axis[CONTROLLER_AXIS_LSTICK_X] = -32768; + if (KBD_STATE(lstick_right)) + state->axis[CONTROLLER_AXIS_LSTICK_X] = 32767; + if (KBD_STATE(lstick_down)) + state->axis[CONTROLLER_AXIS_LSTICK_Y] = -32768; + if (KBD_STATE(ltrigger)) + state->axis[CONTROLLER_AXIS_LTRIG] = 32767; + + if (KBD_STATE(rstick_up)) + state->axis[CONTROLLER_AXIS_RSTICK_Y] = 32767; + if (KBD_STATE(rstick_left)) + state->axis[CONTROLLER_AXIS_RSTICK_X] = -32768; + if (KBD_STATE(rstick_right)) + state->axis[CONTROLLER_AXIS_RSTICK_X] = 32767; + if (KBD_STATE(rstick_down)) + state->axis[CONTROLLER_AXIS_RSTICK_Y] = -32768; + if (KBD_STATE(rtrigger)) + state->axis[CONTROLLER_AXIS_RTRIG] = 32767; + +#undef KBD_STATE } void xemu_input_update_sdl_controller_state(ControllerState *state) @@ -449,48 +539,76 @@ void xemu_input_update_sdl_controller_state(ControllerState *state) state->buttons = 0; memset(state->axis, 0, sizeof(state->axis)); - const SDL_GameControllerButton sdl_button_map[15] = { - SDL_CONTROLLER_BUTTON_A, - SDL_CONTROLLER_BUTTON_B, - SDL_CONTROLLER_BUTTON_X, - SDL_CONTROLLER_BUTTON_Y, - SDL_CONTROLLER_BUTTON_DPAD_LEFT, - SDL_CONTROLLER_BUTTON_DPAD_UP, - SDL_CONTROLLER_BUTTON_DPAD_RIGHT, - SDL_CONTROLLER_BUTTON_DPAD_DOWN, - SDL_CONTROLLER_BUTTON_BACK, - SDL_CONTROLLER_BUTTON_START, - SDL_CONTROLLER_BUTTON_LEFTSHOULDER, - SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, - SDL_CONTROLLER_BUTTON_LEFTSTICK, - SDL_CONTROLLER_BUTTON_RIGHTSTICK, - SDL_CONTROLLER_BUTTON_GUIDE - }; +#define SDL_MASK_BUTTON(state, btn, idx) \ + (SDL_GameControllerGetButton( \ + (state)->sdl_gamecontroller, \ + (state)->controller_map->controller_mapping.btn) \ + << idx) - for (int i = 0; i < 15; i++) { - state->buttons |= SDL_GameControllerGetButton(state->sdl_gamecontroller, sdl_button_map[i]) << i; + state->buttons |= SDL_MASK_BUTTON(state, a, 0); + state->buttons |= SDL_MASK_BUTTON(state, b, 1); + state->buttons |= SDL_MASK_BUTTON(state, x, 2); + state->buttons |= SDL_MASK_BUTTON(state, y, 3); + state->buttons |= SDL_MASK_BUTTON(state, dpad_left, 4); + state->buttons |= SDL_MASK_BUTTON(state, dpad_up, 5); + state->buttons |= SDL_MASK_BUTTON(state, dpad_right, 6); + state->buttons |= SDL_MASK_BUTTON(state, dpad_down, 7); + state->buttons |= SDL_MASK_BUTTON(state, back, 8); + state->buttons |= SDL_MASK_BUTTON(state, start, 9); + state->buttons |= SDL_MASK_BUTTON(state, lshoulder, 10); + state->buttons |= SDL_MASK_BUTTON(state, rshoulder, 11); + state->buttons |= SDL_MASK_BUTTON(state, lstick_btn, 12); + state->buttons |= SDL_MASK_BUTTON(state, rstick_btn, 13); + state->buttons |= SDL_MASK_BUTTON(state, guide, 14); + +#undef SDL_MASK_BUTTON + +#define SDL_GET_AXIS(state, axis) \ + SDL_GameControllerGetAxis( \ + (state)->sdl_gamecontroller, \ + (state)->controller_map->controller_mapping.axis) + + state->axis[0] = SDL_GET_AXIS(state, axis_trigger_left); + state->axis[1] = SDL_GET_AXIS(state, axis_trigger_right); + state->axis[2] = SDL_GET_AXIS(state, axis_left_x); + state->axis[3] = SDL_GET_AXIS(state, axis_left_y); + state->axis[4] = SDL_GET_AXIS(state, axis_right_x); + state->axis[5] = SDL_GET_AXIS(state, axis_right_y); + +#undef SDL_GET_AXIS + +// FIXME: Check range +#define INVERT_AXIS(controller_axis) \ + state->axis[controller_axis] = -1 - state->axis[controller_axis] + + if (state->controller_map->controller_mapping.invert_axis_left_x) { + INVERT_AXIS(CONTROLLER_AXIS_LSTICK_X); } - const SDL_GameControllerAxis sdl_axis_map[6] = { - SDL_CONTROLLER_AXIS_TRIGGERLEFT, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, - SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, - SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, - }; - - for (int i = 0; i < 6; i++) { - state->axis[i] = SDL_GameControllerGetAxis(state->sdl_gamecontroller, sdl_axis_map[i]); + if (!state->controller_map->controller_mapping.invert_axis_left_y) { + INVERT_AXIS(CONTROLLER_AXIS_LSTICK_Y); } - // FIXME: Check range - state->axis[CONTROLLER_AXIS_LSTICK_Y] = -1 - state->axis[CONTROLLER_AXIS_LSTICK_Y]; - state->axis[CONTROLLER_AXIS_RSTICK_Y] = -1 - state->axis[CONTROLLER_AXIS_RSTICK_Y]; + if (state->controller_map->controller_mapping.invert_axis_right_x) { + INVERT_AXIS(CONTROLLER_AXIS_RSTICK_X); + } + + if (!state->controller_map->controller_mapping.invert_axis_right_y) { + INVERT_AXIS(CONTROLLER_AXIS_RSTICK_Y); + } + +#undef INVERT_AXIS // xemu_input_print_controller_state(state); } void xemu_input_update_rumble(ControllerState *state) { - if (!state->rumble_enabled || !g_config.input.allow_vibration) { + if (state->type != INPUT_DEVICE_SDL_GAMECONTROLLER) { + return; + } + + if (!state->controller_map->enable_rumble) { return; } @@ -794,3 +912,12 @@ int xemu_input_get_test_mode(void) { return test_mode; } + +void xemu_input_reset_input_mapping(ControllerState *state) +{ + if (state->type == INPUT_DEVICE_SDL_GAMECONTROLLER) { + xemu_input_bindings_reload_controller_map(state, /*reset_to_default=*/true); + } else if (state->type == INPUT_DEVICE_SDL_KEYBOARD) { + xemu_settings_reset_keyboard_mapping(); + } +} diff --git a/ui/xemu-input.h b/ui/xemu-input.h index 23c1a9f91b..97694d667f 100644 --- a/ui/xemu-input.h +++ b/ui/xemu-input.h @@ -29,6 +29,8 @@ #include #include "qemu/queue.h" +#include "xemu-settings.h" +#include #define DRIVER_DUKE "usb-xbox-gamepad" #define DRIVER_S "usb-xbox-gamepad-s" @@ -66,6 +68,12 @@ enum controller_state_axis_index { CONTROLLER_AXIS__COUNT, }; +#ifdef __cplusplus +using GamepadMappings = struct config::input::gamepad_mappings; +#else +typedef struct gamepad_mappings GamepadMappings; +#endif + enum controller_input_device_type { INPUT_DEVICE_SDL_KEYBOARD, INPUT_DEVICE_SDL_GAMECONTROLLER, @@ -93,7 +101,6 @@ typedef struct ControllerState { uint32_t animate_trigger_end; // Rumble state - bool rumble_enabled; uint16_t rumble_l, rumble_r; enum controller_input_device_type type; @@ -106,6 +113,8 @@ typedef struct ControllerState { enum peripheral_type peripheral_types[2]; void *peripherals[2]; + GamepadMappings *controller_map; + int bound; // Which port this input device is bound to void *device; // DeviceState opaque } ControllerState; @@ -139,6 +148,7 @@ void xemu_save_peripheral_settings(int player_index, int peripheral_index, void xemu_input_set_test_mode(int enabled); int xemu_input_get_test_mode(void); +void xemu_input_reset_input_mapping(ControllerState *state); #ifdef __cplusplus } diff --git a/ui/xemu-settings.cc b/ui/xemu-settings.cc index 5237d5682b..08b0b8d038 100644 --- a/ui/xemu-settings.cc +++ b/ui/xemu-settings.cc @@ -31,6 +31,7 @@ #include #include +#include "xemu-controllers.h" #include "xemu-settings.h" #define DEFINE_CONFIG_TREE @@ -217,6 +218,11 @@ void xemu_settings_save(void) /* Ensure numeric values are printed with '.' radix, no grouping */ setlocale(LC_NUMERIC, "C"); + // The global controller vibration setting is replaced with a per-controller config. + // xemu_settings_load_gamepad_mapping should have migrated that setting to any connected + // controller, so we can set it to true (default) now to remove it from the user config. + g_config.input.allow_vibration = true; + config_tree.update_from_struct(&g_config); fprintf(fd, "%s", config_tree.generate_delta_toml().c_str()); fclose(fd); @@ -254,3 +260,57 @@ void remove_net_nat_forward_ports(unsigned int index) cnode->free_allocations(&g_config); cnode->store_to_struct(&g_config); } + +GamepadMappings *xemu_settings_load_gamepad_mapping(const char *guid, bool reset_to_default) +{ + unsigned int i; + unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count; + for (i = 0; i < gamepad_mappings_count; ++i) { + auto *mapping = &g_config.input.gamepad_mappings[i]; + if (strcmp(mapping->gamepad_id, guid) != 0) { + continue; + } + + if (reset_to_default) { + break; + } + + // Migrate global 'allow_vibration' setting to the controller config + mapping->enable_rumble = g_config.input.allow_vibration; + + return mapping; + } + + auto cnode = config_tree.child("input")->child("gamepad_mappings"); + cnode->update_from_struct(&g_config); + cnode->free_allocations(&g_config); + + CNode *mapping_node; + if (reset_to_default && i < gamepad_mappings_count) { + mapping_node = &cnode->children[i]; + mapping_node->reset_to_defaults(); + } else { + cnode->children.push_back(*cnode->array_item_type); + mapping_node = &cnode->children.back(); + } + + mapping_node->child("gamepad_id")->set_string(guid); + + cnode->store_to_struct(&g_config); + + auto *mapping = &g_config.input + .gamepad_mappings[g_config.input.gamepad_mappings_count - 1]; + + // Migrate global 'allow_vibration' setting to the controller config + mapping->enable_rumble = g_config.input.allow_vibration; + + return mapping; +} + +void xemu_settings_reset_keyboard_mapping(void) +{ + auto cnode = config_tree.child("input")->child("keyboard_controller_scancode_map"); + cnode->update_from_struct(&g_config); + cnode->reset_to_defaults(); + cnode->store_to_struct(&g_config); +} diff --git a/ui/xemu-settings.h b/ui/xemu-settings.h index 491e5ce8de..3a9b8b2ed9 100644 --- a/ui/xemu-settings.h +++ b/ui/xemu-settings.h @@ -67,6 +67,9 @@ static inline void xemu_settings_set_string(const char **str, const char *new_st void add_net_nat_forward_ports(int host, int guest, CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol); void remove_net_nat_forward_ports(unsigned int index); +// Reset keyboard mappings to default settings. +void xemu_settings_reset_keyboard_mapping(void); + #ifdef __cplusplus } #endif diff --git a/ui/xemu.c b/ui/xemu.c index 81a951d35c..3131d52159 100644 --- a/ui/xemu.c +++ b/ui/xemu.c @@ -558,8 +558,10 @@ void sdl2_poll_events(struct sdl2_console *scon) xemu_hud_should_capture_kbd_mouse(&kbd, &mouse); while (SDL_PollEvent(ev)) { - xemu_input_process_sdl_events(ev); + // HUD must process events first so that if a controller is detached, + // a latent rebind request can cancel before the state is freed xemu_hud_process_sdl_events(ev); + xemu_input_process_sdl_events(ev); switch (ev->type) { case SDL_KEYDOWN: diff --git a/ui/xui/input-manager.cc b/ui/xui/input-manager.cc index 786b54b176..5f9de128fc 100644 --- a/ui/xui/input-manager.cc +++ b/ui/xui/input-manager.cc @@ -1,6 +1,7 @@ -#include "common.hh" +#include "ui/xui/main-menu.hh" #include "input-manager.hh" #include "../xemu-input.h" +#include "common.hh" InputManager g_input_mgr; @@ -18,14 +19,18 @@ void InputManager::Update() m_buttons = 0; int16_t axis[CONTROLLER_AXIS__COUNT] = {0}; - ControllerState *iter; - QTAILQ_FOREACH(iter, &available_controllers, entry) { - if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) continue; - m_buttons |= iter->buttons; - // We simply take any axis that is >10 % activation - for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) { - if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) { - axis[i] = iter->axis[i]; + // If we are rebinding a controller, prevent navigation + if (!g_main_menu.IsInputRebinding()) { + ControllerState *iter; + QTAILQ_FOREACH (iter, &available_controllers, entry) { + if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) + continue; + m_buttons |= iter->buttons; + // We simply take any axis that is >10 % activation + for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) { + if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) { + axis[i] = iter->axis[i]; + } } } } diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index 4524d9b450..e11a115d79 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -47,7 +47,9 @@ MainMenuScene g_main_menu; MainMenuTabView::~MainMenuTabView() {} -void MainMenuTabView::Draw() {} +void MainMenuTabView::Draw() +{ +} void MainMenuGeneralView::Draw() { @@ -76,6 +78,25 @@ void MainMenuGeneralView::Draw() // "Limit DVD/HDD throughput to approximate Xbox load times"); } +bool MainMenuInputView::ConsumeRebindEvent(SDL_Event *event) +{ + if (!m_rebinding) { + return false; + } + + RebindEventResult rebind_result = m_rebinding->ConsumeRebindEvent(event); + if (rebind_result == RebindEventResult::Complete) { + m_rebinding = nullptr; + } + + return rebind_result == RebindEventResult::Ignore; +} + +bool MainMenuInputView::IsInputRebinding() +{ + return m_rebinding != nullptr; +} + void MainMenuInputView::Draw() { SectionTitle("Controllers"); @@ -144,6 +165,7 @@ void MainMenuInputView::Draw() if (activated) { active = i; + m_rebinding = nullptr; } uint32_t port_color = 0xafafafff; @@ -185,10 +207,8 @@ void MainMenuInputView::Draw() if (ImGui::BeginCombo("###InputDrivers", driver, ImGuiComboFlags_NoArrowButton)) { const char *available_drivers[] = { DRIVER_DUKE, DRIVER_S }; - const char *driver_display_names[] = { - DRIVER_DUKE_DISPLAY_NAME, - DRIVER_S_DISPLAY_NAME - }; + const char *driver_display_names[] = { DRIVER_DUKE_DISPLAY_NAME, + DRIVER_S_DISPLAY_NAME }; bool is_selected = false; int num_drivers = sizeof(driver_display_names) / sizeof(driver_display_names[0]); for (int i = 0; i < num_drivers; i++) { @@ -298,7 +318,7 @@ void MainMenuInputView::Draw() device_selected = true; RenderController(0, 0, 0x81dc8a00, 0x0f0f0f00, bound_state); } else { - static ControllerState state = { 0 }; + static ControllerState state{}; RenderController(0, 0, 0x1f1f1f00, 0x0f0f0f00, &state); } @@ -496,16 +516,221 @@ void MainMenuInputView::Draw() ImGui::Columns(1); } + SectionTitle("Mapping"); + ImVec4 tc = ImGui::GetStyle().Colors[ImGuiCol_Header]; + tc.w = 0.0f; + ImGui::PushStyleColor(ImGuiCol_Header, tc); + + if (ImGui::CollapsingHeader("Input Mapping")) { + float p = ImGui::GetFrameHeight() * 0.3; + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(p, p)); + if (ImGui::BeginTable("input_remap_tbl", 2, + ImGuiTableFlags_RowBg | + ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("Emulated Input"); + ImGui::TableSetupColumn("Host Input"); + ImGui::TableHeadersRow(); + + PopulateTableController(bound_state); + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + + if (bound_state && bound_state->type == INPUT_DEVICE_SDL_GAMECONTROLLER) { + Toggle("Enable Rumble", &bound_state->controller_map->enable_rumble); + Toggle("Invert Left X Axis", + &bound_state->controller_map->controller_mapping + .invert_axis_left_x); + Toggle("Invert Left Y Axis", + &bound_state->controller_map->controller_mapping + .invert_axis_left_y); + Toggle("Invert Right X Axis", + &bound_state->controller_map->controller_mapping + .invert_axis_right_x); + Toggle("Invert Right Y Axis", + &bound_state->controller_map->controller_mapping + .invert_axis_right_y); + } + + if (ImGui::Button("Reset to Default")) { + xemu_input_reset_input_mapping(bound_state); + } + + ImGui::PopStyleColor(); + SectionTitle("Options"); Toggle("Auto-bind controllers", &g_config.input.auto_bind, "Bind newly connected controllers to any open port"); - Toggle("Controller vibration", &g_config.input.allow_vibration, - "Allows the controllers to vibrate"); Toggle("Background controller input capture", &g_config.input.background_input_capture, "Capture even if window is unfocused (requires restart)"); } +void MainMenuInputView::Hide() +{ + m_rebinding = nullptr; +} + +void MainMenuInputView::PopulateTableController(ControllerState *state) +{ + if (!state) + return; + + // Must match g_keyboard_scancode_map and the controller + // button map below. + static constexpr const char *face_button_index_to_name_map[15] = { + "A", + "B", + "X", + "Y", + "Back", + "Guide", + "Start", + "Left Stick Button", + "Right Stick Button", + "White", + "Black", + "DPad Up", + "DPad Down", + "DPad Left", + "DPad Right", + }; + + // Must match g_keyboard_scancode_map[15:]. Each axis requires + // two keys for the positive and negative direction with the + // exception of the triggers, which only require one each. + static constexpr const char *keyboard_stick_index_to_name_map[10] = { + "Left Stick Up", + "Left Stick Left", + "Left Stick Right", + "Left Stick Down", + "Left Trigger", + "Right Stick Up", + "Right Stick Left", + "Right Stick Right", + "Right Stick Down", + "Right Trigger", + }; + + // Must match controller axis map below. + static constexpr const char *gamepad_axis_index_to_name_map[6] = { + "Left Stick Axis X", + "Left Stick Axis Y", + "Right Stick Axis X", + "Right Stick Axis Y", + "Left Trigger Axis", + "Right Trigger Axis", + }; + + bool is_keyboard = state->type == INPUT_DEVICE_SDL_KEYBOARD; + + int num_axis_mappings; + const char *const *axis_index_to_name_map; + if (is_keyboard) { + num_axis_mappings = std::size(keyboard_stick_index_to_name_map); + axis_index_to_name_map = keyboard_stick_index_to_name_map; + } else { + num_axis_mappings = std::size(gamepad_axis_index_to_name_map); + axis_index_to_name_map = gamepad_axis_index_to_name_map; + } + + constexpr int num_face_buttons = std::size(face_button_index_to_name_map); + const int table_rows = num_axis_mappings + num_face_buttons; + for (int i = 0; i < table_rows; ++i) { + ImGui::TableNextRow(); + + // Button/Axis Name Column + ImGui::TableSetColumnIndex(0); + + if (i < num_face_buttons) { + ImGui::Text("%s", face_button_index_to_name_map[i]); + } else { + ImGui::Text("%s", axis_index_to_name_map[i - num_face_buttons]); + } + + // Button Binding Column + ImGui::TableSetColumnIndex(1); + + if (m_rebinding && m_rebinding->GetTableRow() == i) { + ImGui::Text("Press a key to rebind"); + continue; + } + + const char *remap_button_text = "Invalid"; + if (is_keyboard) { + // g_keyboard_scancode_map includes both face buttons and axis buttons. + int keycode = *(g_keyboard_scancode_map[i]); + if (keycode != SDL_SCANCODE_UNKNOWN) { + remap_button_text = + SDL_GetScancodeName(static_cast(keycode)); + } + } else if (i < num_face_buttons) { + int *button_map[num_face_buttons] = { + &state->controller_map->controller_mapping.a, + &state->controller_map->controller_mapping.b, + &state->controller_map->controller_mapping.x, + &state->controller_map->controller_mapping.y, + &state->controller_map->controller_mapping.back, + &state->controller_map->controller_mapping.guide, + &state->controller_map->controller_mapping.start, + &state->controller_map->controller_mapping.lstick_btn, + &state->controller_map->controller_mapping.rstick_btn, + &state->controller_map->controller_mapping.lshoulder, + &state->controller_map->controller_mapping.rshoulder, + &state->controller_map->controller_mapping.dpad_up, + &state->controller_map->controller_mapping.dpad_down, + &state->controller_map->controller_mapping.dpad_left, + &state->controller_map->controller_mapping.dpad_right, + }; + + int button = *(button_map[i]); + if (button != SDL_CONTROLLER_BUTTON_INVALID) { + remap_button_text = SDL_GameControllerGetStringForButton( + static_cast(button)); + } + } else { + int *axis_map[6] = { + &state->controller_map->controller_mapping.axis_left_x, + &state->controller_map->controller_mapping.axis_left_y, + &state->controller_map->controller_mapping.axis_right_x, + &state->controller_map->controller_mapping.axis_right_y, + &state->controller_map->controller_mapping + .axis_trigger_left, + &state->controller_map->controller_mapping + .axis_trigger_right, + }; + int axis = *(axis_map[i - num_face_buttons]); + if (axis != SDL_CONTROLLER_AXIS_INVALID) { + remap_button_text = SDL_GameControllerGetStringForAxis( + static_cast(axis)); + } + } + + ImGui::PushID(i); + float tw = ImGui::CalcTextSize(remap_button_text).x; + auto &style = ImGui::GetStyle(); + float max_button_width = + tw + g_viewport_mgr.m_scale * 2 * style.FramePadding.x; + + float min_button_width = ImGui::GetColumnWidth(1) / 2; + float button_width = std::max(min_button_width, max_button_width); + + if (ImGui::Button(remap_button_text, ImVec2(button_width, 0))) { + if (is_keyboard) { + m_rebinding = + std::make_unique(i); + } else { + m_rebinding = + std::make_unique(i, + state); + } + } + ImGui::PopID(); + } +} + void MainMenuDisplayView::Draw() { SectionTitle("Renderer"); @@ -1532,6 +1757,7 @@ void MainMenuScene::Show() void MainMenuScene::Hide() { + m_views[m_current_view_index]->Hide(); m_background.Hide(); m_nav_control_view.Hide(); m_animation.EaseOut(); @@ -1544,6 +1770,7 @@ bool MainMenuScene::IsAnimating() void MainMenuScene::SetNextViewIndex(int i) { + m_views[m_current_view_index]->Hide(); m_next_view_index = i % m_tabs.size(); g_config.general.last_viewed_menu_index = i; } @@ -1583,6 +1810,16 @@ void MainMenuScene::UpdateAboutViewConfigInfo() m_about_view.UpdateConfigInfoText(); } +bool MainMenuScene::ConsumeRebindEvent(SDL_Event *event) +{ + return m_input_view.ConsumeRebindEvent(event); +} + +bool MainMenuScene::IsInputRebinding() +{ + return m_input_view.IsInputRebinding(); +} + bool MainMenuScene::Draw() { m_animation.Step(); diff --git a/ui/xui/main-menu.hh b/ui/xui/main-menu.hh index bce3927333..dccb4da771 100644 --- a/ui/xui/main-menu.hh +++ b/ui/xui/main-menu.hh @@ -25,6 +25,8 @@ #include "scene.hh" #include "scene-components.hh" #include "../xemu-snapshots.h" +#include "../xemu-controllers.h" +#include "ui/xemu-input.h" extern "C" { #include "net/pcap.h" @@ -36,6 +38,9 @@ class MainMenuTabView public: virtual ~MainMenuTabView(); virtual void Draw(); + virtual void Hide() + { + } }; class MainMenuGeneralView : public virtual MainMenuTabView @@ -47,7 +52,16 @@ public: class MainMenuInputView : public virtual MainMenuTabView { public: + std::unique_ptr m_rebinding; + + MainMenuInputView() : m_rebinding{ nullptr } + { + } + bool ConsumeRebindEvent(SDL_Event *event); + bool IsInputRebinding(); void Draw() override; + void Hide() override; + void PopulateTableController(ControllerState *state); }; class MainMenuDisplayView : public virtual MainMenuTabView @@ -193,6 +207,8 @@ public: void SetNextViewIndex(int i); void HandleInput(); void UpdateAboutViewConfigInfo(); + bool ConsumeRebindEvent(SDL_Event *event); + bool IsInputRebinding(); bool Draw() override; }; diff --git a/ui/xui/main.cc b/ui/xui/main.cc index 07441f3a64..6039d65b00 100644 --- a/ui/xui/main.cc +++ b/ui/xui/main.cc @@ -170,6 +170,11 @@ void xemu_hud_cleanup(void) void xemu_hud_process_sdl_events(SDL_Event *event) { + // Ignore inputs that are consumed by rebinding + if (g_main_menu.ConsumeRebindEvent(event)) { + return; + } + ImGui_ImplSDL2_ProcessEvent(event); }