Skip to content

Commit

Permalink
Improve gamepad support on Linux
Browse files Browse the repository at this point in the history
Previously, the controller input mapping relied on `core/input/gamecontrollerdb.txt`,
so if the device is newer, the device input mapping may be confused.

For gamepads not documented in this file, they are now simply mapped according to the
enumeration semantics.

Reference: https://docs.kernel.org/input/gamepad.html#linux-gamepad-specification

Add a project setting so that users can decide whether to auto map.
  • Loading branch information
Rindbee committed Aug 22, 2024
1 parent 8ef2c3d commit 0aaeed5
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 2 deletions.
59 changes: 58 additions & 1 deletion core/input/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,63 @@ void Input::set_event_dispatch_function(EventDispatchFunc p_function) {
event_dispatch_function = p_function;
}

bool Input::has_mapping(const StringName &p_guid) {
for (int i = 0; i < map_db.size(); i++) {
if (map_db[i].uid == p_guid) {
return true;
}
}
return false;
}

void Input::set_unknown_gamepad_auto_mapped(bool p_auto) {
unknown_gamepad_auto_mapped = p_auto;
}

bool Input::is_unknown_gamepad_auto_mapped() {
return unknown_gamepad_auto_mapped;
}

void Input::unknown_gamepad_auto_map(const StringName &p_guid, const String &p_name, const int *p_key_map, const int *p_axis_map, bool p_trigger_is_key) {
JoyDeviceMapping mapping;
mapping.uid = p_guid;
mapping.name = p_name + "[auto]";

for (int i = 0; i < int(JoyButton::SDL_MAX); i++) {
JoyBinding binding;

binding.outputType = TYPE_BUTTON;
binding.output.button = JoyButton(i);

binding.inputType = TYPE_BUTTON;
binding.input.button = JoyButton(p_key_map[i]);

mapping.bindings.push_back(binding);
}

for (int i = 0; i < int(JoyAxis::SDL_MAX); i++) {
JoyBinding binding;

binding.outputType = TYPE_AXIS;
binding.output.axis.axis = JoyAxis(i);
binding.output.axis.range = JoyAxisRange::FULL_AXIS;

if (p_trigger_is_key && i >= int(JoyAxis::SDL_MAX) - 2) {
binding.inputType = TYPE_BUTTON;
binding.input.button = JoyButton(p_axis_map[i]);
} else {
binding.inputType = TYPE_AXIS;
binding.input.axis.axis = JoyAxis(p_axis_map[i]);
binding.input.axis.range = JoyAxisRange::FULL_AXIS;
binding.input.axis.invert = false;
}

mapping.bindings.push_back(binding);
}

map_db.push_back(mapping);
}

void Input::joy_button(int p_device, JoyButton p_button, bool p_pressed) {
_THREAD_SAFE_METHOD_;
Joypad &joy = joy_names[p_device];
Expand Down Expand Up @@ -1619,7 +1676,7 @@ void Input::set_fallback_mapping(const String &p_guid) {
bool Input::is_joy_known(int p_device) {
if (joy_names.has(p_device)) {
int mapping = joy_names[p_device].mapping;
if (mapping != -1 && mapping != fallback_mapping) {
if (mapping != -1 && mapping != fallback_mapping && !map_db[mapping].name.ends_with("[auto]")) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions core/input/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class Input : public Object {

HashMap<StringName, ActionState> action_states;

bool unknown_gamepad_auto_mapped = true;
bool emulate_touch_from_mouse = false;
bool emulate_mouse_from_touch = false;
bool agile_input_event_flushing = false;
Expand Down Expand Up @@ -286,6 +287,11 @@ class Input : public Object {

static Input *get_singleton();

bool has_mapping(const StringName &p_guid);
void set_unknown_gamepad_auto_mapped(bool p_auto);
bool is_unknown_gamepad_auto_mapped();
void unknown_gamepad_auto_map(const StringName &p_guid, const String &p_name, const int *p_key_map, const int *p_axis_map, bool p_trigger_is_key);

bool is_anything_pressed() const;
bool is_key_pressed(Key p_keycode) const;
bool is_physical_key_pressed(Key p_keycode) const;
Expand Down
2 changes: 2 additions & 0 deletions doc/classes/Input.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
<param index="0" name="device" type="int" />
<description>
Returns the name of the joypad at the specified device index, e.g. [code]PS4 Controller[/code]. Godot uses the [url=https://github.com/gabomdq/SDL_GameControllerDB]SDL2 game controller database[/url] to determine gamepad names.
[b]Note:[/b] For device whose name ends with [code]"[auto]"[/code], its mapping was auto added. See [member ProjectSettings.input_devices/gamepad/unknown_gamepad_auto_mapped].
</description>
</method>
<method name="get_joy_vibration_duration">
Expand Down Expand Up @@ -248,6 +249,7 @@
<param index="0" name="device" type="int" />
<description>
Returns [code]true[/code] if the system knows the specified device. This means that it sets all button and axis indices. Unknown joypads are not expected to match these constants, but you can still retrieve events from them.
[b]Note:[/b] Unknown gamepads meight try to auto mapped. See [member ProjectSettings.input_devices/gamepad/unknown_gamepad_auto_mapped]. Even if they are mapped correctly, they are still considered unknown, i.e. still returns [code]false[/code] for them.
</description>
</method>
<method name="is_key_label_pressed" qualifiers="const">
Expand Down
5 changes: 5 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,11 @@
If [code]false[/code], no input will be lost.
[b]Note:[/b] You should in nearly all cases prefer the [code]false[/code] setting. The legacy behavior is to enable supporting old projects that rely on the old logic, without changes to script.
</member>
<member name="input_devices/gamepad/unknown_gamepad_auto_mapped" type="bool" setter="" getter="" default="true">
If [code]true[/code], allows unknown gamepads to be auto mapped according to keycode semantics and convention.
[b]Note:[/b] Auto mapped gamepad will have [code]"[auto]"[/code] appended to the end of its name.
[b]Note:[/b] Currently implemented only on Linux.
</member>
<member name="input_devices/pen_tablet/driver" type="String" setter="" getter="">
Specifies the tablet driver to use. If left empty, the default driver will be used.
[b]Note:[/b] The driver in use can be overridden at runtime via the [code]--tablet-driver[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url].
Expand Down
3 changes: 3 additions & 0 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3051,6 +3051,9 @@ Error Main::setup2(bool p_show_boot_logo) {
bool agile_input_event_flushing = GLOBAL_DEF("input_devices/buffering/agile_event_flushing", false);
id->set_agile_input_event_flushing(agile_input_event_flushing);

bool unknown_gamepad_auto_mapped = GLOBAL_DEF("input_devices/gamepad/unknown_gamepad_auto_mapped", true);
id->set_unknown_gamepad_auto_mapped(unknown_gamepad_auto_mapped);

if (bool(GLOBAL_DEF_BASIC("input_devices/pointing/emulate_touch_from_mouse", false)) &&
!(editor || project_manager)) {
if (!DisplayServer::get_singleton()->is_touchscreen_available()) {
Expand Down
123 changes: 122 additions & 1 deletion platform/linuxbsd/joypad_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ JoypadLinux::Joypad::~Joypad() {
void JoypadLinux::Joypad::reset() {
dpad = 0;
fd = -1;
for (int i = 0; i < MAX_KEY; i++) {
key_map[i] = -1;
}
for (int i = 0; i < MAX_ABS; i++) {
abs_map[i] = -1;
curr_axis[i] = 0;
Expand Down Expand Up @@ -314,6 +317,7 @@ void JoypadLinux::setup_joypad_properties(Joypad &p_joypad) {
p_joypad.key_map[i] = num_buttons++;
}
}

for (int i = 0; i < ABS_MISC; ++i) {
/* Skip hats */
if (i == ABS_HAT0X) {
Expand All @@ -340,6 +344,108 @@ void JoypadLinux::setup_joypad_properties(Joypad &p_joypad) {
}
}

void JoypadLinux::_auto_remap(Joypad &p_joypad, const StringName &p_guid, const String &p_name, bool p_hat0x_exist, bool p_hat0y_exist) {
if (p_joypad.key_map[BTN_GAMEPAD] == -1) {
return;
}

// Generate key mapping for JoyButton.

int joy_button_mappings[int(JoyButton::SDL_MAX)];
for (int i = 0; i < int(JoyButton::SDL_MAX); i++) {
joy_button_mappings[i] = -1;
}

#define BUTTON_MAP_KEY(button, keycode) (joy_button_mappings[int(button)] = p_joypad.key_map[keycode])
#define BUTTON_MAPPED(button) (joy_button_mappings[int(button)] != int(JoyButton::INVALID))
#define UNUSED_KEY_HIDE(keycode) (p_joypad.key_map[keycode] = -p_joypad.key_map[keycode] - 2) // Used for two events occur at one key press.

BUTTON_MAP_KEY(JoyButton::A, BTN_A);
BUTTON_MAP_KEY(JoyButton::B, BTN_B);
BUTTON_MAP_KEY(JoyButton::X, BTN_X);
BUTTON_MAP_KEY(JoyButton::Y, BTN_Y);

BUTTON_MAP_KEY(JoyButton::BACK, KEY_BACK);
BUTTON_MAP_KEY(JoyButton::GUIDE, KEY_HOMEPAGE);
if (!BUTTON_MAPPED(JoyButton::BACK)) {
BUTTON_MAP_KEY(JoyButton::BACK, BTN_SELECT);
}
if (!BUTTON_MAPPED(JoyButton::GUIDE)) {
BUTTON_MAP_KEY(JoyButton::GUIDE, BTN_MODE);
}

BUTTON_MAP_KEY(JoyButton::START, BTN_START);
BUTTON_MAP_KEY(JoyButton::LEFT_STICK, BTN_THUMBL);
BUTTON_MAP_KEY(JoyButton::RIGHT_STICK, BTN_THUMBR);
BUTTON_MAP_KEY(JoyButton::LEFT_SHOULDER, BTN_TL);
BUTTON_MAP_KEY(JoyButton::RIGHT_SHOULDER, BTN_TR);

if (!p_hat0y_exist) {
BUTTON_MAP_KEY(JoyButton::DPAD_UP, BTN_DPAD_UP);
BUTTON_MAP_KEY(JoyButton::DPAD_DOWN, BTN_DPAD_DOWN);
} else {
UNUSED_KEY_HIDE(BTN_DPAD_UP);
UNUSED_KEY_HIDE(BTN_DPAD_DOWN);
}
if (!p_hat0x_exist) {
BUTTON_MAP_KEY(JoyButton::DPAD_LEFT, BTN_DPAD_LEFT);
BUTTON_MAP_KEY(JoyButton::DPAD_RIGHT, BTN_DPAD_RIGHT);
} else {
UNUSED_KEY_HIDE(BTN_DPAD_LEFT);
UNUSED_KEY_HIDE(BTN_DPAD_RIGHT);
}

BUTTON_MAP_KEY(JoyButton::MISC1, KEY_RECORD);
if (!BUTTON_MAPPED(JoyButton::MISC1)) {
BUTTON_MAP_KEY(JoyButton::MISC1, BTN_Z);
}

// Generate key mapping for JoyAxis.

int joy_axis_mappings[int(JoyAxis::SDL_MAX)];
for (int i = 0; i < int(JoyAxis::SDL_MAX); i++) {
joy_axis_mappings[i] = -1;
}

#define AXIS_MAP_ABS(axis, abscode) (joy_axis_mappings[int(axis)] = p_joypad.abs_map[abscode])
#define AXIS_MAP_KEY(axis, keycode) (joy_axis_mappings[int(axis)] = p_joypad.key_map[keycode])
#define AXIS_MAPPED(axis) (joy_axis_mappings[int(axis)] != -1)

AXIS_MAP_ABS(JoyAxis::LEFT_X, ABS_X);
AXIS_MAP_ABS(JoyAxis::LEFT_Y, ABS_Y);

AXIS_MAP_ABS(JoyAxis::RIGHT_X, ABS_RX);
AXIS_MAP_ABS(JoyAxis::RIGHT_Y, ABS_RY);

if (AXIS_MAPPED(JoyAxis::RIGHT_X)) {
AXIS_MAP_ABS(JoyAxis::TRIGGER_LEFT, ABS_BRAKE);
AXIS_MAP_ABS(JoyAxis::TRIGGER_RIGHT, ABS_GAS);

if (!AXIS_MAPPED(JoyAxis::TRIGGER_LEFT)) {
AXIS_MAP_ABS(JoyAxis::TRIGGER_LEFT, ABS_Z);
AXIS_MAP_ABS(JoyAxis::TRIGGER_RIGHT, ABS_RZ);
}
} else {
AXIS_MAP_ABS(JoyAxis::RIGHT_X, ABS_Z);
AXIS_MAP_ABS(JoyAxis::RIGHT_Y, ABS_RZ);

AXIS_MAP_ABS(JoyAxis::TRIGGER_LEFT, ABS_BRAKE);
AXIS_MAP_ABS(JoyAxis::TRIGGER_RIGHT, ABS_GAS);
}

bool trigger_is_key = false;
if (!AXIS_MAPPED(JoyAxis::TRIGGER_LEFT) && !AXIS_MAPPED(JoyAxis::TRIGGER_RIGHT)) {
trigger_is_key = true;
AXIS_MAP_KEY(JoyAxis::TRIGGER_LEFT, BTN_TL2);
AXIS_MAP_KEY(JoyAxis::TRIGGER_RIGHT, BTN_TR2);
} else {
UNUSED_KEY_HIDE(BTN_TL2);
UNUSED_KEY_HIDE(BTN_TR2);
}

input->unknown_gamepad_auto_map(p_guid, p_name, joy_button_mappings, joy_axis_mappings, trigger_is_key);
}

void JoypadLinux::open_joypad(const char *p_path) {
int joy_num = input->get_unused_joy_id();
int fd = open(p_path, O_RDWR | O_NONBLOCK);
Expand Down Expand Up @@ -419,6 +525,12 @@ void JoypadLinux::open_joypad(const char *p_path) {
}
}

if (input->is_unknown_gamepad_auto_mapped() && !input->has_mapping(uid)) {
bool hat0x_exist = test_bit(ABS_HAT0X, absbit);
bool hat0y_exist = test_bit(ABS_HAT0Y, absbit);
_auto_remap(joypad, uid, name, hat0x_exist, hat0y_exist);
}

input->joy_connection_changed(joy_num, true, name, uid, joypad_info);
} else {
String uidname = uid;
Expand All @@ -427,6 +539,13 @@ void JoypadLinux::open_joypad(const char *p_path) {
uidname = uidname + _hex_str(name[i]);
}
uidname += "00";

if (input->is_unknown_gamepad_auto_mapped() && !input->has_mapping(uid)) {
bool hat0x_exist = test_bit(ABS_HAT0X, absbit);
bool hat0y_exist = test_bit(ABS_HAT0Y, absbit);
_auto_remap(joypad, uid, name, hat0x_exist, hat0y_exist);
}

input->joy_connection_changed(joy_num, true, name, uidname);
}
}
Expand Down Expand Up @@ -536,7 +655,9 @@ void JoypadLinux::process_joypads() {

switch (joypad_event.type) {
case EV_KEY:
input->joy_button(i, (JoyButton)joypad.key_map[joypad_event.code], joypad_event.value);
if (joypad.key_map[joypad_event.code] >= 0) {
input->joy_button(i, (JoyButton)joypad.key_map[joypad_event.code], joypad_event.value);
}
break;

case EV_ABS:
Expand Down
2 changes: 2 additions & 0 deletions platform/linuxbsd/joypad_linux.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class JoypadLinux {
static void monitor_joypads_thread_func(void *p_user);
void monitor_joypads_thread_run();

void _auto_remap(Joypad &p_joypad, const StringName &p_guid, const String &p_name, bool p_hat0x_exist, bool p_hat0y_exist);

void open_joypad(const char *p_path);
void setup_joypad_properties(Joypad &p_joypad);

Expand Down

0 comments on commit 0aaeed5

Please sign in to comment.