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 Dec 15, 2024
1 parent f89706e commit 2e76ce5
Show file tree
Hide file tree
Showing 7 changed files with 205 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 @@ -1074,6 +1074,54 @@ bool Input::is_emulating_touch_from_mouse() const {
return emulate_touch_from_mouse;
}

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);
}

// Calling this whenever the game window is focused helps unsticking the "touch mouse"
// if the OS or its abstraction class hasn't properly reported that touch pointers raised
void Input::ensure_touch_mouse_raised() {
Expand Down Expand Up @@ -1735,11 +1783,20 @@ void Input::set_fallback_mapping(const String &p_guid) {
}
}

bool Input::is_mapping_known(const StringName &p_guid) {
for (const JoyDeviceMapping &map : map_db) {
if (map.uid == p_guid) {
return true;
}
}
return false;
}

//platforms that use the remapping system can override and call to these ones
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
5 changes: 5 additions & 0 deletions core/input/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,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 @@ -349,6 +350,9 @@ class Input : public Object {

void set_emulate_touch_from_mouse(bool p_emulate);
bool is_emulating_touch_from_mouse() const;
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);
void ensure_touch_mouse_raised();

void set_emulate_mouse_from_touch(bool p_emulate);
Expand All @@ -369,6 +373,7 @@ class Input : public Object {

int get_unused_joy_id();

bool is_mapping_known(const StringName &p_guid);
bool is_joy_known(int p_device);
String get_joy_guid(int p_device) const;
bool should_ignore_device(int p_vendor_id, int p_product_id) 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] A device that is mapped automatically ends with [code]"[auto]"[/code] in its name (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] This method returns [code]false[/code] even if the specified device is mapped automatically (see [member ProjectSettings.input_devices/gamepad/unknown_gamepad_auto_mapped]).
</description>
</method>
<method name="is_key_label_pressed" qualifiers="const">
Expand Down
4 changes: 4 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,10 @@
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 mapped automatically, according to keycode semantics and convention. Gamepads mapped automatically will have [code]"[auto]"[/code] appended to the end of their name.
[b]Note:[/b] This setting is only effective 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 @@ -3226,6 +3226,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
132 changes: 131 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,117 @@ 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 KEY_EXIST(keycode) (p_joypad.key_map[keycode] != -1)
#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);

if (KEY_EXIST(KEY_BACK)) {
BUTTON_MAP_KEY(JoyButton::BACK, KEY_BACK);
} else {
BUTTON_MAP_KEY(JoyButton::BACK, BTN_SELECT);
}

if (KEY_EXIST(KEY_HOMEPAGE)) {
BUTTON_MAP_KEY(JoyButton::GUIDE, KEY_HOMEPAGE);
} else {
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);
}

if (KEY_EXIST(KEY_RECORD)) {
BUTTON_MAP_KEY(JoyButton::MISC1, KEY_RECORD);
} else {
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 ABS_EXIST(abscode) (p_joypad.abs_map[abscode] != -1)

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

bool trigger_is_key = true;

if (ABS_EXIST(ABS_RX)) {
AXIS_MAP_ABS(JoyAxis::RIGHT_X, ABS_RX);
AXIS_MAP_ABS(JoyAxis::RIGHT_Y, ABS_RY);

if (ABS_EXIST(ABS_BRAKE)) {
AXIS_MAP_ABS(JoyAxis::TRIGGER_LEFT, ABS_BRAKE);
AXIS_MAP_ABS(JoyAxis::TRIGGER_RIGHT, ABS_GAS);
trigger_is_key = false;
} else if (ABS_EXIST(ABS_Z)) {
AXIS_MAP_ABS(JoyAxis::TRIGGER_LEFT, ABS_Z);
AXIS_MAP_ABS(JoyAxis::TRIGGER_RIGHT, ABS_RZ);
trigger_is_key = false;
}
} else { // ABS_RX does not exist. Try another solution.
AXIS_MAP_ABS(JoyAxis::RIGHT_X, ABS_Z);
AXIS_MAP_ABS(JoyAxis::RIGHT_Y, ABS_RZ);

if (ABS_EXIST(ABS_BRAKE)) {
AXIS_MAP_ABS(JoyAxis::TRIGGER_LEFT, ABS_BRAKE);
AXIS_MAP_ABS(JoyAxis::TRIGGER_RIGHT, ABS_GAS);
trigger_is_key = false;
}
}

if (trigger_is_key) {
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 +534,12 @@ void JoypadLinux::open_joypad(const char *p_path) {
}
}

if (input->is_unknown_gamepad_auto_mapped() && !input->is_mapping_known(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 +548,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->is_mapping_known(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 +664,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 2e76ce5

Please sign in to comment.