From dbd69536ed29bc69440982bdf0be7928e92c7851 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Wed, 26 Jul 2017 13:03:06 -0500 Subject: [PATCH] Linux evdev joystick support (#2885) * Linux evdev joystick support (#2678) * Cleanup libevdev configure code * evdev fixes * Evdev joystick additions/fixes * Error message tweak * Fix evdev multiple joysticks (thanks @hcorion!) * Change by-id to by-path in evdev --- rpcs3/CMakeLists.txt | 11 + rpcs3/Emu/System.cpp | 3 + rpcs3/Emu/System.h | 3 + rpcs3/evdev_joystick_handler.cpp | 403 +++++++++++++++++++++++++++++++ rpcs3/evdev_joystick_handler.h | 90 +++++++ rpcs3/rpcs3_app.cpp | 6 + 6 files changed, 516 insertions(+) create mode 100644 rpcs3/evdev_joystick_handler.cpp create mode 100644 rpcs3/evdev_joystick_handler.h diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index 4ea1bf8945..1ce045121b 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -158,6 +158,17 @@ elseif(WIN32) set(PLATFORM_ARCH "Windows/x86_64") else() set(PLATFORM_ARCH "linux/x86_64") + option(USE_LIBEVDEV "libevdev-based joystick support" ON) +endif() + +if(USE_LIBEVDEV) + find_package(PkgConfig) + pkg_check_modules(LIBEVDEV libevdev) + if(LIBEVDEV_FOUND) + add_definitions(-DHAVE_LIBEVDEV) + include_directories(SYSTEM ${LIBEVDEV_INCLUDE_DIRS}) + list(APPEND ADDITIONAL_LIBS ${LIBEVDEV_LDFLAGS}) + endif() endif() # Select the version of libpng to use, default is builtin diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 2c43b673b2..c96145f9d3 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -74,6 +74,9 @@ void fmt_class_string::format(std::string& out, u64 arg) #endif #ifdef _WIN32 case pad_handler::mm: return "MMJoystick"; +#endif +#ifdef HAVE_LIBEVDEV + case pad_handler::evdev: return "Evdev"; #endif } diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 28b88c7117..c163aad0d6 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -68,6 +68,9 @@ enum class pad_handler #ifdef _WIN32 mm, #endif +#ifdef HAVE_LIBEVDEV + evdev, +#endif }; enum class video_renderer diff --git a/rpcs3/evdev_joystick_handler.cpp b/rpcs3/evdev_joystick_handler.cpp new file mode 100644 index 0000000000..91e450da96 --- /dev/null +++ b/rpcs3/evdev_joystick_handler.cpp @@ -0,0 +1,403 @@ +#ifdef HAVE_LIBEVDEV + +#include "evdev_joystick_handler.h" +#include "Utilities/Thread.h" +#include "Utilities/Log.h" + +#include +#include +#include +#include +#include +#include +#include + +evdev_joystick_config g_evdev_joystick_config; + +namespace +{ + const u32 THREAD_SLEEP_USEC = 100; + const u32 THREAD_SLEEP_INACTIVE_USEC = 1000000; + const u32 READ_TIMEOUT = 10; + const u32 THREAD_TIMEOUT_USEC = 1000000; + + const std::string EVENT_JOYSTICK = "event-joystick"; +} + +evdev_joystick_handler::evdev_joystick_handler() {} + +evdev_joystick_handler::~evdev_joystick_handler() { Close(); } + +void evdev_joystick_handler::Init(const u32 max_connect) +{ + std::memset(&m_info, 0, sizeof m_info); + + g_evdev_joystick_config.load(); + + needscale = static_cast(g_evdev_joystick_config.needscale); + axistrigger = static_cast(g_evdev_joystick_config.axistrigger); + + revaxis.emplace_back(g_evdev_joystick_config.lxreverse); + revaxis.emplace_back(g_evdev_joystick_config.lyreverse); + revaxis.emplace_back(g_evdev_joystick_config.rxreverse); + revaxis.emplace_back(g_evdev_joystick_config.ryreverse); + + fs::dir devdir{"/dev/input/by-path"}; + fs::dir_entry et; + + while (devdir.read(et)) + { + // Does the entry name end with event-joystick? + if (et.name.size() > EVENT_JOYSTICK.size() && + et.name.compare(et.name.size() - EVENT_JOYSTICK.size(), + EVENT_JOYSTICK.size(), EVENT_JOYSTICK) == 0) + { + joy_paths.emplace_back(fmt::format("/dev/input/by-path/%s", et.name)); + } + } + + m_info.max_connect = std::min(max_connect, static_cast(joy_paths.size())); + + for (u32 i = 0; i < m_info.max_connect; ++i) + { + joy_devs.push_back(nullptr); + joy_axis_maps.emplace_back(ABS_RZ - ABS_X, -1); + joy_button_maps.emplace_back(KEY_MAX - BTN_JOYSTICK, -1); + joy_hat_ids.emplace_back(-1); + m_pads.emplace_back( + CELL_PAD_STATUS_DISCONNECTED, + CELL_PAD_SETTING_PRESS_OFF | CELL_PAD_SETTING_SENSOR_OFF, + CELL_PAD_CAPABILITY_PS3_CONFORMITY | CELL_PAD_CAPABILITY_PRESS_MODE, + CELL_PAD_DEV_TYPE_STANDARD + ); + auto& pad = m_pads.back(); + + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.triangle, CELL_PAD_CTRL_TRIANGLE); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.circle, CELL_PAD_CTRL_CIRCLE); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.cross, CELL_PAD_CTRL_CROSS); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.square, CELL_PAD_CTRL_SQUARE); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.l2, CELL_PAD_CTRL_L2); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.r2, CELL_PAD_CTRL_R2); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.l1, CELL_PAD_CTRL_L1); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, g_evdev_joystick_config.r1, CELL_PAD_CTRL_R1); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.start, CELL_PAD_CTRL_START); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.select, CELL_PAD_CTRL_SELECT); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.l3, CELL_PAD_CTRL_L3); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.r3, CELL_PAD_CTRL_R3); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, 0, 0x100/*CELL_PAD_CTRL_PS*/);// TODO: PS button support + + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.up, CELL_PAD_CTRL_UP); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.down, CELL_PAD_CTRL_DOWN); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.left, CELL_PAD_CTRL_LEFT); + pad.m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, g_evdev_joystick_config.right, CELL_PAD_CTRL_RIGHT); + + pad.m_sticks.emplace_back(CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_X+g_evdev_joystick_config.lxstick, 0, 0); + pad.m_sticks.emplace_back(CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_X+g_evdev_joystick_config.lystick, 0, 0); + pad.m_sticks.emplace_back(CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_X+g_evdev_joystick_config.rxstick, 0, 0); + pad.m_sticks.emplace_back(CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_X+g_evdev_joystick_config.rystick, 0, 0); + } + + update_devs(); + active.store(true); + joy_thread.reset(new std::thread(std::bind(&evdev_joystick_handler::thread_func, this))); +} + +void evdev_joystick_handler::update_devs() +{ + int connected=0; + + for (u32 i = 0; i < m_info.max_connect; ++i) + if (try_open_dev(i)) ++connected; + + m_info.now_connect = connected; +} + +bool evdev_joystick_handler::try_open_dev(u32 index) +{ + libevdev*& dev = joy_devs[index]; + bool was_connected = dev != nullptr; + + if (index >= joy_paths.size()) return false; + const auto& path = joy_paths[index]; + + if (access(path.c_str(), R_OK) == -1) + { + if (was_connected) + { + // It was disconnected. + m_pads[index].m_port_status |= CELL_PAD_STATUS_ASSIGN_CHANGES; + + int fd = libevdev_get_fd(dev); + libevdev_free(dev); + close(fd); + dev = nullptr; + } + m_pads[index].m_port_status &= ~CELL_PAD_STATUS_CONNECTED; + LOG_ERROR(GENERAL, "Joystick %s is not present or accessible [previous status: %d]", path.c_str(), + was_connected ? 1 : 0); + return false; + } + + if (was_connected) return true; // It's already been connected, and the js is still present. + int fd = open(path.c_str(), O_RDONLY | O_NONBLOCK); + + if (fd == -1) + { + int err = errno; + LOG_ERROR(GENERAL, "Failed to open joystick #%d: %s [errno %d]", index, strerror(err), err); + return false; + } + + int ret = libevdev_new_from_fd(fd, &dev); + if (ret < 0) + { + LOG_ERROR(GENERAL, "Failed to initialize libevdev for joystick #%d: %s [errno %d]", index, strerror(-ret), -ret); + return false; + } + + LOG_NOTICE(GENERAL, "Opened joystick #%d '%s' at %s (fd %d)", index, libevdev_get_name(dev), path, fd); + + if (!was_connected) + // Connection status changed from disconnected to connected. + m_pads[index].m_port_status |= CELL_PAD_STATUS_ASSIGN_CHANGES; + m_pads[index].m_port_status |= CELL_PAD_STATUS_CONNECTED; + + int buttons=0; + for (int i=BTN_JOYSTICK; idetach(); + + for (auto& dev : joy_devs) + { + if (dev != nullptr) + { + int fd = libevdev_get_fd(dev); + libevdev_free(dev); + close(fd); + } + } +} + +void evdev_joystick_handler::thread_func() +{ + while (active) + { + update_devs(); + + for (int i=0; im_pressed = evt.value; + which_button->m_value = evt.value ? 255 : 0; + break; + } + case EV_ABS: + LOG_NOTICE(GENERAL, "Joystick #%d EV_ABS: %d %d", i, evt.code, evt.value); + + if (evt.code >= ABS_HAT0X && evt.code <= ABS_HAT3Y) { + int hat = evt.code - ABS_HAT0X; + if (hat != joy_hat_ids[i] && hat-1 != joy_hat_ids[i]) + { + LOG_ERROR(GENERAL, "Joystick #%d sent HAT event for invalid hat %d (expected %d)", i, hat, joy_hat_ids[i]); + break; + } + + int source_axis = hat == joy_hat_ids[i] ? EVDEV_DPAD_HAT_AXIS_X : EVDEV_DPAD_HAT_AXIS_Y; + + for (Button& bt : pad.m_buttons) + { + if (bt.m_keyCode != source_axis) continue; + + if (evt.value == 0) + { + bt.m_pressed = false; + bt.m_value = 0; + } + else + { + int code = -1; + if (source_axis == EVDEV_DPAD_HAT_AXIS_X) + { + code = evt.value > 0 ? CELL_PAD_CTRL_RIGHT : CELL_PAD_CTRL_LEFT; + } + else + { + code = evt.value > 0 ? CELL_PAD_CTRL_DOWN : CELL_PAD_CTRL_UP; + } + + if (bt.m_outKeyCode == code) + { + bt.m_pressed = true; + bt.m_value = 255; + } + } + } + } + else if (axistrigger && (evt.code == ABS_Z || evt.code == ABS_RZ)) + { + // For Xbox 360 controllers, a third axis represent the left/right triggers. + int which_trigger=0; + + if (evt.code == ABS_Z) + { + which_trigger = CELL_PAD_CTRL_L2; + } + else if (evt.code == ABS_RZ) + { + which_trigger = CELL_PAD_CTRL_R2; + } + else + { + LOG_ERROR(GENERAL, "Joystick #%d sent invalid event code %d for 3rd axis", i, evt.code); + break; + } + + auto which_button = std::find_if( + pad.m_buttons.begin(), pad.m_buttons.end(), + [&](const Button& bt) { return bt.m_outKeyCode == which_trigger; }); + if (which_button == pad.m_buttons.end()) + { + LOG_ERROR(GENERAL, "Joystick #%d's pad has no trigger %d", i, which_trigger); + break; + } + which_button->m_pressed = evt.value == 255; + which_button->m_value = evt.value; + } + else if (evt.code >= ABS_X && evt.code <= ABS_RZ) + { + int axis = joy_axis_maps[i][evt.code - ABS_X]; + + if (axis > pad.m_sticks.size()) + { + LOG_ERROR(GENERAL, "Joystick #%d sent axis event for invalid axis %d", i, axis); + break; + } + + auto& stick = pad.m_sticks[axis]; + + int value = evt.value; + + if (needscale) + { + // Scale from the -32768...32768 range to the 0...256 range. + value = (value / 256) + 128; + } + + if (revaxis[axis]) + { + value = 256 - value; + } + + stick.m_value = value; + } + break; + default: + LOG_ERROR(GENERAL, "Unknown joystick #%d event %d", i, evt.type); + break; + } + } + + int to_sleep = m_info.now_connect > 0 ? THREAD_SLEEP_USEC : THREAD_SLEEP_INACTIVE_USEC; + usleep(to_sleep); + } + + dead = true; +} + +#endif diff --git a/rpcs3/evdev_joystick_handler.h b/rpcs3/evdev_joystick_handler.h new file mode 100644 index 0000000000..66285687fe --- /dev/null +++ b/rpcs3/evdev_joystick_handler.h @@ -0,0 +1,90 @@ +#pragma once + +#include "Utilities/types.h" +#include "Utilities/Config.h" +#include "Utilities/File.h" +#include "Emu/Io/PadHandler.h" +#include +#include +#include + + +enum { EVDEV_DPAD_HAT_AXIS_X = -1, EVDEV_DPAD_HAT_AXIS_Y = -2 }; + +struct evdev_joystick_config final : cfg::node +{ + const std::string cfg_name = fs::get_config_dir() + "/config_linuxjoystick.yml"; + + cfg::int32 select{this, "Select", 6}; + cfg::int32 start{this, "Start", 7}; + cfg::int32 cross{this, "Cross", 0}; + cfg::int32 circle{this, "Circle", 1}; + cfg::int32 square{this, "Square", 2}; + cfg::int32 triangle{this, "Triangle", 3}; + + cfg::int32 r1{this, "R1", 5}; + cfg::int32 r2{this, "R2", 11}; + cfg::int32 r3{this, "R3", 10}; + cfg::int32 l1{this, "L1", 4}; + cfg::int32 l2{this, "L2", 12}; + cfg::int32 l3{this, "L3", 9}; + + cfg::int32 up{this, "Up", EVDEV_DPAD_HAT_AXIS_Y}; + cfg::int32 down{this, "Down", EVDEV_DPAD_HAT_AXIS_Y}; + cfg::int32 left{this, "Left", EVDEV_DPAD_HAT_AXIS_X}; + cfg::int32 right{this, "Right", EVDEV_DPAD_HAT_AXIS_X}; + + cfg::int32 rxstick{this, "Right stick X axis", 0}; + cfg::int32 rystick{this, "Right stick Y axis", 1}; + cfg::int32 lxstick{this, "Left stick X axis", 2}; + cfg::int32 lystick{this, "Left stick Y axis", 3}; + + cfg::_bool rxreverse{this, "Reverse right stick X axis", false}; + cfg::_bool ryreverse{this, "Reverse right stick Y axis", false}; + cfg::_bool lxreverse{this, "Reverse left stick X axis", false}; + cfg::_bool lyreverse{this, "Reverse left stick Y axis", false}; + + cfg::_bool needscale{this, "Axis scaling", true}; + cfg::_bool axistrigger{this, "Z axis triggers", true}; + + bool load() + { + if (fs::file cfg_file{ cfg_name, fs::read }) + { + return from_string(cfg_file.to_string()); + } + + return false; + } + + void save() + { + fs::file(cfg_name, fs::rewrite).write(to_string()); + } +}; + + +class evdev_joystick_handler final : public PadHandlerBase +{ +public: + evdev_joystick_handler(); + ~evdev_joystick_handler(); + + void Init(const u32 max_connect) override; + void Close(); + +private: + void update_devs(); + bool try_open_dev(u32 index); + void thread_func(); + + std::unique_ptr joy_thread; + mutable atomic_t active{false}, dead{false}; + std::vector joy_paths; + std::vector joy_devs; + std::vector> joy_button_maps; + std::vector> joy_axis_maps; + std::vector joy_hat_ids; + bool needscale, axistrigger; + std::vector revaxis; +}; diff --git a/rpcs3/rpcs3_app.cpp b/rpcs3/rpcs3_app.cpp index a3c2849a5a..7defcb07e3 100644 --- a/rpcs3/rpcs3_app.cpp +++ b/rpcs3/rpcs3_app.cpp @@ -27,6 +27,9 @@ #ifdef _WIN32 #include "mm_joystick_handler.h" #endif +#ifdef HAVE_LIBEVDEV +#include "evdev_joystick_handler.h" +#endif #include "Emu/RSX/Null/NullGSRender.h" @@ -137,6 +140,9 @@ void rpcs3_app::InitializeCallbacks() #endif #ifdef _WIN32 case pad_handler::mm: return std::make_shared(); +#endif +#ifdef HAVE_LIBEVDEV + case pad_handler::evdev: return std::make_shared(); #endif default: fmt::throw_exception("Invalid pad handler: %s", type); }