diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 29da659b7..7859af4e2 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -75,6 +75,17 @@ if(SDL2_FOUND) endif() endif() +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + find_package(LIBEVDEV REQUIRED) + target_compile_definitions(frontend-common PUBLIC "-DWITH_EVDEV=1") + target_include_directories(frontend-common PRIVATE ${LIBEVDEV_INCLUDE_DIRS}) + target_link_libraries(frontend-common PRIVATE ${LIBEVDEV_LIBRARIES}) + target_sources(frontend-common PRIVATE + evdev_controller_interface.cpp + evdev_controller_interface.h + ) +endif() + if(ENABLE_DISCORD_PRESENCE) target_compile_definitions(frontend-common PUBLIC -DWITH_DISCORD_PRESENCE=1) target_link_libraries(frontend-common PRIVATE discord-rpc) diff --git a/src/frontend-common/controller_interface.cpp b/src/frontend-common/controller_interface.cpp index 50c9e1119..12cb8636c 100644 --- a/src/frontend-common/controller_interface.cpp +++ b/src/frontend-common/controller_interface.cpp @@ -95,6 +95,9 @@ static constexpr std::array(ControllerInterface::B // Deliberately not translated as it's not exposed to users. "Android", #endif +#ifdef WITH_EVDEV + TRANSLATABLE("ControllerInterface", "Evdev"), +#endif }}; std::optional ControllerInterface::ParseBackendName(const char* name) @@ -132,6 +135,9 @@ ControllerInterface::Backend ControllerInterface::GetDefaultBackend() #include "dinput_controller_interface.h" #include "xinput_controller_interface.h" #endif +#ifdef WITH_EVDEV +#include "evdev_controller_interface.h" +#endif std::unique_ptr ControllerInterface::Create(Backend type) { @@ -145,6 +151,10 @@ std::unique_ptr ControllerInterface::Create(Backend type) if (type == Backend::DInput) return std::make_unique(); #endif +#ifdef WITH_EVDEV + if (type == Backend::Evdev) + return std::make_unique(); +#endif return {}; } diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h index d3091fb16..4cfd32ab7 100644 --- a/src/frontend-common/controller_interface.h +++ b/src/frontend-common/controller_interface.h @@ -26,6 +26,9 @@ public: #endif #ifdef ANDROID Android, +#endif +#ifdef WITH_EVDEV + Evdev, #endif Count }; diff --git a/src/frontend-common/evdev_controller_interface.cpp b/src/frontend-common/evdev_controller_interface.cpp new file mode 100644 index 000000000..5377d6fb9 --- /dev/null +++ b/src/frontend-common/evdev_controller_interface.cpp @@ -0,0 +1,382 @@ +#include "evdev_controller_interface.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/log.h" +#include "core/controller.h" +#include "core/host_interface.h" +#include "core/system.h" +#include +#include +#include +#include +#include +Log_SetChannel(EvdevControllerInterface); + +EvdevControllerInterface::EvdevControllerInterface() = default; + +EvdevControllerInterface::~EvdevControllerInterface() = default; + +ControllerInterface::Backend EvdevControllerInterface::GetBackend() const +{ + return ControllerInterface::Backend::Evdev; +} + +bool EvdevControllerInterface::Initialize(CommonHostInterface* host_interface) +{ + for (int index = 0; index < 1000; index++) + { + TinyString path; + path.Format("/dev/input/event%d", index); + + int fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd < 0) + break; + + struct libevdev* obj; + if (libevdev_new_from_fd(fd, &obj) != 0) + { + Log_ErrorPrintf("libevdev_new_from_fd(%s) failed", path.GetCharArray()); + close(fd); + continue; + } + + ControllerData data(fd, obj); + if (InitializeController(index, &data)) + m_controllers.push_back(std::move(data)); + } + + if (!ControllerInterface::Initialize(host_interface)) + return false; + + return true; +} + +void EvdevControllerInterface::Shutdown() +{ + ControllerInterface::Shutdown(); +} + +EvdevControllerInterface::ControllerData::ControllerData(int fd_, struct libevdev* obj_) : obj(obj_), fd(fd_) {} + +EvdevControllerInterface::ControllerData::ControllerData(ControllerData&& move) + : obj(move.obj), fd(move.fd), controller_id(move.controller_id), num_motors(move.num_motors), deadzone(move.deadzone), + axises(std::move(move.axises)), buttons(std::move(move.buttons)) +{ + move.obj = nullptr; + move.fd = -1; +} + +EvdevControllerInterface::ControllerData::~ControllerData() +{ + if (obj) + libevdev_free(obj); + if (fd >= 0) + close(fd); +} + +EvdevControllerInterface::ControllerData& +EvdevControllerInterface::ControllerData::operator=(EvdevControllerInterface::ControllerData&& move) +{ + if (obj) + libevdev_free(obj); + obj = move.obj; + move.obj = nullptr; + if (fd >= 0) + close(fd); + fd = move.fd; + move.fd = -1; + controller_id = move.controller_id; + num_motors = move.num_motors; + deadzone = move.deadzone; + axises = std::move(move.axises); + buttons = std::move(move.buttons); + return *this; +} + +EvdevControllerInterface::ControllerData* EvdevControllerInterface::GetControllerById(int id) +{ + for (ControllerData& cd : m_controllers) + { + if (cd.controller_id == id) + return &cd; + } + + return nullptr; +} + +bool EvdevControllerInterface::InitializeController(int index, ControllerData* cd) +{ + const char* name = libevdev_get_name(cd->obj); + Log_DevPrintf("Input %d device name: \"%s\"", index, name); + Log_DevPrintf("Input %d device ID: bus %#x vendor %#x product %#x", index, libevdev_get_id_bustype(cd->obj), + libevdev_get_id_vendor(cd->obj), libevdev_get_id_product(cd->obj)); + + for (u32 key = 0; key < KEY_CNT; key++) + { + if (!libevdev_has_event_code(cd->obj, EV_KEY, key)) + continue; + + const char* button_name = libevdev_event_code_get_name(EV_KEY, key); + Log_DevPrintf("Key %d: %s -> Button %zu", key, button_name ? button_name : "null", cd->buttons.size()); + + ControllerData::Button button; + button.id = key; + cd->buttons.push_back(std::move(button)); + } + + // Heuristic borrowed from Dolphin's evdev controller interface - ignore bogus devices + // which do have less than 2 axises and less than 8 buttons. + if (cd->axises.size() < 2 && cd->buttons.size() < 8) + { + Log_InfoPrintf("Ignoring device %s due to heuristic", name); + return false; + } + + return true; +} + +void EvdevControllerInterface::HandleControllerEvents(ControllerData* cd) +{ + struct input_event ev; + while (libevdev_next_event(cd->obj, LIBEVDEV_READ_FLAG_NORMAL, &ev) == 0) + { + switch (ev.type) + { + case EV_KEY: + { + // auto-repeat + if (ev.value == 2) + continue; + + const bool pressed = (ev.value == 1); + Log_DevPrintf("Key %d %s", ev.code, pressed ? "pressed" : "unpressed"); + + for (u32 i = 0; i < static_cast(cd->buttons.size()); i++) + { + if (cd->buttons[i].id == ev.code) + { + HandleButtonEvent(cd, i, ev.code, pressed); + break; + } + } + } + break; + + default: + break; + } + } +} + +void EvdevControllerInterface::PollEvents() +{ + if (m_controllers.empty()) + return; + + struct pollfd* fds = static_cast(alloca(sizeof(struct pollfd) * m_controllers.size())); + for (size_t i = 0; i < m_controllers.size(); i++) + { + fds[i].events = POLLIN; + fds[i].fd = m_controllers[i].fd; + fds[i].revents = 0; + } + + if (poll(fds, static_cast(m_controllers.size()), 0) <= 0) + return; + + for (size_t i = 0; i < m_controllers.size(); i++) + { + if (fds[i].revents & POLLIN) + HandleControllerEvents(&m_controllers[i]); + } +} + +void EvdevControllerInterface::ClearBindings() +{ + for (ControllerData& cd : m_controllers) + { + for (ControllerData::Button& btn : cd.buttons) + { + btn.callback = {}; + btn.axis_callback = {}; + } + for (ControllerData::Axis& axis : cd.axises) + { + axis.callback = {}; + axis.button_callback = {}; + } + } +} + +bool EvdevControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, + AxisCallback callback) +{ + ControllerData* cd = GetControllerById(controller_index); + if (!cd || static_cast(axis_number) >= cd->axises.size()) + return false; + + cd->axises[axis_number].callback[axis_side] = std::move(callback); + return true; +} + +bool EvdevControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback) +{ + ControllerData* cd = GetControllerById(controller_index); + if (!cd || static_cast(button_number) >= cd->buttons.size()) + return false; + + cd->buttons[button_number].callback = std::move(callback); + return true; +} + +bool EvdevControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) +{ + ControllerData* cd = GetControllerById(controller_index); + if (!cd || static_cast(axis_number) >= cd->axises.size()) + return false; + + cd->axises[axis_number].button_callback[BoolToUInt8(direction)] = std::move(callback); + return true; +} + +bool EvdevControllerInterface::BindControllerHatToButton(int controller_index, int hat_number, + std::string_view hat_position, ButtonCallback callback) +{ + // Hats don't exist in XInput + return false; +} + +bool EvdevControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, + AxisCallback callback) +{ + ControllerData* cd = GetControllerById(controller_index); + if (!cd || static_cast(button_number) >= cd->buttons.size()) + return false; + + cd->buttons[button_number].axis_callback = std::move(callback); + return true; +} + +bool EvdevControllerInterface::HandleAxisEvent(ControllerData* cd, u32 axis, s32 value) +{ + const float f_value = static_cast(value) / (value < 0 ? 32768.0f : 32767.0f); + Log_DevPrintf("controller %u axis %u %d %f", cd->controller_id, axis, value, f_value); + + if (DoEventHook(Hook::Type::Axis, cd->controller_id, axis, f_value)) + return true; + + const AxisCallback& cb = cd->axises[axis].callback[AxisSide::Full]; + if (cb) + { + cb(f_value); + return true; + } + + // set the other direction to false so large movements don't leave the opposite on + const bool outside_deadzone = (std::abs(f_value) >= cd->deadzone); + const bool positive = (f_value >= 0.0f); + const ButtonCallback& other_button_cb = cd->axises[axis].button_callback[BoolToUInt8(!positive)]; + const ButtonCallback& button_cb = cd->axises[axis].button_callback[BoolToUInt8(positive)]; + if (button_cb) + { + button_cb(outside_deadzone); + if (other_button_cb) + other_button_cb(false); + return true; + } + else if (other_button_cb) + { + other_button_cb(false); + return true; + } + else + { + return false; + } +} + +static FrontendCommon::ControllerNavigationButton MapEventButtonToNavigationButton(int button_id) +{ + switch (button_id) + { + case BTN_A: + return FrontendCommon::ControllerNavigationButton::Activate; + case BTN_B: + return FrontendCommon::ControllerNavigationButton::Cancel; + case BTN_TL: + return FrontendCommon::ControllerNavigationButton::LeftShoulder; + case BTN_TR: + return FrontendCommon::ControllerNavigationButton::RightShoulder; + case BTN_DPAD_LEFT: + return FrontendCommon::ControllerNavigationButton::DPadLeft; + case BTN_DPAD_RIGHT: + return FrontendCommon::ControllerNavigationButton::DPadRight; + case BTN_DPAD_UP: + return FrontendCommon::ControllerNavigationButton::DPadUp; + case BTN_DPAD_DOWN: + return FrontendCommon::ControllerNavigationButton::DPadDown; + default: + return FrontendCommon::ControllerNavigationButton::Count; + } +} + +bool EvdevControllerInterface::HandleButtonEvent(ControllerData* cd, u32 button, int button_id, bool pressed) +{ + Log_DevPrintf("controller %d button %u %s", cd->controller_id, button, pressed ? "pressed" : "released"); + + if (DoEventHook(Hook::Type::Button, cd->controller_id, button, pressed ? 1.0f : 0.0f)) + return true; + + const FrontendCommon::ControllerNavigationButton nav_button = MapEventButtonToNavigationButton(button_id); + if (nav_button < FrontendCommon::ControllerNavigationButton::Count && + m_host_interface->SetControllerNavigationButtonState(nav_button, pressed)) + { + // UI consumed the event + return true; + } + + const ButtonCallback& cb = cd->buttons[button].callback; + if (cb) + { + cb(pressed); + return true; + } + + // Assume a half-axis, i.e. in 0..1 range + const AxisCallback& axis_cb = cd->buttons[button].axis_callback; + if (axis_cb) + { + axis_cb(pressed ? 1.0f : 0.0f); + } + return true; +} + +u32 EvdevControllerInterface::GetControllerRumbleMotorCount(int controller_index) +{ + ControllerData* cd = GetControllerById(controller_index); + return cd ? cd->num_motors : 0; +} + +void EvdevControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) +{ + ControllerData* cd = GetControllerById(controller_index); + if (!cd) + return; + + /* XINPUT_VIBRATION vib; + vib.wLeftMotorSpeed = static_cast(strengths[0] * 65535.0f); + vib.wRightMotorSpeed = static_cast(strengths[1] * 65535.0f); + m_xinput_set_state(static_cast(controller_index), &vib);*/ +} + +bool EvdevControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */) +{ + ControllerData* cd = GetControllerById(controller_index); + if (!cd) + return false; + + cd->deadzone = std::clamp(std::abs(size), 0.01f, 0.99f); + Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, cd->deadzone); + return true; +} diff --git a/src/frontend-common/evdev_controller_interface.h b/src/frontend-common/evdev_controller_interface.h new file mode 100644 index 000000000..4e90dae03 --- /dev/null +++ b/src/frontend-common/evdev_controller_interface.h @@ -0,0 +1,97 @@ +#pragma once +#include "controller_interface.h" +#include "core/types.h" +#include +#include +#include +#include +#include + +class EvdevControllerInterface final : public ControllerInterface +{ +public: + EvdevControllerInterface(); + ~EvdevControllerInterface() override; + + Backend GetBackend() const override; + bool Initialize(CommonHostInterface* host_interface) override; + void Shutdown() override; + + // Removes all bindings. Call before setting new bindings. + void ClearBindings() override; + + // Binding to events. If a binding for this axis/button already exists, returns false. + bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override; + bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; + bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) override; + bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position, + ButtonCallback callback) override; + bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; + + // Changing rumble strength. + u32 GetControllerRumbleMotorCount(int controller_index) override; + void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override; + + // Set deadzone that will be applied on axis-to-button mappings + bool SetControllerDeadzone(int controller_index, float size = 0.25f) override; + + void PollEvents() override; + +private: + enum class Axis : u32 + { + LeftX, + LeftY, + RightX, + RightY, + LeftTrigger, + RightTrigger + }; + + struct ControllerData + { + ControllerData(int fd_, struct libevdev* obj_); + ControllerData(const ControllerData&) = delete; + ControllerData(ControllerData&& move); + ~ControllerData(); + + ControllerData& operator=(const ControllerData&) = delete; + ControllerData& operator=(ControllerData&& move); + + struct libevdev* obj = nullptr; + int fd = -1; + int controller_id = 0; + u32 num_motors = 0; + + float deadzone = 0.25f; + + struct Axis + { + u32 id; + std::array callback; + std::array button_callback; + }; + + struct Button + { + u32 id; + ButtonCallback callback; + AxisCallback axis_callback; + }; + + std::vector axises; + std::vector