From 0ac4e3cc3c33999c2b2b741b24619700f53ecbae Mon Sep 17 00:00:00 2001 From: Vithorio Polten Date: Sat, 15 Jun 2024 21:21:18 -0300 Subject: [PATCH] feat(input/linux): add support for more virtual input devices (#2606) Co-authored-by: ABeltramo Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- .gitmodules | 4 + cmake/compile_definitions/linux.cmake | 21 +- cmake/dependencies/libevdev_Sunshine.cmake | 3 + cmake/macros/common.cmake | 16 +- cmake/prep/options.cmake | 2 + docs/source/about/setup.rst | 7 +- src/config.cpp | 17 +- src/input.cpp | 42 ++- src/input.h | 3 + src/main.cpp | 5 + src/platform/common.h | 22 +- src/platform/linux/input/inputtino.cpp | 149 +++++++++ src/platform/linux/input/inputtino_common.h | 97 ++++++ .../linux/input/inputtino_gamepad.cpp | 297 ++++++++++++++++++ src/platform/linux/input/inputtino_gamepad.h | 41 +++ .../linux/input/inputtino_keyboard.cpp | 117 +++++++ src/platform/linux/input/inputtino_keyboard.h | 17 + src/platform/linux/input/inputtino_mouse.cpp | 88 ++++++ src/platform/linux/input/inputtino_mouse.h | 31 ++ src/platform/linux/input/inputtino_pen.cpp | 69 ++++ src/platform/linux/input/inputtino_pen.h | 16 + src/platform/linux/input/inputtino_touch.cpp | 51 +++ src/platform/linux/input/inputtino_touch.h | 16 + .../{input.cpp => input/legacy_input.cpp} | 23 +- src/platform/macos/input.cpp | 16 +- src/platform/windows/input.cpp | 39 ++- .../common/assets/web/configs/tabs/Inputs.vue | 19 +- .../assets/web/public/assets/locale/en.json | 3 + src_assets/linux/misc/60-sunshine.rules | 3 +- tests/unit/test_mouse.cpp | 5 +- third-party/inputtino | 1 + 31 files changed, 1163 insertions(+), 77 deletions(-) create mode 100644 src/platform/linux/input/inputtino.cpp create mode 100644 src/platform/linux/input/inputtino_common.h create mode 100644 src/platform/linux/input/inputtino_gamepad.cpp create mode 100644 src/platform/linux/input/inputtino_gamepad.h create mode 100644 src/platform/linux/input/inputtino_keyboard.cpp create mode 100644 src/platform/linux/input/inputtino_keyboard.h create mode 100644 src/platform/linux/input/inputtino_mouse.cpp create mode 100644 src/platform/linux/input/inputtino_mouse.h create mode 100644 src/platform/linux/input/inputtino_pen.cpp create mode 100644 src/platform/linux/input/inputtino_pen.h create mode 100644 src/platform/linux/input/inputtino_touch.cpp create mode 100644 src/platform/linux/input/inputtino_touch.h rename src/platform/linux/{input.cpp => input/legacy_input.cpp} (99%) create mode 160000 third-party/inputtino diff --git a/.gitmodules b/.gitmodules index 17365eb8ca6..15f8504603b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,6 +14,10 @@ path = third-party/googletest url = https://github.com/google/googletest/ branch = v1.14.x +[submodule "third-party/inputtino"] + path = third-party/inputtino + url = https://github.com/games-on-whales/inputtino.git + branch = stable [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 9fcba3b37e8..e07c2a55d8f 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -222,6 +222,26 @@ if(${SUNSHINE_ENABLE_TRAY} AND ${SUNSHINE_TRAY} EQUAL 0 AND SUNSHINE_REQUIRE_TRA message(FATAL_ERROR "Tray icon is required") endif() +if(${SUNSHINE_USE_LEGACY_INPUT}) # TODO: Remove this legacy option after the next stable release + list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/input/legacy_input.cpp") +else() + # These need to be set before adding the inputtino subdirectory in order for them to be picked up + set(LIBEVDEV_CUSTOM_INCLUDE_DIR "${EVDEV_INCLUDE_DIR}") + set(LIBEVDEV_CUSTOM_LIBRARY "${EVDEV_LIBRARY}") + + add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino") + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino) + file(GLOB_RECURSE INPUTTINO_SOURCES + ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h + ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp) + list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES}) + + # build libevdev before the libinputtino target + if(EXTERNAL_PROJECT_LIBEVDEV_USED) + add_dependencies(libinputtino libevdev) + endif() +endif() + list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h" @@ -229,7 +249,6 @@ list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp" - "${CMAKE_SOURCE_DIR}/src/platform/linux/input.cpp" "${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h" diff --git a/cmake/dependencies/libevdev_Sunshine.cmake b/cmake/dependencies/libevdev_Sunshine.cmake index 4d0af70a009..ca9e22dce35 100644 --- a/cmake/dependencies/libevdev_Sunshine.cmake +++ b/cmake/dependencies/libevdev_Sunshine.cmake @@ -37,6 +37,9 @@ else() endif() if(EVDEV_INCLUDE_DIR AND EVDEV_LIBRARY) + message(STATUS "Found libevdev library: ${EVDEV_LIBRARY}") + message(STATUS "Found libevdev include directory: ${EVDEV_INCLUDE_DIR}") + include_directories(SYSTEM ${EVDEV_INCLUDE_DIR}) list(APPEND PLATFORM_LIBRARIES ${EVDEV_LIBRARY}) else() diff --git a/cmake/macros/common.cmake b/cmake/macros/common.cmake index 215b9fd8dec..53a3c694afe 100644 --- a/cmake/macros/common.cmake +++ b/cmake/macros/common.cmake @@ -19,7 +19,7 @@ macro(find_package) # cmake-lint: disable=C0103 string(TOLOWER "${ARGV0}" ARGV0_LOWER) if( (("${ARGV0_LOWER}" STREQUAL "boost") AND DEFINED FETCH_CONTENT_BOOST_USED) OR - (("${ARGV0_LOWER}" STREQUAL "libevdev") AND DEFINED FETCH_CONTENT_LIBEVDEV_USED) + (("${ARGV0_LOWER}" STREQUAL "libevdev") AND DEFINED EXTERNAL_PROJECT_LIBEVDEV_USED) ) # Do nothing, as the package has already been fetched else() @@ -27,17 +27,3 @@ macro(find_package) # cmake-lint: disable=C0103 _find_package(${ARGV}) endif() endmacro() - -# override pkg_check_modules function -macro(pkg_check_modules) # cmake-lint: disable=C0103 - string(TOLOWER "${ARGV0}" ARGV0_LOWER) - if( - (("${ARGV0_LOWER}" STREQUAL "boost") AND DEFINED FETCH_CONTENT_BOOST_USED) OR - (("${ARGV0_LOWER}" STREQUAL "libevdev") AND DEFINED FETCH_CONTENT_LIBEVDEV_USED) - ) - # Do nothing, as the package has already been fetched - else() - # Call the original pkg_check_modules function - _pkg_check_modules(${ARGV}) - endif() -endmacro() diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index 078025e07f8..c659e2f9ca2 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -54,4 +54,6 @@ elseif(UNIX) # Linux "Enable building wayland specific code." ON) option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available." ON) + option(SUNSHINE_USE_LEGACY_INPUT # TODO: Remove this legacy option after the next stable release + "Use the legacy virtual input implementation." OFF) endif() diff --git a/docs/source/about/setup.rst b/docs/source/about/setup.rst index e2fc9b6699f..0082d7388b7 100644 --- a/docs/source/about/setup.rst +++ b/docs/source/about/setup.rst @@ -209,12 +209,13 @@ Install The `deb`, `rpm`, `zst`, `Flatpak` and `AppImage` packages should handle these steps automatically. Third party packages may not. - Sunshine needs access to `uinput` to create mouse and gamepad events. + Sunshine needs access to `uinput` to create mouse and gamepad virtual devices and (optionally) to `uhid` + in order to emulate a PS5 DualSense joypad with Gyro, Acceleration and Touchpad support. - #. Create and reload `udev` rules for uinput. + #. Create and reload `udev` rules for `uinput` and `uhid`. .. code-block:: bash - echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"' | \ + echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"\nKERNEL=="uhid", TAG+="uaccess"' | \ sudo tee /etc/udev/rules.d/60-sunshine.rules sudo udevadm control --reload-rules sudo udevadm trigger diff --git a/src/config.cpp b/src/config.cpp index 4bede1a4267..6799e2b7c54 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -434,8 +434,8 @@ namespace config { std::chrono::duration { 1 / 24.9 }, // key_repeat_period { - platf::supported_gamepads().front().data(), - platf::supported_gamepads().front().size(), + platf::supported_gamepads(nullptr).front().name.data(), + platf::supported_gamepads(nullptr).front().name.size(), }, // Default gamepad true, // back as touchpad click enabled (manual DS4 only) true, // client gamepads with motion events are emulated as DS4 @@ -938,6 +938,17 @@ namespace config { return ret; } + std::vector & + get_supported_gamepad_options() { + const auto options = platf::supported_gamepads(nullptr); + static std::vector opts {}; + opts.reserve(options.size()); + for (auto &opt : options) { + opts.emplace_back(opt.name); + } + return opts; + } + void apply_config(std::unordered_map &&vars) { if (!fs::exists(stream.file_apps.c_str())) { @@ -1086,7 +1097,7 @@ namespace config { input.key_repeat_delay = std::chrono::milliseconds { to }; } - string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads()); + string_restricted_f(vars, "gamepad"s, input.gamepad, get_supported_gamepad_options()); bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click); bool_f(vars, "motion_as_ds4", input.motion_as_ds4); bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4); diff --git a/src/input.cpp b/src/input.cpp index 2e26d5b00a8..c9efc343bfa 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -118,7 +118,7 @@ namespace input { void free_gamepad(platf::input_t &platf_input, int id) { - platf::gamepad(platf_input, id, platf::gamepad_state_t {}); + platf::gamepad_update(platf_input, id, platf::gamepad_state_t {}); platf::free_gamepad(platf_input, id); free_id(gamepadMask, id); @@ -711,28 +711,28 @@ namespace input { if (!release) { // Press any synthetic modifiers required for this key if (synthetic_modifiers & MODIFIER_SHIFT) { - platf::keyboard(platf_input, VKEY_SHIFT, false, flags); + platf::keyboard_update(platf_input, VKEY_SHIFT, false, flags); } if (synthetic_modifiers & MODIFIER_CTRL) { - platf::keyboard(platf_input, VKEY_CONTROL, false, flags); + platf::keyboard_update(platf_input, VKEY_CONTROL, false, flags); } if (synthetic_modifiers & MODIFIER_ALT) { - platf::keyboard(platf_input, VKEY_MENU, false, flags); + platf::keyboard_update(platf_input, VKEY_MENU, false, flags); } } - platf::keyboard(platf_input, map_keycode(key_code), release, flags); + platf::keyboard_update(platf_input, map_keycode(key_code), release, flags); if (!release) { // Raise any synthetic modifier keys we pressed if (synthetic_modifiers & MODIFIER_SHIFT) { - platf::keyboard(platf_input, VKEY_SHIFT, true, flags); + platf::keyboard_update(platf_input, VKEY_SHIFT, true, flags); } if (synthetic_modifiers & MODIFIER_CTRL) { - platf::keyboard(platf_input, VKEY_CONTROL, true, flags); + platf::keyboard_update(platf_input, VKEY_CONTROL, true, flags); } if (synthetic_modifiers & MODIFIER_ALT) { - platf::keyboard(platf_input, VKEY_MENU, true, flags); + platf::keyboard_update(platf_input, VKEY_MENU, true, flags); } } } @@ -963,7 +963,7 @@ namespace input { contact_area.second, }; - platf::touch(input->client_context.get(), abs_port, touch); + platf::touch_update(input->client_context.get(), abs_port, touch); } /** @@ -1022,7 +1022,7 @@ namespace input { contact_area.second, }; - platf::pen(input->client_context.get(), abs_port, pen); + platf::pen_update(input->client_context.get(), abs_port, pen); } /** @@ -1211,18 +1211,18 @@ namespace input { // Force the back button up gamepad.back_button_state = button_state_e::UP; state.buttonFlags &= ~platf::BACK; - platf::gamepad(platf_input, gamepad.id, state); + platf::gamepad_update(platf_input, gamepad.id, state); // Press Home button state.buttonFlags |= platf::HOME; - platf::gamepad(platf_input, gamepad.id, state); + platf::gamepad_update(platf_input, gamepad.id, state); // Sleep for a short time to allow the input to be detected std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Release Home button state.buttonFlags &= ~platf::HOME; - platf::gamepad(platf_input, gamepad.id, state); + platf::gamepad_update(platf_input, gamepad.id, state); gamepad.back_timeout_id = nullptr; }; @@ -1236,7 +1236,7 @@ namespace input { } } - platf::gamepad(platf_input, gamepad.id, gamepad_state); + platf::gamepad_update(platf_input, gamepad.id, gamepad_state); gamepad.gamepad_state = gamepad_state; } @@ -1665,7 +1665,7 @@ namespace input { // already released continue; } - platf::keyboard(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first)); + platf::keyboard_update(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first)); key_press[kp.first] = false; } }); @@ -1685,6 +1685,18 @@ namespace input { return std::make_unique(); } + bool + probe_gamepads() { + auto input = static_cast(platf_input.get()); + const auto gamepads = platf::supported_gamepads(input); + for (auto &gamepad : gamepads) { + if (gamepad.is_enabled && gamepad.name != "auto") { + return false; + } + } + return true; + } + std::shared_ptr alloc(safe::mail_t mail) { auto input = std::make_shared( diff --git a/src/input.h b/src/input.h index 33a9ee42741..d95406b9e48 100644 --- a/src/input.h +++ b/src/input.h @@ -22,6 +22,9 @@ namespace input { [[nodiscard]] std::unique_ptr init(); + bool + probe_gamepads(); + std::shared_ptr alloc(safe::mail_t mail); diff --git a/src/main.cpp b/src/main.cpp index 6231e503ef3..ec3aa9b56a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -272,6 +272,11 @@ main(int argc, char *argv[]) { reed_solomon_init(); auto input_deinit_guard = input::init(); + + if (input::probe_gamepads()) { + BOOST_LOG(warning) << "No gamepad input is available"sv; + } + if (video::probe_encoders()) { BOOST_LOG(error) << "Video failed to find working encoder"sv; } diff --git a/src/platform/common.h b/src/platform/common.h index 1960d829878..c388029ba44 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -80,6 +80,12 @@ namespace platf { constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000; constexpr std::uint32_t MISC_BUTTON = 0x200000; + struct supported_gamepad_t { + std::string name; + bool is_enabled; + std::string reason_disabled; + }; + enum class gamepad_feedback_e { rumble, rumble_triggers, @@ -695,9 +701,9 @@ namespace platf { void hscroll(input_t &input, int distance); void - keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags); + keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags); void - gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state); + gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state); void unicode(input_t &input, char *utf8, int size); @@ -718,7 +724,7 @@ namespace platf { * @param touch The touch event. */ void - touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); + touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); /** * @brief Sends a pen event to the OS. @@ -727,7 +733,7 @@ namespace platf { * @param pen The pen event. */ void - pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); + pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); /** * @brief Sends a gamepad touch event to the OS. @@ -784,6 +790,10 @@ namespace platf { [[nodiscard]] std::unique_ptr init(); - std::vector & - supported_gamepads(); + /** + * @brief Gets the supported gamepads for this platform backend. + * @return Vector of gamepad options and status. + */ + std::vector & + supported_gamepads(input_t *input); } // namespace platf diff --git a/src/platform/linux/input/inputtino.cpp b/src/platform/linux/input/inputtino.cpp new file mode 100644 index 00000000000..17344755d93 --- /dev/null +++ b/src/platform/linux/input/inputtino.cpp @@ -0,0 +1,149 @@ +#include +#include + +#include "src/config.h" +#include "src/platform/common.h" +#include "src/utility.h" + +#include "inputtino_common.h" +#include "inputtino_gamepad.h" +#include "inputtino_keyboard.h" +#include "inputtino_mouse.h" +#include "inputtino_pen.h" +#include "inputtino_touch.h" + +using namespace std::literals; + +namespace platf { + + input_t + input() { + return { new input_raw_t() }; + } + + std::unique_ptr + allocate_client_input_context(input_t &input) { + return std::make_unique(input); + } + + void + freeInput(void *p) { + auto *input = (input_raw_t *) p; + delete input; + } + + void + move_mouse(input_t &input, int deltaX, int deltaY) { + auto raw = (input_raw_t *) input.get(); + platf::mouse::move(raw, deltaX, deltaY); + } + + void + abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + auto raw = (input_raw_t *) input.get(); + platf::mouse::move_abs(raw, touch_port, x, y); + } + + void + button_mouse(input_t &input, int button, bool release) { + auto raw = (input_raw_t *) input.get(); + platf::mouse::button(raw, button, release); + } + + void + scroll(input_t &input, int high_res_distance) { + auto raw = (input_raw_t *) input.get(); + platf::mouse::scroll(raw, high_res_distance); + } + + void + hscroll(input_t &input, int high_res_distance) { + auto raw = (input_raw_t *) input.get(); + platf::mouse::hscroll(raw, high_res_distance); + } + + void + keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + auto raw = (input_raw_t *) input.get(); + platf::keyboard::update(raw, modcode, release, flags); + } + + void + unicode(input_t &input, char *utf8, int size) { + auto raw = (input_raw_t *) input.get(); + platf::keyboard::unicode(raw, utf8, size); + } + + void + touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + auto raw = (client_input_raw_t *) input; + platf::touch::update(raw, touch_port, touch); + } + + void + pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + auto raw = (client_input_raw_t *) input; + platf::pen::update(raw, touch_port, pen); + } + + int + alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + auto raw = (input_raw_t *) input.get(); + return platf::gamepad::alloc(raw, id, metadata, feedback_queue); + } + + void + free_gamepad(input_t &input, int nr) { + auto raw = (input_raw_t *) input.get(); + platf::gamepad::free(raw, nr); + } + + void + gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + auto raw = (input_raw_t *) input.get(); + platf::gamepad::update(raw, nr, gamepad_state); + } + + void + gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + auto raw = (input_raw_t *) input.get(); + platf::gamepad::touch(raw, touch); + } + + void + gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + auto raw = (input_raw_t *) input.get(); + platf::gamepad::motion(raw, motion); + } + + void + gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + auto raw = (input_raw_t *) input.get(); + platf::gamepad::battery(raw, battery); + } + + platform_caps::caps_t + get_capabilities() { + platform_caps::caps_t caps = 0; + // TODO: if has_uinput + caps |= platform_caps::pen_touch; + + // We support controller touchpad input only when emulating the PS5 controller + if (config::input.gamepad == "ds5"sv || config::input.gamepad == "auto"sv) { + caps |= platform_caps::controller_touch; + } + + return caps; + } + + util::point_t + get_mouse_loc(input_t &input) { + auto raw = (input_raw_t *) input.get(); + return platf::mouse::get_location(raw); + } + + std::vector & + supported_gamepads(input_t *input) { + return platf::gamepad::supported_gamepads(input); + } +} // namespace platf diff --git a/src/platform/linux/input/inputtino_common.h b/src/platform/linux/input/inputtino_common.h new file mode 100644 index 00000000000..494e8a01d34 --- /dev/null +++ b/src/platform/linux/input/inputtino_common.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include + +#include "src/config.h" +#include "src/logging.h" +#include "src/platform/common.h" +#include "src/utility.h" + +using namespace std::literals; + +namespace platf { + + using joypads_t = std::variant; + + struct joypad_state { + std::unique_ptr joypad; + gamepad_feedback_msg_t last_rumble; + gamepad_feedback_msg_t last_rgb_led; + }; + + struct input_raw_t { + input_raw_t(): + mouse(inputtino::Mouse::create({ + .name = "Mouse passthrough", + .vendor_id = 0xBEEF, + .product_id = 0xDEAD, + .version = 0x111, + })), + keyboard(inputtino::Keyboard::create({ + .name = "Keyboard passthrough", + .vendor_id = 0xBEEF, + .product_id = 0xDEAD, + .version = 0x111, + })), + gamepads(MAX_GAMEPADS) { + if (!mouse) { + BOOST_LOG(warning) << "Unable to create virtual mouse: " << mouse.getErrorMessage(); + } + if (!keyboard) { + BOOST_LOG(warning) << "Unable to create virtual keyboard: " << keyboard.getErrorMessage(); + } + } + + ~input_raw_t() = default; + + // All devices are wrapped in Result because it might be that we aren't able to create them (ex: udev permission denied) + inputtino::Result mouse; + inputtino::Result keyboard; + + /** + * A list of gamepads that are currently connected. + * The pointer is shared because that state will be shared with background threads that deal with rumble and LED + */ + std::vector> gamepads; + }; + + struct client_input_raw_t: public client_input_t { + client_input_raw_t(input_t &input): + touch(inputtino::TouchScreen::create({ + .name = "Touch passthrough", + .vendor_id = 0xBEEF, + .product_id = 0xDEAD, + .version = 0x111, + })), + pen(inputtino::PenTablet::create({ + .name = "Pen passthrough", + .vendor_id = 0xBEEF, + .product_id = 0xDEAD, + .version = 0x111, + })) { + global = (input_raw_t *) input.get(); + if (!touch) { + BOOST_LOG(warning) << "Unable to create virtual touch screen: " << touch.getErrorMessage(); + } + if (!pen) { + BOOST_LOG(warning) << "Unable to create virtual pen tablet: " << pen.getErrorMessage(); + } + } + + input_raw_t *global; + + // Device state and handles for pen and touch input must be stored in the per-client + // input context, because each connected client may be sending their own independent + // pen/touch events. To maintain separation, we expose separate pen and touch devices + // for each client. + inputtino::Result touch; + inputtino::Result pen; + }; + + inline float + deg2rad(float degree) { + return degree * (M_PI / 180.f); + } +} // namespace platf diff --git a/src/platform/linux/input/inputtino_gamepad.cpp b/src/platform/linux/input/inputtino_gamepad.cpp new file mode 100644 index 00000000000..6d2e2347fa9 --- /dev/null +++ b/src/platform/linux/input/inputtino_gamepad.cpp @@ -0,0 +1,297 @@ +#include +#include +#include + +#include "src/config.h" +#include "src/logging.h" +#include "src/platform/common.h" +#include "src/utility.h" + +#include "inputtino_common.h" +#include "inputtino_gamepad.h" + +using namespace std::literals; + +namespace platf::gamepad { + + enum GamepadStatus { + UHID_NOT_AVAILABLE = 0, + UINPUT_NOT_AVAILABLE, + XINPUT_NOT_AVAILABLE, + GAMEPAD_STATUS // Helper to indicate the number of status + }; + + auto + create_xbox_one() { + return inputtino::XboxOneJoypad::create({ .name = "Sunshine X-Box One (virtual) pad", + // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 + .vendor_id = 0x045E, + .product_id = 0x02EA, + .version = 0x0408 }); + } + + auto + create_switch() { + return inputtino::SwitchJoypad::create({ .name = "Sunshine Nintendo (virtual) pad", + // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 + .vendor_id = 0x057e, + .product_id = 0x2009, + .version = 0x8111 }); + } + + auto + create_ds5() { + return inputtino::PS5Joypad::create({ .name = "Sunshine DualSense (virtual) pad", + .vendor_id = 0x054C, + .product_id = 0x0CE6, + .version = 0x8111 }); + } + + int + alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + ControllerType selectedGamepadType; + + if (config::input.gamepad == "xone"sv) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (manual selection)"sv; + selectedGamepadType = XboxOneWired; + } + else if (config::input.gamepad == "ds5"sv) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense 5 controller (manual selection)"sv; + selectedGamepadType = DualSenseWired; + } + else if (config::input.gamepad == "switch"sv) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (manual selection)"sv; + selectedGamepadType = SwitchProWired; + } + else if (metadata.type == LI_CTYPE_XBOX) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (auto-selected by client-reported type)"sv; + selectedGamepadType = XboxOneWired; + } + else if (metadata.type == LI_CTYPE_PS) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by client-reported type)"sv; + selectedGamepadType = DualSenseWired; + } + else if (metadata.type == LI_CTYPE_NINTENDO) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (auto-selected by client-reported type)"sv; + selectedGamepadType = SwitchProWired; + } + else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by motion sensor presence)"sv; + selectedGamepadType = DualSenseWired; + } + else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by touchpad presence)"sv; + selectedGamepadType = DualSenseWired; + } + else { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (default)"sv; + selectedGamepadType = XboxOneWired; + } + + if (selectedGamepadType == XboxOneWired || selectedGamepadType == SwitchProWired) { + if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has motion sensors, but they are not usable when emulating a joypad different from DS5"sv; + } + if (metadata.capabilities & LI_CCAP_TOUCHPAD) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has a touchpad, but it is not usable when emulating a joypad different from DS5"sv; + } + if (metadata.capabilities & LI_CCAP_RGB_LED) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating a joypad different from DS5"sv; + } + } + else if (selectedGamepadType == DualSenseWired) { + if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have motion sensors active"sv; + } + if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have a touchpad"sv; + } + } + + auto gamepad = std::make_shared(joypad_state {}); + auto on_rumble_fn = [feedback_queue, idx = id.clientRelativeIndex, gamepad](int low_freq, int high_freq) { + // Don't resend duplicate rumble data + if (gamepad->last_rumble.type == platf::gamepad_feedback_e::rumble && gamepad->last_rumble.data.rumble.lowfreq == low_freq && gamepad->last_rumble.data.rumble.highfreq == high_freq) { + return; + } + + gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble(idx, low_freq, high_freq); + feedback_queue->raise(msg); + gamepad->last_rumble = msg; + }; + + switch (selectedGamepadType) { + case XboxOneWired: { + auto xOne = create_xbox_one(); + if (xOne) { + (*xOne).set_on_rumble(on_rumble_fn); + gamepad->joypad = std::make_unique(std::move(*xOne)); + raw->gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } + else { + BOOST_LOG(warning) << "Unable to create virtual Xbox One controller: " << xOne.getErrorMessage(); + return -1; + } + } + case SwitchProWired: { + auto switchPro = create_switch(); + if (switchPro) { + (*switchPro).set_on_rumble(on_rumble_fn); + gamepad->joypad = std::make_unique(std::move(*switchPro)); + raw->gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } + else { + BOOST_LOG(warning) << "Unable to create virtual Switch Pro controller: " << switchPro.getErrorMessage(); + return -1; + } + } + case DualSenseWired: { + auto ds5 = create_ds5(); + if (ds5) { + (*ds5).set_on_rumble(on_rumble_fn); + (*ds5).set_on_led([feedback_queue, idx = id.clientRelativeIndex, gamepad](int r, int g, int b) { + // Don't resend duplicate LED data + if (gamepad->last_rgb_led.type == platf::gamepad_feedback_e::set_rgb_led && gamepad->last_rgb_led.data.rgb_led.r == r && gamepad->last_rgb_led.data.rgb_led.g == g && gamepad->last_rgb_led.data.rgb_led.b == b) { + return; + } + + auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b); + feedback_queue->raise(msg); + gamepad->last_rgb_led = msg; + }); + + // Activate the motion sensors + feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); + feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); + + gamepad->joypad = std::make_unique(std::move(*ds5)); + raw->gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } + else { + BOOST_LOG(warning) << "Unable to create virtual DualShock 5 controller: " << ds5.getErrorMessage(); + return -1; + } + } + } + return -1; + } + + void + free(input_raw_t *raw, int nr) { + // This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device) + raw->gamepads[nr]->joypad.reset(); + raw->gamepads[nr].reset(); + } + + void + update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) { + auto gamepad = raw->gamepads[nr]; + if (!gamepad) { + return; + } + + std::visit([gamepad_state](inputtino::Joypad &gc) { + gc.set_pressed_buttons(gamepad_state.buttonFlags); + gc.set_stick(inputtino::Joypad::LS, gamepad_state.lsX, gamepad_state.lsY); + gc.set_stick(inputtino::Joypad::RS, gamepad_state.rsX, gamepad_state.rsY); + gc.set_triggers(gamepad_state.lt, gamepad_state.rt); + }, + *gamepad->joypad); + } + + void + touch(input_raw_t *raw, const gamepad_touch_t &touch) { + auto gamepad = raw->gamepads[touch.id.globalIndex]; + if (!gamepad) { + return; + } + // Only the PS5 controller supports touch input + if (std::holds_alternative(*gamepad->joypad)) { + if (touch.pressure > 0.5) { + std::get(*gamepad->joypad).place_finger(touch.pointerId, touch.x * inputtino::PS5Joypad::touchpad_width, touch.y * inputtino::PS5Joypad::touchpad_height); + } + else { + std::get(*gamepad->joypad).release_finger(touch.pointerId); + } + } + } + + void + motion(input_raw_t *raw, const gamepad_motion_t &motion) { + auto gamepad = raw->gamepads[motion.id.globalIndex]; + if (!gamepad) { + return; + } + // Only the PS5 controller supports motion + if (std::holds_alternative(*gamepad->joypad)) { + switch (motion.motionType) { + case LI_MOTION_TYPE_ACCEL: + std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::ACCELERATION, motion.x, motion.y, motion.z); + break; + case LI_MOTION_TYPE_GYRO: + std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::GYROSCOPE, deg2rad(motion.x), deg2rad(motion.y), deg2rad(motion.z)); + break; + } + } + } + + void + battery(input_raw_t *raw, const gamepad_battery_t &battery) { + auto gamepad = raw->gamepads[battery.id.globalIndex]; + if (!gamepad) { + return; + } + // Only the PS5 controller supports motion + if (std::holds_alternative(*gamepad->joypad)) { + inputtino::PS5Joypad::BATTERY_STATE state = inputtino::PS5Joypad::CHARGHING_ERROR; + switch (battery.state) { + case LI_BATTERY_STATE_CHARGING: + state = inputtino::PS5Joypad::BATTERY_CHARGHING; + break; + case LI_BATTERY_STATE_DISCHARGING: + state = inputtino::PS5Joypad::BATTERY_DISCHARGING; + break; + case LI_BATTERY_STATE_FULL: + state = inputtino::PS5Joypad::BATTERY_FULL; + break; + } + std::get(*gamepad->joypad).set_battery(state, battery.percentage); + } + } + + std::vector & + supported_gamepads(input_t *input) { + if (!input) { + static std::vector gps { + supported_gamepad_t { "auto", true, "" }, + supported_gamepad_t { "xone", false, "" }, + supported_gamepad_t { "ds5", false, "" }, + supported_gamepad_t { "switch", false, "" }, + }; + + return gps; + } + + auto ds5 = create_ds5(); + auto switchPro = create_switch(); + auto xOne = create_xbox_one(); + + static std::vector gps { + supported_gamepad_t { "auto", true, "" }, + supported_gamepad_t { "xone", static_cast(xOne), !xOne ? xOne.getErrorMessage() : "" }, + supported_gamepad_t { "ds5", static_cast(ds5), !ds5 ? ds5.getErrorMessage() : "" }, + supported_gamepad_t { "switch", static_cast(switchPro), !switchPro ? switchPro.getErrorMessage() : "" }, + }; + + for (auto &[name, is_enabled, reason_disabled] : gps) { + if (!is_enabled) { + BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; + } + } + + return gps; + } +} // namespace platf::gamepad diff --git a/src/platform/linux/input/inputtino_gamepad.h b/src/platform/linux/input/inputtino_gamepad.h new file mode 100644 index 00000000000..f3eb4e5e07c --- /dev/null +++ b/src/platform/linux/input/inputtino_gamepad.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include "src/platform/common.h" + +#include "inputtino_common.h" + +using namespace std::literals; + +namespace platf::gamepad { + + enum ControllerType { + XboxOneWired, + DualSenseWired, + SwitchProWired + }; + + int + alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); + + void + free(input_raw_t *raw, int nr); + + void + update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state); + + void + touch(input_raw_t *raw, const gamepad_touch_t &touch); + + void + motion(input_raw_t *raw, const gamepad_motion_t &motion); + + void + battery(input_raw_t *raw, const gamepad_battery_t &battery); + + std::vector & + supported_gamepads(input_t *input); +} // namespace platf::gamepad diff --git a/src/platform/linux/input/inputtino_keyboard.cpp b/src/platform/linux/input/inputtino_keyboard.cpp new file mode 100644 index 00000000000..67def659e13 --- /dev/null +++ b/src/platform/linux/input/inputtino_keyboard.cpp @@ -0,0 +1,117 @@ +#include +#include +#include + +#include "src/config.h" +#include "src/logging.h" +#include "src/platform/common.h" +#include "src/utility.h" + +#include "inputtino_common.h" +#include "inputtino_keyboard.h" + +using namespace std::literals; + +namespace platf::keyboard { + + /** + * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) + * + * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 + * + * adapted from: https://stackoverflow.com/a/7639754 + */ + std::string + to_hex(const std::basic_string &str) { + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for (const auto &ch : str) { + ss << static_cast(ch); + } + + std::string hex_unicode(ss.str()); + std::ranges::transform(hex_unicode, hex_unicode.begin(), ::toupper); + return hex_unicode; + } + + /** + * A map of linux scan code -> Moonlight keyboard code + */ + static const std::map key_mappings = { + { KEY_BACKSPACE, 0x08 }, { KEY_TAB, 0x09 }, { KEY_ENTER, 0x0D }, { KEY_LEFTSHIFT, 0x10 }, + { KEY_LEFTCTRL, 0x11 }, { KEY_CAPSLOCK, 0x14 }, { KEY_ESC, 0x1B }, { KEY_SPACE, 0x20 }, + { KEY_PAGEUP, 0x21 }, { KEY_PAGEDOWN, 0x22 }, { KEY_END, 0x23 }, { KEY_HOME, 0x24 }, + { KEY_LEFT, 0x25 }, { KEY_UP, 0x26 }, { KEY_RIGHT, 0x27 }, { KEY_DOWN, 0x28 }, + { KEY_SYSRQ, 0x2C }, { KEY_INSERT, 0x2D }, { KEY_DELETE, 0x2E }, { KEY_0, 0x30 }, + { KEY_1, 0x31 }, { KEY_2, 0x32 }, { KEY_3, 0x33 }, { KEY_4, 0x34 }, + { KEY_5, 0x35 }, { KEY_6, 0x36 }, { KEY_7, 0x37 }, { KEY_8, 0x38 }, + { KEY_9, 0x39 }, { KEY_A, 0x41 }, { KEY_B, 0x42 }, { KEY_C, 0x43 }, + { KEY_D, 0x44 }, { KEY_E, 0x45 }, { KEY_F, 0x46 }, { KEY_G, 0x47 }, + { KEY_H, 0x48 }, { KEY_I, 0x49 }, { KEY_J, 0x4A }, { KEY_K, 0x4B }, + { KEY_L, 0x4C }, { KEY_M, 0x4D }, { KEY_N, 0x4E }, { KEY_O, 0x4F }, + { KEY_P, 0x50 }, { KEY_Q, 0x51 }, { KEY_R, 0x52 }, { KEY_S, 0x53 }, + { KEY_T, 0x54 }, { KEY_U, 0x55 }, { KEY_V, 0x56 }, { KEY_W, 0x57 }, + { KEY_X, 0x58 }, { KEY_Y, 0x59 }, { KEY_Z, 0x5A }, { KEY_LEFTMETA, 0x5B }, + { KEY_RIGHTMETA, 0x5C }, { KEY_KP0, 0x60 }, { KEY_KP1, 0x61 }, { KEY_KP2, 0x62 }, + { KEY_KP3, 0x63 }, { KEY_KP4, 0x64 }, { KEY_KP5, 0x65 }, { KEY_KP6, 0x66 }, + { KEY_KP7, 0x67 }, { KEY_KP8, 0x68 }, { KEY_KP9, 0x69 }, { KEY_KPASTERISK, 0x6A }, + { KEY_KPPLUS, 0x6B }, { KEY_KPMINUS, 0x6D }, { KEY_KPDOT, 0x6E }, { KEY_KPSLASH, 0x6F }, + { KEY_F1, 0x70 }, { KEY_F2, 0x71 }, { KEY_F3, 0x72 }, { KEY_F4, 0x73 }, + { KEY_F5, 0x74 }, { KEY_F6, 0x75 }, { KEY_F7, 0x76 }, { KEY_F8, 0x77 }, + { KEY_F9, 0x78 }, { KEY_F10, 0x79 }, { KEY_F11, 0x7A }, { KEY_F12, 0x7B }, + { KEY_NUMLOCK, 0x90 }, { KEY_SCROLLLOCK, 0x91 }, { KEY_LEFTSHIFT, 0xA0 }, { KEY_RIGHTSHIFT, 0xA1 }, + { KEY_LEFTCTRL, 0xA2 }, { KEY_RIGHTCTRL, 0xA3 }, { KEY_LEFTALT, 0xA4 }, { KEY_RIGHTALT, 0xA5 }, + { KEY_SEMICOLON, 0xBA }, { KEY_EQUAL, 0xBB }, { KEY_COMMA, 0xBC }, { KEY_MINUS, 0xBD }, + { KEY_DOT, 0xBE }, { KEY_SLASH, 0xBF }, { KEY_GRAVE, 0xC0 }, { KEY_LEFTBRACE, 0xDB }, + { KEY_BACKSLASH, 0xDC }, { KEY_RIGHTBRACE, 0xDD }, { KEY_APOSTROPHE, 0xDE }, { KEY_102ND, 0xE2 } + }; + + void + update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) { + if (raw->keyboard) { + if (release) { + (*raw->keyboard).release(modcode); + } + else { + (*raw->keyboard).press(modcode); + } + } + } + + void + unicode(input_raw_t *raw, char *utf8, int size) { + if (raw->keyboard) { + /* Reading input text as UTF-8 */ + auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); + /* Converting to UTF-32 */ + auto utf32_str = boost::locale::conv::utf_to_utf(utf8_str); + /* To HEX string */ + auto hex_unicode = to_hex(utf32_str); + BOOST_LOG(debug) << "Unicode, typing U+"sv << hex_unicode; + + /* pressing + + U */ + (*raw->keyboard).press(0xA2); // LEFTCTRL + (*raw->keyboard).press(0xA0); // LEFTSHIFT + (*raw->keyboard).press(0x55); // U + (*raw->keyboard).release(0x55); // U + + /* input each HEX character */ + for (auto &ch : hex_unicode) { + auto key_str = "KEY_"s + ch; + auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); + auto wincode = key_mappings.find(keycode); + if (keycode == -1 || wincode == key_mappings.end()) { + BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; + } + else { + (*raw->keyboard).press(wincode->second); + (*raw->keyboard).release(wincode->second); + } + } + + /* releasing and */ + (*raw->keyboard).release(0xA0); // LEFTSHIFT + (*raw->keyboard).release(0xA2); // LEFTCTRL + } + } +} // namespace platf::keyboard diff --git a/src/platform/linux/input/inputtino_keyboard.h b/src/platform/linux/input/inputtino_keyboard.h new file mode 100644 index 00000000000..254aac356d6 --- /dev/null +++ b/src/platform/linux/input/inputtino_keyboard.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +#include "inputtino_common.h" + +using namespace std::literals; + +namespace platf::keyboard { + void + update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags); + + void + unicode(input_raw_t *raw, char *utf8, int size); +} // namespace platf::keyboard diff --git a/src/platform/linux/input/inputtino_mouse.cpp b/src/platform/linux/input/inputtino_mouse.cpp new file mode 100644 index 00000000000..3bf6da835ab --- /dev/null +++ b/src/platform/linux/input/inputtino_mouse.cpp @@ -0,0 +1,88 @@ +#include +#include +#include + +#include "src/config.h" +#include "src/logging.h" +#include "src/platform/common.h" +#include "src/utility.h" + +#include "inputtino_common.h" +#include "inputtino_mouse.h" + +using namespace std::literals; + +namespace platf::mouse { + + void + move(input_raw_t *raw, int deltaX, int deltaY) { + if (raw->mouse) { + (*raw->mouse).move(deltaX, deltaY); + } + } + + void + move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) { + if (raw->mouse) { + (*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height); + } + } + + void + button(input_raw_t *raw, int button, bool release) { + if (raw->mouse) { + inputtino::Mouse::MOUSE_BUTTON btn_type; + switch (button) { + case BUTTON_LEFT: + btn_type = inputtino::Mouse::LEFT; + break; + case BUTTON_MIDDLE: + btn_type = inputtino::Mouse::MIDDLE; + break; + case BUTTON_RIGHT: + btn_type = inputtino::Mouse::RIGHT; + break; + case BUTTON_X1: + btn_type = inputtino::Mouse::SIDE; + break; + case BUTTON_X2: + btn_type = inputtino::Mouse::EXTRA; + break; + default: + BOOST_LOG(warning) << "Unknown mouse button: " << button; + return; + } + if (release) { + (*raw->mouse).release(btn_type); + } + else { + (*raw->mouse).press(btn_type); + } + } + } + + void + scroll(input_raw_t *raw, int high_res_distance) { + if (raw->mouse) { + (*raw->mouse).vertical_scroll(high_res_distance); + } + } + + void + hscroll(input_raw_t *raw, int high_res_distance) { + if (raw->mouse) { + (*raw->mouse).horizontal_scroll(high_res_distance); + } + } + + util::point_t + get_location(input_raw_t *raw) { + if (raw->mouse) { + // TODO: decide what to do after https://github.com/games-on-whales/inputtino/issues/6 is resolved. + // TODO: auto x = (*raw->mouse).get_absolute_x(); + // TODO: auto y = (*raw->mouse).get_absolute_y(); + return { 0, 0 }; + } + return { 0, 0 }; + } +} // namespace platf::mouse diff --git a/src/platform/linux/input/inputtino_mouse.h b/src/platform/linux/input/inputtino_mouse.h new file mode 100644 index 00000000000..c891407b2b4 --- /dev/null +++ b/src/platform/linux/input/inputtino_mouse.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +#include "src/platform/common.h" + +#include "inputtino_common.h" + +using namespace std::literals; + +namespace platf::mouse { + void + move(input_raw_t *raw, int deltaX, int deltaY); + + void + move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y); + + void + button(input_raw_t *raw, int button, bool release); + + void + scroll(input_raw_t *raw, int high_res_distance); + + void + hscroll(input_raw_t *raw, int high_res_distance); + + util::point_t + get_location(input_raw_t *raw); +} // namespace platf::mouse diff --git a/src/platform/linux/input/inputtino_pen.cpp b/src/platform/linux/input/inputtino_pen.cpp new file mode 100644 index 00000000000..e0ed341ef3a --- /dev/null +++ b/src/platform/linux/input/inputtino_pen.cpp @@ -0,0 +1,69 @@ +#include +#include +#include + +#include "src/config.h" +#include "src/logging.h" +#include "src/platform/common.h" +#include "src/utility.h" + +#include "inputtino_common.h" +#include "inputtino_pen.h" + +using namespace std::literals; + +namespace platf::pen { + void + update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) { + if (raw->pen) { + // First set the buttons + (*raw->pen).set_btn(inputtino::PenTablet::PRIMARY, pen.penButtons & LI_PEN_BUTTON_PRIMARY); + (*raw->pen).set_btn(inputtino::PenTablet::SECONDARY, pen.penButtons & LI_PEN_BUTTON_SECONDARY); + (*raw->pen).set_btn(inputtino::PenTablet::TERTIARY, pen.penButtons & LI_PEN_BUTTON_TERTIARY); + + // Set the tool + inputtino::PenTablet::TOOL_TYPE tool; + switch (pen.toolType) { + case LI_TOOL_TYPE_PEN: + tool = inputtino::PenTablet::PEN; + break; + case LI_TOOL_TYPE_ERASER: + tool = inputtino::PenTablet::ERASER; + break; + default: + tool = inputtino::PenTablet::SAME_AS_BEFORE; + break; + } + + // Normalize rotation value to 0-359 degree range + auto rotation = pen.rotation; + if (rotation != LI_ROT_UNKNOWN) { + rotation %= 360; + } + + // Here we receive: + // - Rotation: degrees from vertical in Y dimension (parallel to screen, 0..360) + // - Tilt: degrees from vertical in Z dimension (perpendicular to screen, 0..90) + float tilt_x = 0; + float tilt_y = 0; + // Convert polar coordinates into Y tilt angles + if (pen.tilt != LI_TILT_UNKNOWN && rotation != LI_ROT_UNKNOWN) { + auto rotation_rads = deg2rad(rotation); + auto tilt_rads = deg2rad(pen.tilt); + auto r = std::sin(tilt_rads); + auto z = std::cos(tilt_rads); + + tilt_x = std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI; + tilt_y = std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI; + } + + (*raw->pen).place_tool(tool, + pen.x, + pen.y, + pen.eventType == LI_TOUCH_EVENT_DOWN ? pen.pressureOrDistance : -1, + pen.eventType == LI_TOUCH_EVENT_HOVER ? pen.pressureOrDistance : -1, + tilt_x, + tilt_y); + } + } +} // namespace platf::pen diff --git a/src/platform/linux/input/inputtino_pen.h b/src/platform/linux/input/inputtino_pen.h new file mode 100644 index 00000000000..1914748866d --- /dev/null +++ b/src/platform/linux/input/inputtino_pen.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#include "src/platform/common.h" + +#include "inputtino_common.h" + +using namespace std::literals; + +namespace platf::pen { + void + update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen); +} diff --git a/src/platform/linux/input/inputtino_touch.cpp b/src/platform/linux/input/inputtino_touch.cpp new file mode 100644 index 00000000000..035c4f1db43 --- /dev/null +++ b/src/platform/linux/input/inputtino_touch.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +#include "src/config.h" +#include "src/logging.h" +#include "src/platform/common.h" +#include "src/utility.h" + +#include "inputtino_common.h" +#include "inputtino_touch.h" + +using namespace std::literals; + +namespace platf::touch { + void + update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) { + if (raw->touch) { + switch (touch.eventType) { + case LI_TOUCH_EVENT_HOVER: + case LI_TOUCH_EVENT_DOWN: + case LI_TOUCH_EVENT_MOVE: { + // Convert our 0..360 range to -90..90 relative to Y axis + int adjusted_angle = touch.rotation; + + if (adjusted_angle > 90 && adjusted_angle < 270) { + // Lower hemisphere + adjusted_angle = 180 - adjusted_angle; + } + + // Wrap the value if it's out of range + if (adjusted_angle > 90) { + adjusted_angle -= 360; + } + else if (adjusted_angle < -90) { + adjusted_angle += 360; + } + (*raw->touch).place_finger(touch.pointerId, touch.x, touch.y, touch.pressureOrDistance, adjusted_angle); + break; + } + case LI_TOUCH_EVENT_CANCEL: + case LI_TOUCH_EVENT_UP: + case LI_TOUCH_EVENT_HOVER_LEAVE: { + (*raw->touch).release_finger(touch.pointerId); + break; + } + // TODO: LI_TOUCH_EVENT_CANCEL_ALL + } + } + } +} // namespace platf::touch diff --git a/src/platform/linux/input/inputtino_touch.h b/src/platform/linux/input/inputtino_touch.h new file mode 100644 index 00000000000..b3a4b842309 --- /dev/null +++ b/src/platform/linux/input/inputtino_touch.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#include "src/platform/common.h" + +#include "inputtino_common.h" + +using namespace std::literals; + +namespace platf::touch { + void + update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch); +} diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input/legacy_input.cpp similarity index 99% rename from src/platform/linux/input.cpp rename to src/platform/linux/input/legacy_input.cpp index 4af7c568eee..d8362fd6995 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input/legacy_input.cpp @@ -1,6 +1,7 @@ /** - * @file src/platform/linux/input.cpp - * @brief todo + * @file src/platform/linux/input/legacy_input.cpp + * @brief Implementation of input handling, prior to migration to inputtino + * @todo Remove this file after the next stable release */ #include #include @@ -32,7 +33,7 @@ extern "C" { #include "src/platform/common.h" -#include "misc.h" +#include "src/platform/linux/misc.h" // Support older versions #ifndef REL_HWHEEL_HI_RES @@ -1502,7 +1503,7 @@ namespace platf { * ``` */ void - keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto keyboard = ((input_raw_t *) input.get())->keyboard_input.get(); if (!keyboard) { x_keyboard(input, modcode, release, flags); @@ -1617,7 +1618,7 @@ namespace platf { } void - gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *) input.get())->gamepads[nr]); auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags; @@ -1764,7 +1765,7 @@ namespace platf { * @param touch The touch event. */ void - touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { auto raw = (client_input_raw_t *) input; if (!raw->touch_input) { @@ -1949,7 +1950,7 @@ namespace platf { * @param pen The pen event. */ void - pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { auto raw = (client_input_raw_t *) input; if (!raw->pen_input) { @@ -2594,9 +2595,11 @@ namespace platf { delete input; } - std::vector & - supported_gamepads() { - static std::vector gamepads { "x360"sv }; + std::vector & + supported_gamepads(input_t *input) { + static std::vector gamepads { + supported_gamepad_t { "x360", true, "" }, + }; return gamepads; } diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index ec2822ad356..cbca3fffab5 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -243,7 +243,7 @@ const KeyCodeMap kKeyCodesMap[] = { } void - keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto key = keysym(modcode); BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; @@ -317,7 +317,7 @@ const KeyCodeMap kKeyCodesMap[] = { } void - gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; } @@ -492,7 +492,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param touch The touch event. */ void - touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { // Unimplemented feature - platform_caps::pen_touch } @@ -503,7 +503,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param pen The pen event. */ void - pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { // Unimplemented feature - platform_caps::pen_touch } @@ -596,9 +596,11 @@ const KeyCodeMap kKeyCodesMap[] = { delete input; } - std::vector & - supported_gamepads() { - static std::vector gamepads { ""sv }; + std::vector & + supported_gamepads(input_t *input) { + static std::vector gamepads { + supported_gamepad_t { "", false, "gamepads.macos_not_implemented" } + }; return gamepads; } diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 74aba6e09ef..2a917dd5e25 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -612,7 +612,7 @@ namespace platf { } void - keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { INPUT i {}; i.type = INPUT_KEYBOARD; auto &ki = i.ki; @@ -919,7 +919,7 @@ namespace platf { * @param touch The touch event. */ void - touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { auto raw = (client_input_raw_t *) input; // Bail if we're not running on an OS that supports virtual touch input @@ -1050,7 +1050,7 @@ namespace platf { * @param pen The pen event. */ void - pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { auto raw = (client_input_raw_t *) input; // Bail if we're not running on an OS that supports virtual pen input @@ -1482,7 +1482,7 @@ namespace platf { * @param gamepad_state The gamepad button/axis state sent from the client. */ void - gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support @@ -1734,17 +1734,32 @@ namespace platf { delete input; } - /** - * @brief Gets the supported gamepads for this platform backend. - * @return Vector of gamepad type strings. - */ - std::vector & - supported_gamepads() { + std::vector & + supported_gamepads(input_t *input) { + bool enabled; + if (input) { + auto vigem = ((input_raw_t *) input)->vigem; + enabled = vigem != nullptr; + } + else { + enabled = false; + } + + auto reason = enabled ? "" : "gamepads.vigem-not-available"; + // ds4 == ps4 - static std::vector gps { - "auto"sv, "x360"sv, "ds4"sv, "ps4"sv + static std::vector gps { + supported_gamepad_t { "auto", true, reason }, + supported_gamepad_t { "x360", enabled, reason }, + supported_gamepad_t { "ds4", enabled, reason } }; + for (auto &[name, is_enabled, reason_disabled] : gps) { + if (!is_enabled) { + BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; + } + } + return gps; } diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue index a5dd014870b..a2f0c249dfc 100644 --- a/src_assets/common/assets/web/configs/tabs/Inputs.vue +++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue @@ -1,5 +1,6 @@