ui: Add controller/keyboard input mapping configuration

This commit is contained in:
antangelo
2025-12-25 14:54:30 -07:00
committed by GitHub
parent d0478b6154
commit aed1fa4ac6
13 changed files with 874 additions and 96 deletions
+86
View File
@@ -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:
+1
View File
@@ -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',
+141
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "xemu-controllers.h"
#include "xemu-settings.h"
#include <assert.h>
#include <cmath>
#include <limits>
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<Sint16>::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;
}
}
+85
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef XEMU_CONTROLLERS_H
#define XEMU_CONTROLLERS_H
#include "xemu-input.h"
#include <SDL.h>
#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
+204 -77
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#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();
}
}
+11 -1
View File
@@ -29,6 +29,8 @@
#include <stdbool.h>
#include "qemu/queue.h"
#include "xemu-settings.h"
#include <SDL2/SDL.h>
#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
}
+60
View File
@@ -31,6 +31,7 @@
#include <iostream>
#include <locale.h>
#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);
}
+3
View File
@@ -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
+3 -1
View File
@@ -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:
+14 -9
View File
@@ -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];
}
}
}
}
+245 -8
View File
@@ -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<SDL_Scancode>(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<SDL_GameControllerButton>(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<SDL_GameControllerAxis>(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<ControllerKeyboardRebindingMap>(i);
} else {
m_rebinding =
std::make_unique<ControllerGamepadRebindingMap>(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();
+16
View File
@@ -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<RebindingMap> 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;
};
+5
View File
@@ -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);
}