diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 967eb8c1..2af69ad3 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: DoozyX/clang-format-lint-action@v0.14 with: diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0f35aa1c..f2c2b8e8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Set derived configuration variables: # - images: images to build (docker and/or github) @@ -109,9 +109,9 @@ jobs: file: docker/gstreamer.Dockerfile push: true build-args: | - GSTREAMER_VERSION=1.22.7 + GSTREAMER_VERSION=1.24.5 BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/gpu-drivers:2023.11 - tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.22.7,gameonwhales/gstreamer:1.22.7 # TODO: set gstreamer version as param + tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.24.5,gameonwhales/gstreamer:1.24.5 # TODO: set gstreamer version as param labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache,mode=max @@ -126,7 +126,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/gstreamer:1.22.7 + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/gstreamer:1.24.5 IMAGE_SOURCE=${{ steps.prep.outputs.github_server_url }}/${{ github.repository }} cache-from: ${{ steps.prep.outputs.cache_from }} cache-to: ${{ steps.prep.outputs.cache_to }} \ No newline at end of file diff --git a/.github/workflows/linux-build-test.yml b/.github/workflows/linux-build-test.yml index 37ae20d9..18c74b67 100644 --- a/.github/workflows/linux-build-test.yml +++ b/.github/workflows/linux-build-test.yml @@ -1,4 +1,3 @@ -# Adapted from https://github.com/catchorg/Catch2/blob/devel/.github/workflows/linux-simple-builds.yml name: Linux build and test on: @@ -16,7 +15,7 @@ jobs: timeout-minutes: 30 runs-on: [ self-hosted, ARM64 ] # self-hosted, using Oracle free tier instance steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Prepare environment run: | @@ -47,6 +46,16 @@ jobs: cargo install cargo-c cargo cinstall -p c-bindings --prefix=/usr/local + - name: Archive libgstwaylanddisplay-aarch64 + uses: actions/upload-artifact@v4 + with: + name: libgstwaylanddisplay-aarch64 + path: | + /usr/local/lib/aarch64-linux-gnu/liblibgstwaylanddisplay* + /usr/local/lib/aarch64-linux-gnu/pkgconfig/ + /usr/local/include/libgstwaylanddisplay/* + if-no-files-found: error + - name: Configure build working-directory: ${{runner.workspace}} run: | @@ -56,26 +65,70 @@ jobs: -DCMAKE_CXX_EXTENSIONS=OFF \ -DCMAKE_CXX_STANDARD=17 \ -DTEST_VIRTUAL_INPUT=OFF \ - -DTEST_LIBINPUT=OFF \ -DTEST_DOCKER=ON \ - -DLINK_RUST_WAYLAND=ON \ - -DTEST_RUST_WAYLAND=OFF \ + -DTEST_RUST_WAYLAND=ON \ -DTEST_NVIDIA=OFF \ -DTEST_EXCEPTIONS=OFF \ - -DTEST_SDL=OFF \ - -DCARGO_TARGET_BUILD=aarch64-unknown-linux-gnu \ + -DTEST_UHID=OFF \ -G Ninja - name: Build tests + lib working-directory: ${{runner.workspace}}/build - run: ninja -j 4 wolftests + run: ninja -j $(nproc) wolftests - name: Run tests working-directory: ${{runner.workspace}}/build/tests - run: ./wolftests + env: + RUST_BACKTRACE: FULL + RUST_LOG: FATAL + XDG_RUNTIME_DIR: /tmp + run: ./wolftests --reporter JUnit::out=${{runner.workspace}}/report.xml --reporter console::out=-::colour-mode=ansi + + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() # run this step even if previous step failed + with: + name: aarch64 + path: ${{runner.workspace}}/report.xml + reporter: java-junit + + # First build the common dependencies: Rust-based libgstwaylanddisplay + build-gst-wayland: + runs-on: ubuntu-22.04 + steps: + - name: Prepare environment + # ubuntu-latest breaks without libunwind-dev, + # see: https://github.com/actions/runner-images/issues/6399#issuecomment-1286050292 + run: | + sudo apt-get update -y + sudo apt-get install -y libunwind-dev + sudo apt-get install -y \ + libwayland-dev libwayland-server0 libinput-dev libxkbcommon-dev libgbm-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup gst-wayland-display + run: | + git clone https://github.com/games-on-whales/gst-wayland-display + cd gst-wayland-display + cargo install cargo-c + cargo cinstall -p c-bindings --prefix=/usr/local --destdir=${{runner.workspace}} + + - name: Archive libgstwaylanddisplay-x86_64 + uses: actions/upload-artifact@v4 + with: + name: libgstwaylanddisplay-x86_64 + path: | + ${{runner.workspace}}/usr/local/lib/x86_64-linux-gnu/liblibgstwaylanddisplay* + ${{runner.workspace}}/usr/local/lib/x86_64-linux-gnu/pkgconfig/ + ${{runner.workspace}}/usr/local/include/libgstwaylanddisplay/* + if-no-files-found: error test: runs-on: ubuntu-22.04 + needs: build-gst-wayland strategy: fail-fast: false matrix: @@ -99,7 +152,18 @@ jobs: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Download pre-built libgstwaylanddisplay-x86_64 + uses: actions/download-artifact@v4 + with: + name: libgstwaylanddisplay-x86_64 + path: ${{runner.workspace}}/libgstwaylanddisplay + + - name: Move the library in the right place + run: | + ls -R ${{runner.workspace}}/libgstwaylanddisplay + sudo cp -rn ${{runner.workspace}}/libgstwaylanddisplay/* /usr/local/ - name: Prepare environment # ubuntu-latest breaks without libunwind-dev, @@ -122,17 +186,6 @@ jobs: libunwind-dev \ ${{ join(matrix.other_pkgs, ' ') }} - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - - - name: Setup gst-wayland-display - run: | - git clone https://github.com/games-on-whales/gst-wayland-display - cd gst-wayland-display - cargo install cargo-c - cargo cinstall -p c-bindings --prefix=/usr/local --destdir=temp - sudo cp -r temp/usr/local/* /usr/local/ - - name: Configure build working-directory: ${{runner.workspace}} env: @@ -149,7 +202,6 @@ jobs: -DBUILD_SHARED_LIBS=${{ matrix.shared }} \ -DCATCH_DEVELOPMENT_BUILD=ON \ -DTEST_VIRTUAL_INPUT=OFF \ - -DTEST_LIBINPUT=OFF \ -DTEST_DOCKER=OFF \ -DLINK_RUST_WAYLAND=ON \ -DTEST_RUST_WAYLAND=OFF \ @@ -159,8 +211,16 @@ jobs: - name: Build tests + lib working-directory: ${{runner.workspace}}/build - run: ninja -j 2 wolftests + run: ninja -j $(nproc) wolftests - name: Run tests working-directory: ${{runner.workspace}}/build/tests - run: ./wolftests + run: ./wolftests --reporter JUnit::out=${{runner.workspace}}/report.xml --reporter console::out=-::colour-mode=ansi + + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() # run this step even if previous step failed + with: + name: ${{matrix.cxx}} - STD ${{ matrix.std }} - Shared ${{ matrix.shared }} + path: ${{runner.workspace}}/report.xml + reporter: java-junit diff --git a/.github/workflows/mirror-repo.yml b/.github/workflows/mirror-repo.yml index d2e8b987..063e2fb1 100644 --- a/.github/workflows/mirror-repo.yml +++ b/.github/workflows/mirror-repo.yml @@ -5,7 +5,7 @@ jobs: codeberg: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: pixta-dev/repository-mirroring-action@v1 diff --git a/docker/gstreamer.Dockerfile b/docker/gstreamer.Dockerfile index 16f56832..c53ca308 100644 --- a/docker/gstreamer.Dockerfile +++ b/docker/gstreamer.Dockerfile @@ -4,7 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive ENV BUILD_ARCHITECTURE=amd64 ENV DEB_BUILD_OPTIONS=noddebs -ARG GSTREAMER_VERSION=1.22.7 +ARG GSTREAMER_VERSION=1.24.5 ENV GSTREAMER_VERSION=$GSTREAMER_VERSION ENV SOURCE_PATH=/sources/ diff --git a/docker/wolf.Dockerfile b/docker/wolf.Dockerfile index 9cafd1a7..4c49a9f0 100644 --- a/docker/wolf.Dockerfile +++ b/docker/wolf.Dockerfile @@ -1,4 +1,4 @@ -ARG BASE_IMAGE=ghcr.io/games-on-whales/gstreamer:1.22.7 +ARG BASE_IMAGE=ghcr.io/games-on-whales/gstreamer:1.24.5 ######################################################## FROM $BASE_IMAGE AS wolf-builder @@ -39,7 +39,7 @@ RUN <<_GST_WAYLAND_DISPLAY cd gst-wayland-display git checkout 6c7d8cb cargo install cargo-c - cargo cinstall -p c-bindings --prefix=/usr/local + cargo cinstall -p c-bindings --prefix=/usr/local --libdir=/usr/local/lib/ _GST_WAYLAND_DISPLAY COPY . /wolf/ diff --git a/docs/modules/user/pages/configuration.adoc b/docs/modules/user/pages/configuration.adoc index 65ee9c06..b75b4b9f 100644 --- a/docs/modules/user/pages/configuration.adoc +++ b/docs/modules/user/pages/configuration.adoc @@ -161,6 +161,26 @@ source = "audiotestsrc wave=ticks is-live=true" See more examples in the xref:gstreamer.adoc[] page. +=== Override the default joypad mapping + +By default, Wolf will try to match the joypad type that Moonlight sends with the correct mapping. +It is possible to override this behaviour by setting the `joypad_mapping` property in the `apps` entry; example: + +[source,toml] +.... +[[apps]] +title = "Test ball" +joypad_type = "xbox" # Force the joypad to always be xbox +.... + +The available joypad types are: + +* `auto` (default) +* `xbox` +* `nintendo` +* `ps` + + [#_app_runner] ==== App Runner diff --git a/docs/modules/user/pages/quickstart.adoc b/docs/modules/user/pages/quickstart.adoc index 74690b8b..ced443ef 100644 --- a/docs/modules/user/pages/quickstart.adoc +++ b/docs/modules/user/pages/quickstart.adoc @@ -24,8 +24,8 @@ docker run \ -v /var/run/docker.sock:/var/run/docker.sock:rw \ --device /dev/dri/ \ --device /dev/uinput \ - -v /dev/shm:/dev/shm:rw \ - -v /dev/input:/dev/input:rw \ + --device /dev/uhid \ + -v /dev/:/dev/:rw \ -v /run/udev:/run/udev:rw \ --device-cgroup-rule "c 13:* rmw" \ ghcr.io/games-on-whales/wolf:stable @@ -46,14 +46,14 @@ services: - /etc/wolf/:/etc/wolf - /tmp/sockets:/tmp/sockets:rw - /var/run/docker.sock:/var/run/docker.sock:rw - - /dev/shm:/dev/shm:rw - - /dev/input:/dev/input:rw + - /dev/:/dev/:rw - /run/udev:/run/udev:rw device_cgroup_rules: - 'c 13:* rmw' devices: - /dev/dri - /dev/uinput + - /dev/uhid network_mode: host restart: unless-stopped .... @@ -142,8 +142,8 @@ docker run \ --device /dev/nvidia0 \ --device /dev/nvidia-modeset \ --device /dev/uinput \ - -v /dev/shm:/dev/shm:rw \ - -v /dev/input:/dev/input:rw \ + --device /dev/uhid \ + -v /dev/:/dev/:rw \ -v /run/udev:/run/udev:rw \ --device-cgroup-rule "c 13:* rmw" \ ghcr.io/games-on-whales/wolf:stable @@ -165,13 +165,13 @@ services: - /etc/wolf/:/etc/wolf:rw - /tmp/sockets:/tmp/sockets:rw - /var/run/docker.sock:/var/run/docker.sock:rw - - /dev/shm:/dev/shm:rw - - /dev/input:/dev/input:rw + - /dev/:/dev/:rw - /run/udev:/run/udev:rw - nvidia-driver-vol:/usr/nvidia:rw devices: - /dev/dri - /dev/uinput + - /dev/uhid - /dev/nvidia-uvm - /dev/nvidia-uvm-tools - /dev/nvidia-caps/nvidia-cap1 @@ -296,6 +296,9 @@ sudo usermod -a -G input $USER # Allows Wolf to acces /dev/uinput KERNEL=="uinput", SUBSYSTEM=="misc", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput" +# Allows Wolf to access /dev/uhid +KERNEL=="uhid", TAG+="uaccess" + # Move virtual keyboard and mouse into a different seat SUBSYSTEMS=="input", ATTRS{id/vendor}=="ab00", MODE="0660", GROUP="input", ENV{ID_SEAT}="seat9" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ba798a8e..734d3074 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -49,11 +49,16 @@ if (UNIX AND NOT APPLE) add_subdirectory(src/platforms/linux/pulseaudio) target_link_libraries(wolf_core PUBLIC wolf::audio) - FetchContent_Declare( - inputtino - GIT_REPOSITORY https://github.com/games-on-whales/inputtino.git - GIT_TAG 753a639) - FetchContent_MakeAvailable(inputtino) + option(WOLF_CUSTOM_INPUTTINO_SRC "Use custom inputtino source" OFF) + if(WOLF_CUSTOM_INPUTTINO_SRC) + add_subdirectory(${WOLF_CUSTOM_INPUTTINO_SRC} ${CMAKE_CURRENT_BINARY_DIR}/inputtino EXCLUDE_FROM_ALL) + else() + FetchContent_Declare( + inputtino + GIT_REPOSITORY https://github.com/games-on-whales/inputtino.git + GIT_TAG f8f5a81) + FetchContent_MakeAvailable(inputtino) + endif () add_subdirectory(src/platforms/linux/uinput) target_link_libraries(wolf_core PUBLIC wolf::uinput) diff --git a/src/core/src/core/input.hpp b/src/core/src/core/input.hpp index a4ceae66..e75fac87 100644 --- a/src/core/src/core/input.hpp +++ b/src/core/src/core/input.hpp @@ -26,7 +26,7 @@ class VirtualDevice { */ class Mouse : public inputtino::Mouse, public VirtualDevice { public: - Mouse(const inputtino::Mouse &m) : inputtino::Mouse(m) {} + Mouse(inputtino::Mouse &&j) noexcept : inputtino::Mouse(std::move(j)) {} std::vector> get_udev_events() const override; std::vector>> get_udev_hw_db_entries() const override; @@ -34,7 +34,7 @@ class Mouse : public inputtino::Mouse, public VirtualDevice { class Trackpad : public inputtino::Trackpad, public VirtualDevice { public: - Trackpad(const inputtino::Trackpad &t) : inputtino::Trackpad(t) {} + Trackpad(inputtino::Trackpad &&j) noexcept : inputtino::Trackpad(std::move(j)) {} std::vector> get_udev_events() const override; std::vector>> get_udev_hw_db_entries() const override; @@ -42,7 +42,7 @@ class Trackpad : public inputtino::Trackpad, public VirtualDevice { class TouchScreen : public inputtino::TouchScreen, public VirtualDevice { public: - TouchScreen(const inputtino::TouchScreen &t) : inputtino::TouchScreen(t) {} + TouchScreen(inputtino::TouchScreen &&j) noexcept : inputtino::TouchScreen(std::move(j)) {} std::vector> get_udev_events() const override; std::vector>> get_udev_hw_db_entries() const override; @@ -50,7 +50,7 @@ class TouchScreen : public inputtino::TouchScreen, public VirtualDevice { class PenTablet : public inputtino::PenTablet, public VirtualDevice { public: - PenTablet(const inputtino::PenTablet &t) : inputtino::PenTablet(t) {} + PenTablet(inputtino::PenTablet &&j) noexcept : inputtino::PenTablet(std::move(j)) {} std::vector> get_udev_events() const override; std::vector>> get_udev_hw_db_entries() const override; @@ -58,17 +58,31 @@ class PenTablet : public inputtino::PenTablet, public VirtualDevice { class Keyboard : public inputtino::Keyboard, public VirtualDevice { public: - Keyboard(const inputtino::Keyboard &k) : inputtino::Keyboard(k) {} + Keyboard(inputtino::Keyboard &&j) noexcept : inputtino::Keyboard(std::move(j)) {} std::vector> get_udev_events() const override; std::vector>> get_udev_hw_db_entries() const override; }; -class Joypad : public inputtino::Joypad, public VirtualDevice { -private: - std::shared_ptr _j; // We have to keep a reference to the original ptr to avoid removing the thread +class XboxOneJoypad : public inputtino::XboxOneJoypad, public VirtualDevice { public: - Joypad(std::shared_ptr j) : _j(std::move(j)), inputtino::Joypad(*j) {} + XboxOneJoypad(inputtino::XboxOneJoypad &&j) noexcept : inputtino::XboxOneJoypad(std::move(j)) {} + + std::vector> get_udev_events() const override; + std::vector>> get_udev_hw_db_entries() const override; +}; + +class SwitchJoypad : public inputtino::SwitchJoypad, public VirtualDevice { +public: + SwitchJoypad(inputtino::SwitchJoypad &&j) noexcept : inputtino::SwitchJoypad(std::move(j)) {} + + std::vector> get_udev_events() const override; + std::vector>> get_udev_hw_db_entries() const override; +}; + +class PS5Joypad : public inputtino::PS5Joypad, public VirtualDevice { +public: + PS5Joypad(inputtino::PS5Joypad &&j) noexcept : inputtino::PS5Joypad(std::move(j)) {} std::vector> get_udev_events() const override; std::vector>> get_udev_hw_db_entries() const override; diff --git a/src/core/src/platforms/all/helpers/helpers/tsqueue.hpp b/src/core/src/platforms/all/helpers/helpers/tsqueue.hpp new file mode 100644 index 00000000..64bf9a08 --- /dev/null +++ b/src/core/src/platforms/all/helpers/helpers/tsqueue.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include + +/** + * Thread safe queue + */ +template class TSQueue { +private: + // Underlying queue + std::queue m_queue; + + // mutex for thread synchronisation + std::mutex m_mutex; + + // Condition variable for signalling + std::condition_variable m_cond; + +public: + TSQueue() = default; + + /** + * Pushes an element to the queue + */ + void push(T item) { + std::unique_lock lock(m_mutex); + m_queue.push(item); + m_cond.notify_all(); + } + + /** + * Pops an element off the queue + * @param timeout it'll wait up until this time for an element to be available + * @return the element if it was available, empty optional otherwise + */ + std::optional pop(std::chrono::milliseconds timeout = std::chrono::milliseconds(100)) { + std::unique_lock lock(m_mutex); + + // wait until queue is not empty or timeout + auto res = m_cond.wait_for(lock, timeout, [this]() { return !m_queue.empty(); }); + + // if timeout returns empty optional + if (!res) { + return {}; + } + + // retrieve item + auto item = std::move(m_queue.front()); + m_queue.pop(); + return item; + } +}; \ No newline at end of file diff --git a/src/core/src/platforms/linux/uinput/joypad.cpp b/src/core/src/platforms/linux/uinput/joypad.cpp index fbca3046..9936d529 100644 --- a/src/core/src/platforms/linux/uinput/joypad.cpp +++ b/src/core/src/platforms/linux/uinput/joypad.cpp @@ -1,94 +1,66 @@ #include "uinput.hpp" #include -#include -#include -#include namespace wolf::core::input { -/** - * This needs to be the same for all the virtual devices in order for SDL to match gyro with the joypad - * see: - * https://github.com/libsdl-org/SDL/blob/7cc3e94eb22f2ee76742bfb4c101757fcb70c4b7/src/joystick/linux/SDL_sysjoystick.c#L1446 - */ -static constexpr std::string_view UNIQ_ID = "00:11:22:33:44:55"; - -/** - * Joypads will also have one `/dev/input/js*` device as child, we want to expose that as well - */ -std::vector get_child_dev_nodes(libevdev_uinput *device) { - std::vector result; - auto udev = udev_new(); - if (auto device_ptr = udev_device_new_from_syspath(udev, libevdev_uinput_get_syspath(device))) { - auto enumerate = udev_enumerate_new(udev); - udev_enumerate_add_match_parent(enumerate, device_ptr); - udev_enumerate_scan_devices(enumerate); - - udev_list_entry *dev_list_entry; - auto devices = udev_enumerate_get_list_entry(enumerate); - udev_list_entry_foreach(dev_list_entry, devices) { - auto path = udev_list_entry_get_name(dev_list_entry); - auto child_dev = udev_device_new_from_syspath(udev, path); - if (auto dev_path = udev_device_get_devnode(child_dev)) { - result.push_back(dev_path); - } - udev_device_unref(child_dev); - } - - udev_enumerate_unref(enumerate); - udev_device_unref(device_ptr); - } - - udev_unref(udev); - return result; -} - -std::vector> Joypad::get_udev_events() const { +std::vector> XboxOneJoypad::get_udev_events() const { std::vector> events; - if (auto joy = _state->joy.get()) { + if (_state->joy.get()) { // eventXY and jsX devices - for (const auto &devnode : get_child_dev_nodes(joy)) { - std::string syspath = libevdev_uinput_get_syspath(joy); + for (const auto &devnode : this->get_nodes()) { + std::string syspath = libevdev_uinput_get_syspath(_state->joy.get()); syspath.erase(0, 4); // Remove leading /sys/ from syspath TODO: what if it's not /sys/? syspath.append("/" + std::filesystem::path(devnode).filename().string()); // Adds /jsX auto event = gen_udev_base_event(devnode, syspath); event["ID_INPUT_JOYSTICK"] = "1"; event[".INPUT_CLASS"] = "joystick"; - event["UNIQ"] = UNIQ_ID; + // event["UNIQ"] = UNIQ_ID; events.emplace_back(event); } } + return events; +} + +std::vector>> XboxOneJoypad::get_udev_hw_db_entries() const { + std::vector>> result; - if (auto trackpad = _state->trackpad) { - auto event = gen_udev_base_event(_state->trackpad->get_nodes()[0], ""); // TODO: syspath? - event["ID_INPUT_TOUCHPAD"] = "1"; - event[".INPUT_CLASS"] = "mouse"; - events.emplace_back(event); + if (_state->joy.get()) { + result.push_back({gen_udev_hw_db_filename(_state->joy), + {"E:ID_INPUT=1", + "E:ID_INPUT_JOYSTICK=1", + "E:ID_BUS=usb", + "G:seat", + "G:uaccess", + "Q:seat", + "Q:uaccess", + "V:1"}}); } + return result; +} + +std::vector> SwitchJoypad::get_udev_events() const { + std::vector> events; - if (auto motion_sensor = _state->motion_sensor.get()) { - for (const auto &devnode : get_child_dev_nodes(motion_sensor)) { - std::string syspath = libevdev_uinput_get_syspath(motion_sensor); + if (_state->joy.get()) { + // eventXY and jsX devices + for (const auto &devnode : this->get_nodes()) { + std::string syspath = libevdev_uinput_get_syspath(_state->joy.get()); syspath.erase(0, 4); // Remove leading /sys/ from syspath TODO: what if it's not /sys/? syspath.append("/" + std::filesystem::path(devnode).filename().string()); // Adds /jsX auto event = gen_udev_base_event(devnode, syspath); - event["ID_INPUT_ACCELEROMETER"] = "1"; - event["ID_INPUT_WIDTH_MM"] = "8"; - event["ID_INPUT_HEIGHT_MM"] = "8"; - event["UNIQ"] = UNIQ_ID; - event["IIO_SENSOR_PROXY_TYPE"] = "input-accel"; - event["SYSTEMD_WANTS"] = "iio-sensor-proxy.service"; + event["ID_INPUT_JOYSTICK"] = "1"; + event[".INPUT_CLASS"] = "joystick"; + // event["UNIQ"] = UNIQ_ID; events.emplace_back(event); } } - return events; } -std::vector>> Joypad::get_udev_hw_db_entries() const { +std::vector>> SwitchJoypad::get_udev_hw_db_entries() const { std::vector>> result; if (_state->joy.get()) { @@ -102,29 +74,131 @@ std::vector>> Joypad::get_udev_h "Q:uaccess", "V:1"}}); } + return result; +} - if (auto trackpad = _state->trackpad) { - result.push_back({gen_udev_hw_db_filename(_state->trackpad->get_nodes()[0]), - {"E:ID_INPUT=1", - "E:ID_INPUT_TOUCHPAD=1", - "E:ID_BUS=usb", - "G:seat", - "G:uaccess", - "Q:seat", - "Q:uaccess", - "V:1"}}); +std::vector> PS5Joypad::get_udev_events() const { + std::vector> events; + + auto sys_nodes = this->get_sys_nodes(); + for (const auto sys_entry : sys_nodes) { + auto input_nodes = std::filesystem::directory_iterator{sys_entry}; + + for (auto sys_node : input_nodes) { + if (sys_node.is_directory() && (sys_node.path().filename().string().rfind("event", 0) == 0 || + sys_node.path().filename().string().rfind("mouse", 0) == 0 || + sys_node.path().filename().string().rfind("js", 0) == 0)) { + auto sys_path = sys_node.path().string(); + sys_path.erase(0, 4); // Remove leading /sys/ from syspath TODO: what if it's not /sys/? + auto dev_path = ("/dev/input/" / sys_node.path().filename()).string(); + auto event = gen_udev_base_event(dev_path, sys_path); + + // Check the name of the device to determine the type + std::ifstream name_file(std::filesystem::path(sys_entry) / "name"); + std::string name; + std::getline(name_file, name); + if (name.find("Touchpad") != std::string::npos) { // touchpad + event["ID_INPUT_TOUCHPAD"] = "1"; + event[".INPUT_CLASS"] = "mouse"; + event["ID_INPUT_TOUCHPAD_INTEGRATION"] = "internal"; + } else if (name.find("Motion") != std::string::npos) { // gyro + acc + event["ID_INPUT_ACCELEROMETER"] = "1"; + event["ID_INPUT_WIDTH_MM"] = "8"; + event["ID_INPUT_HEIGHT_MM"] = "8"; + event["IIO_SENSOR_PROXY_TYPE"] = "input-accel"; + event["SYSTEMD_WANTS"] = "iio-sensor-proxy.service"; + event["UNIQ"] = this->get_mac_address(); + } else { // joypad + event["ID_INPUT_JOYSTICK"] = "1"; + event[".INPUT_CLASS"] = "joystick"; + event["UNIQ"] = this->get_mac_address(); + } + + events.emplace_back(event); + } + } } - if (_state->motion_sensor.get()) { - result.push_back({gen_udev_hw_db_filename(_state->motion_sensor), - {"E:ID_INPUT=1", - "E:ID_INPUT_ACCELEROMETER=1", - "E:ID_BUS=usb", - "G:seat", - "G:uaccess", - "Q:seat", - "Q:uaccess", - "V:1"}}); + if (!sys_nodes.empty()) { + // Add /dev/hidraw* device + // Used by Steam to access the LED status and who knows what else... + auto base_path = + std::filesystem::path(sys_nodes[0]) // /sys/devices/virtual/misc/uhid/0003:054C:0CE6.0016/input/input158 + .parent_path() // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0016/input/ + .parent_path(); // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0016/ + + if (std::filesystem::exists(base_path / "hidraw")) { + auto hidraw_entries = std::filesystem::directory_iterator{base_path / "hidraw"}; + for (auto hidraw_entry : hidraw_entries) { + auto dev_path = "/dev/" + hidraw_entry.path().filename().string(); + auto sys_path = hidraw_entry.path().string(); + sys_path.erase(0, 4); // Remove leading /sys/ from syspath TODO: what if it's not /sys/? + + auto event = gen_udev_base_event(dev_path, sys_path); + event["SUBSYSTEM"] = "hidraw"; + events.emplace_back(event); + } + } else { + logs::log(logs::warning, "Unable to find HIDRAW nodes for PS5 joypad under {}", base_path.string()); + } + } + + return events; +} + +std::vector>> PS5Joypad::get_udev_hw_db_entries() const { + std::vector>> result; + + for (const auto sys_entry : this->get_sys_nodes()) { + auto sys_nodes = std::filesystem::directory_iterator{sys_entry}; + + for (auto sys_node : sys_nodes) { + if (sys_node.is_directory() && (sys_node.path().filename().string().rfind("event", 0) == 0 || + sys_node.path().filename().string().rfind("js", 0) == 0 || + sys_node.path().filename().string().rfind("mouse", 0) == 0)) { + auto sys_path = sys_node.path().string(); + sys_path.erase(0, 4); // Remove leading /sys/ from syspath TODO: what if it's not /sys/? + auto dev_path = ("/dev/input/" / sys_node.path().filename()).string(); + + std::pair> entry; + entry.first = gen_udev_hw_db_filename(dev_path); + + // Check the name of the device to determine the type + std::ifstream name_file(std::filesystem::path(sys_entry) / "name"); + std::string name; + std::getline(name_file, name); + if (name.find("Touchpad") != std::string::npos) { // touchpad + entry.second = {"E:ID_INPUT=1", + "E:ID_INPUT_TOUCHPAD=1", + "E:ID_BUS=usb", + "G:seat", + "G:uaccess", + "Q:seat", + "Q:uaccess", + "V:1"}; + } else if (name.find("Motion") != std::string::npos) { // gyro + acc + entry.second = {"E:ID_INPUT=1", + "E:ID_INPUT_ACCELEROMETER=1", + "E:ID_BUS=usb", + "G:seat", + "G:uaccess", + "Q:seat", + "Q:uaccess", + "V:1"}; + } else { // joypad + entry.second = {"E:ID_INPUT=1", + "E:ID_INPUT_JOYSTICK=1", + "E:ID_BUS=usb", + "G:seat", + "G:uaccess", + "Q:seat", + "Q:uaccess", + "V:1"}; + } + + result.emplace_back(entry); + } + } } return result; diff --git a/src/core/src/platforms/linux/uinput/uinput.hpp b/src/core/src/platforms/linux/uinput/uinput.hpp index 063cbcf3..251e4035 100644 --- a/src/core/src/platforms/linux/uinput/uinput.hpp +++ b/src/core/src/platforms/linux/uinput/uinput.hpp @@ -18,6 +18,7 @@ * * For force feedback see: https://www.kernel.org/doc/html/latest/input/ff.html */ +#pragma once #include #include diff --git a/src/moonlight-protocol/moonlight/control.hpp b/src/moonlight-protocol/moonlight/control.hpp index 12f207b5..808becd5 100644 --- a/src/moonlight-protocol/moonlight/control.hpp +++ b/src/moonlight-protocol/moonlight/control.hpp @@ -53,7 +53,8 @@ enum CONTROLLER_TYPE : uint8_t { UNKNOWN = 0x00, XBOX = 0x01, PS = 0x02, - NINTENDO = 0x03 + NINTENDO = 0x03, + AUTO = 0xFF // not part of the protocol, I've added it for simplicity }; enum CONTROLLER_CAPABILITIES : uint8_t { @@ -244,9 +245,23 @@ struct CONTROLLER_TOUCH_PACKET : INPUT_PKT { utils::netfloat pressure; }; +enum MOTION_TYPE : uint8_t { + ACCELERATION = 0x01, + GYROSCOPE = 0x02 +}; + +enum BATTERY_STATE : unsigned short { + BATTERY_DISCHARGING = 0x0, + BATTERY_CHARGHING = 0x1, + BATTERY_FULL = 0x2, + VOLTAGE_OR_TEMPERATURE_OUT_OF_RANGE = 0xA, + TEMPERATURE_ERROR = 0xB, + CHARGHING_ERROR = 0xF +}; + struct CONTROLLER_MOTION_PACKET : INPUT_PKT { uint8_t controller_number; - wolf::core::input::Joypad::MOTION_TYPE motion_type; + MOTION_TYPE motion_type; uint8_t zero[2]; // Alignment/reserved utils::netfloat x; utils::netfloat y; @@ -255,7 +270,7 @@ struct CONTROLLER_MOTION_PACKET : INPUT_PKT { struct CONTROLLER_BATTERY_PACKET : INPUT_PKT { uint8_t controller_number; - wolf::core::input::Joypad::BATTERY_STATE battery_state; + BATTERY_STATE battery_state; uint8_t battery_percentage; uint8_t zero[1]; // Alignment/reserved }; diff --git a/src/moonlight-server/control/input_handler.cpp b/src/moonlight-server/control/input_handler.cpp index db1cd8ab..b22dd968 100644 --- a/src/moonlight-server/control/input_handler.cpp +++ b/src/moonlight-server/control/input_handler.cpp @@ -13,22 +13,16 @@ using namespace wolf::core::input; using namespace std::string_literals; using namespace moonlight::control; -std::shared_ptr create_new_joypad(const state::StreamSession &session, - const immer::atom &connected_clients, - int controller_number, - Joypad::CONTROLLER_TYPE type, - uint8_t capabilities) { - auto joypad = Joypad::create(type, capabilities); - if (!joypad) { - logs::log(logs::error, "Failed to create joypad: {}", joypad.getErrorMessage()); - return {}; - } +std::shared_ptr create_new_joypad(const state::StreamSession &session, + const immer::atom &connected_clients, + int controller_number, + CONTROLLER_TYPE type, + uint8_t capabilities) { - auto new_pad = std::make_shared(*joypad); - new_pad->set_on_rumble([clients = &connected_clients, - controller_number, - session_id = session.session_id, - aes_key = session.aes_key](int low_freq, int high_freq) { + auto on_rumble_fn = ([clients = &connected_clients, + controller_number, + session_id = session.session_id, + aes_key = session.aes_key](int low_freq, int high_freq) { auto rumble_pkt = ControlRumblePacket{ .header = {.type = RUMBLE_DATA, .length = sizeof(ControlRumblePacket) - sizeof(ControlPacket)}, .controller_number = boost::endian::native_to_little((uint16_t)controller_number), @@ -38,41 +32,123 @@ std::shared_ptr create_new_joypad(const state::StreamSession &session, encrypt_and_send(plaintext, aes_key, *clients, session_id); }); - if (capabilities & Joypad::ACCELEROMETER) { + auto on_led_fn = ([clients = &connected_clients, + controller_number, + session_id = session.session_id, + aes_key = session.aes_key](int r, int g, int b) { + auto led_pkt = ControlRGBLedPacket{ + .header{.type = RGB_LED_EVENT, .length = sizeof(ControlRGBLedPacket) - sizeof(ControlPacket)}, + .controller_number = boost::endian::native_to_little((uint16_t)controller_number), + .r = static_cast(r), + .g = static_cast(g), + .b = static_cast(b)}; + std::string plaintext = {(char *)&led_pkt, sizeof(led_pkt)}; + encrypt_and_send(plaintext, aes_key, *clients, session_id); + }); + + std::shared_ptr new_pad; + CONTROLLER_TYPE final_type = session.app->joypad_type == AUTO ? type : session.app->joypad_type; + switch (final_type) { + case UNKNOWN: + case AUTO: + case XBOX: { + logs::log(logs::info, "Creating Xbox joypad for controller {}", controller_number); + auto result = + XboxOneJoypad::create({.name = "Wolf 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}); + if (!result) { + logs::log(logs::error, "Failed to create Xbox One joypad: {}", result.getErrorMessage()); + return {}; + } else { + (*result).set_on_rumble(on_rumble_fn); + new_pad = std::make_shared(std::move(*result)); + } + break; + } + case PS: { + logs::log(logs::info, "Creating PS joypad for controller {}", controller_number); + auto result = PS5Joypad::create( + {.name = "Wolf DualSense (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111}); + if (!result) { + logs::log(logs::error, "Failed to create PS5 joypad: {}", result.getErrorMessage()); + return {}; + } else { + (*result).set_on_rumble(on_rumble_fn); + (*result).set_on_led(on_led_fn); + new_pad = std::make_shared(std::move(*result)); + + // Let's wait for the kernel to pick it up and mount the /dev/ devices + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + std::visit( + [&session](auto &pad) { + if (auto wl = *session.wayland_display->load()) { + for (const auto node : pad.get_udev_events()) { + if (node.find("ID_INPUT_TOUCHPAD") != node.end()) { + add_input_device(*wl, node.at("DEVNAME")); + } + } + } + }, + *new_pad); + } + break; + } + case NINTENDO: + logs::log(logs::info, "Creating Nintendo joypad for controller {}", controller_number); + auto result = SwitchJoypad::create({.name = "Wolf Nintendo (virtual) pad", + // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 + .vendor_id = 0x057e, + .product_id = 0x2009, + .version = 0x8111}); + if (!result) { + logs::log(logs::error, "Failed to create Switch joypad: {}", result.getErrorMessage()); + return {}; + } else { + (*result).set_on_rumble(on_rumble_fn); + new_pad = std::make_shared(std::move(*result)); + } + break; + } + + if (capabilities & ACCELEROMETER && final_type == PS) { // Request acceleromenter events from the client at 100 Hz + logs::log(logs::info, "Requesting accelerometer events for controller {}", controller_number); auto accelerometer_pkt = ControlMotionEventPacket{ .header{.type = MOTION_EVENT, .length = sizeof(ControlMotionEventPacket) - sizeof(ControlPacket)}, .controller_number = static_cast(controller_number), .reportrate = 100, - .type = Joypad::ACCELERATION}; + .type = ACCELERATION}; std::string plaintext = {(char *)&accelerometer_pkt, sizeof(accelerometer_pkt)}; encrypt_and_send(plaintext, session.aes_key, connected_clients, session.session_id); } - if (capabilities & Joypad::GYRO) { + if (capabilities & GYRO && final_type == PS) { // Request gyroscope events from the client at 100 Hz + logs::log(logs::info, "Requesting gyroscope events for controller {}", controller_number); auto gyro_pkt = ControlMotionEventPacket{ .header{.type = MOTION_EVENT, .length = sizeof(ControlMotionEventPacket) - sizeof(ControlPacket)}, .controller_number = static_cast(controller_number), .reportrate = 100, - .type = Joypad::GYROSCOPE}; + .type = GYROSCOPE}; std::string plaintext = {(char *)&gyro_pkt, sizeof(gyro_pkt)}; encrypt_and_send(plaintext, session.aes_key, connected_clients, session.session_id); } - if (auto trackpad = new_pad->get_trackpad()) { - if (auto wl = *session.wayland_display->load()) { - for (const auto node : trackpad->get_nodes()) { - add_input_device(*wl, node); - } - } - } - session.joypads->update([&](state::JoypadList joypads) { - logs::log(logs::debug, "[INPUT] Creating joypad {} of type: {}", controller_number, type); + logs::log(logs::debug, "[INPUT] Sending PlugDeviceEvent for joypad {} of type: {}", controller_number, type); - session.event_bus->fire_event(immer::box( - state::PlugDeviceEvent{.session_id = session.session_id, .device = new_pad})); + state::PlugDeviceEvent unplug_ev{.session_id = session.session_id}; + std::visit( + [&unplug_ev](auto &pad) { + unplug_ev.udev_events = pad.get_udev_events(); + unplug_ev.udev_hw_db_entries = pad.get_udev_hw_db_entries(); + }, + *new_pad); + session.event_bus->fire_event(immer::box(unplug_ev)); return joypads.set(controller_number, new_pad); }); return new_pad; @@ -89,9 +165,11 @@ std::shared_ptr create_pen_tablet(state::StreamSession &session) { logs::log(logs::error, "Failed to create pen tablet: {}", tablet.getErrorMessage()); return {}; } - auto tablet_ptr = std::make_shared(PenTablet(**tablet)); + auto tablet_ptr = std::make_shared(std::move(*tablet)); session.event_bus->fire_event(immer::box( - state::PlugDeviceEvent{.session_id = session.session_id, .device = tablet_ptr})); + state::PlugDeviceEvent{.session_id = session.session_id, + .udev_events = tablet_ptr->get_udev_events(), + .udev_hw_db_entries = tablet_ptr->get_udev_hw_db_entries()})); session.pen_tablet = tablet_ptr; if (auto wl = *session.wayland_display->load()) { for (const auto node : tablet_ptr->get_nodes()) { @@ -111,9 +189,11 @@ std::shared_ptr create_touch_screen(state::StreamSession &session) if (!touch) { logs::log(logs::error, "Failed to create touch screen: {}", touch.getErrorMessage()); } - auto touch_screen = std::make_shared(TouchScreen(**touch)); + auto touch_screen = std::make_shared(std::move(*touch)); session.event_bus->fire_event(immer::box( - state::PlugDeviceEvent{.session_id = session.session_id, .device = touch_screen})); + state::PlugDeviceEvent{.session_id = session.session_id, + .udev_events = touch_screen->get_udev_events(), + .udev_hw_db_entries = touch_screen->get_udev_hw_db_entries()})); session.touch_screen = touch_screen; if (auto wl = *session.wayland_display->load()) { for (const auto node : touch_screen->get_nodes()) { @@ -131,325 +211,402 @@ static inline float deg2rad(float degree) { return degree * (M_PI / 180.f); } +void mouse_move_rel(const MOUSE_MOVE_REL_PACKET &pkt, state::StreamSession &session) { + short delta_x = boost::endian::big_to_native(pkt.delta_x); + short delta_y = boost::endian::big_to_native(pkt.delta_y); + session.mouse->move(delta_x, delta_y); +} + +void mouse_move_abs(const MOUSE_MOVE_ABS_PACKET &pkt, state::StreamSession &session) { + float x = boost::endian::big_to_native(pkt.x); + float y = boost::endian::big_to_native(pkt.y); + float width = boost::endian::big_to_native(pkt.width); + float height = boost::endian::big_to_native(pkt.height); + session.mouse->move_abs(x, y, width, height); +} + +void mouse_button(const MOUSE_BUTTON_PACKET &pkt, state::StreamSession &session) { + Mouse::MOUSE_BUTTON btn_type; + + switch (pkt.button) { + case 1: + btn_type = Mouse::LEFT; + break; + case 2: + btn_type = Mouse::MIDDLE; + break; + case 3: + btn_type = Mouse::RIGHT; + break; + case 4: + btn_type = Mouse::SIDE; + break; + default: + btn_type = Mouse::EXTRA; + break; + } + if (pkt.type == MOUSE_BUTTON_PRESS) { + session.mouse->press(btn_type); + } else { + session.mouse->release(btn_type); + } +} + +void mouse_scroll(const MOUSE_SCROLL_PACKET &pkt, state::StreamSession &session) { + session.mouse->vertical_scroll(boost::endian::big_to_native(pkt.scroll_amt1)); +} + +void mouse_h_scroll(const MOUSE_HSCROLL_PACKET &pkt, state::StreamSession &session) { + session.mouse->horizontal_scroll(boost::endian::big_to_native(pkt.scroll_amount)); +} + +void keyboard_key(const KEYBOARD_PACKET &pkt, state::StreamSession &session) { + // moonlight always sets the high bit; not sure why but mask it off here + short moonlight_key = (short)boost::endian::little_to_native(pkt.key_code) & (short)0x7fff; + if (pkt.type == KEY_PRESS) { + session.keyboard->press(moonlight_key); + } else { + session.keyboard->release(moonlight_key); + } +} + +void utf8_text(const UTF8_TEXT_PACKET &pkt, state::StreamSession &session) { + /* Here we receive a single UTF-8 encoded char at a time, + * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+ in order to produce any + * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input + * + * ex: + * - when receiving UTF-8 [0xF0 0x9F 0x92 0xA9] (which is '💩') + * - we'll convert it to UTF-32 [0x1F4A9] + * - then type: CTRL+SHIFT+U+1F4A9 + * see the conversion at: https://www.compart.com/en/unicode/U+1F4A9 + */ + auto size = boost::endian::big_to_native(pkt.data_size) - sizeof(pkt.packet_type) - 2; + /* Reading input text as UTF-8 */ + auto utf8 = boost::locale::conv::to_utf(pkt.text, pkt.text + size, "UTF-8"); + /* Converting to UTF-32 */ + auto utf32 = boost::locale::conv::utf_to_utf(utf8); + wolf::platforms::input::paste_utf(session.keyboard, utf32); +} + +void touch(const TOUCH_PACKET &pkt, state::StreamSession &session) { + if (!session.touch_screen) { + create_touch_screen(session); + } + auto finger_id = boost::endian::little_to_native(pkt.pointer_id); + auto x = netfloat_to_0_1(pkt.x); + auto y = netfloat_to_0_1(pkt.y); + auto pressure_or_distance = netfloat_to_0_1(pkt.pressure_or_distance); + switch (pkt.event_type) { + case pkts::TOUCH_EVENT_HOVER: + case pkts::TOUCH_EVENT_DOWN: + case pkts::TOUCH_EVENT_MOVE: { + // Convert our 0..360 range to -90..90 relative to Y axis + int adjusted_angle = pkt.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; + } + session.touch_screen->place_finger(finger_id, x, y, pressure_or_distance, adjusted_angle); + break; + } + case pkts::TOUCH_EVENT_UP: + case pkts::TOUCH_EVENT_HOVER_LEAVE: + case pkts::TOUCH_EVENT_CANCEL: + session.touch_screen->release_finger(finger_id); + break; + default: + logs::log(logs::warning, "[INPUT] Unknown touch event type {}", pkt.event_type); + } +} + +void pen(const PEN_PACKET &pkt, state::StreamSession &session) { + if (!session.pen_tablet) { + create_pen_tablet(session); + } + // First set the buttons + session.pen_tablet->set_btn(PenTablet::PRIMARY, pkt.pen_buttons & PEN_BUTTON_TYPE_PRIMARY); + session.pen_tablet->set_btn(PenTablet::SECONDARY, pkt.pen_buttons & PEN_BUTTON_TYPE_SECONDARY); + session.pen_tablet->set_btn(PenTablet::TERTIARY, pkt.pen_buttons & PEN_BUTTON_TYPE_TERTIARY); + + // Set the tool + PenTablet::TOOL_TYPE tool; + switch (pkt.tool_type) { + case moonlight::control::pkts::TOOL_TYPE_PEN: + tool = PenTablet::PEN; + break; + case moonlight::control::pkts::TOOL_TYPE_ERASER: + tool = PenTablet::ERASER; + break; + default: + tool = PenTablet::SAME_AS_BEFORE; + break; + } + + auto pressure_or_distance = netfloat_to_0_1(pkt.pressure_or_distance); + + // Normalize rotation value to 0-359 degree range + auto rotation = boost::endian::little_to_native(pkt.rotation); + if (rotation != PEN_ROTATION_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 (pkt.tilt != PEN_TILT_UNKNOWN && rotation != PEN_ROTATION_UNKNOWN) { + auto rotation_rads = deg2rad(rotation); + auto tilt_rads = deg2rad(pkt.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; + } + + session.pen_tablet->place_tool(tool, + netfloat_to_0_1(pkt.x), + netfloat_to_0_1(pkt.y), + pkt.event_type == TOUCH_EVENT_DOWN ? pressure_or_distance : -1, + pkt.event_type == TOUCH_EVENT_HOVER ? pressure_or_distance : -1, + tilt_x, + tilt_y); +} + +void controller_arrival(const CONTROLLER_ARRIVAL_PACKET &pkt, + state::StreamSession &session, + const immer::atom &connected_clients) { + auto joypads = session.joypads->load(); + if (joypads->find(pkt.controller_number)) { + // TODO: should we replace it instead? + logs::log(logs::debug, + "[INPUT] Received CONTROLLER_ARRIVAL for controller {} which is already present; skipping...", + pkt.controller_number); + } else { + create_new_joypad(session, + connected_clients, + pkt.controller_number, + (CONTROLLER_TYPE)pkt.controller_type, + pkt.capabilities); + } +} + +void controller_multi(const CONTROLLER_MULTI_PACKET &pkt, + state::StreamSession &session, + const immer::atom &connected_clients) { + auto joypads = session.joypads->load(); + std::shared_ptr selected_pad; + if (auto joypad = joypads->find(pkt.controller_number)) { + selected_pad = std::move(*joypad); + + // Check if Moonlight is sending the final packet for this pad + if (!(pkt.active_gamepad_mask & (1 << pkt.controller_number))) { + logs::log(logs::debug, "Removing joypad {}", pkt.controller_number); + // Send the event downstream, Docker will pick it up and remove the device + state::UnplugDeviceEvent unplug_ev{.session_id = session.session_id}; + std::visit( + [&unplug_ev](auto &pad) { + unplug_ev.udev_events = pad.get_udev_events(); + unplug_ev.udev_hw_db_entries = pad.get_udev_hw_db_entries(); + }, + *selected_pad); + session.event_bus->fire_event(immer::box(unplug_ev)); + + // Remove the joypad, this will delete the last reference + session.joypads->update([&](state::JoypadList joypads) { return joypads.erase(pkt.controller_number); }); + } + } else { + // Old Moonlight doesn't support CONTROLLER_ARRIVAL, we create a default pad when it's first mentioned + selected_pad = create_new_joypad(session, connected_clients, pkt.controller_number, XBOX, ANALOG_TRIGGERS | RUMBLE); + } + std::visit( + [pkt](inputtino::Joypad &pad) { + std::uint16_t bf = pkt.button_flags; + std::uint32_t bf2 = pkt.buttonFlags2; + pad.set_pressed_buttons(bf | (bf2 << 16)); + pad.set_stick(inputtino::Joypad::LS, pkt.left_stick_x, pkt.left_stick_y); + pad.set_stick(inputtino::Joypad::RS, pkt.right_stick_x, pkt.right_stick_y); + pad.set_triggers(pkt.left_trigger, pkt.right_trigger); + }, + *selected_pad); +} + +void controller_touch(const CONTROLLER_TOUCH_PACKET &pkt, state::StreamSession &session) { + auto joypads = session.joypads->load(); + std::shared_ptr selected_pad; + if (auto joypad = joypads->find(pkt.controller_number)) { + selected_pad = std::move(*joypad); + auto pointer_id = boost::endian::little_to_native(pkt.pointer_id); + switch (pkt.event_type) { + case TOUCH_EVENT_DOWN: + case TOUCH_EVENT_HOVER: + case TOUCH_EVENT_MOVE: { + if (std::holds_alternative(*selected_pad)) { + std::get(*selected_pad) + .place_finger(pointer_id, + netfloat_to_0_1(pkt.x) * (uint16_t)inputtino::PS5Joypad::touchpad_width, + netfloat_to_0_1(pkt.y) * (uint16_t)inputtino::PS5Joypad::touchpad_height); + } + break; + } + case TOUCH_EVENT_UP: + case TOUCH_EVENT_HOVER_LEAVE: + case TOUCH_EVENT_CANCEL: { + if (std::holds_alternative(*selected_pad)) { + std::get(*selected_pad).release_finger(pointer_id); + } + break; + } + case TOUCH_EVENT_CANCEL_ALL: + logs::log(logs::warning, "Received TOUCH_EVENT_CANCEL_ALL which isn't supported"); + break; // TODO: remove all fingers + case TOUCH_EVENT_BUTTON_ONLY: // TODO: ??? + logs::log(logs::warning, "Received TOUCH_EVENT_BUTTON_ONLY which isn't supported"); + break; + } + } else { + logs::log(logs::warning, "Received controller touch for unknown controller {}", pkt.controller_number); + } +} + +void controller_motion(const CONTROLLER_MOTION_PACKET &pkt, state::StreamSession &session) { + auto joypads = session.joypads->load(); + std::shared_ptr selected_pad; + if (auto joypad = joypads->find(pkt.controller_number)) { + selected_pad = std::move(*joypad); + if (std::holds_alternative(*selected_pad)) { + auto x = utils::from_netfloat(pkt.x); + auto y = utils::from_netfloat(pkt.y); + auto z = utils::from_netfloat(pkt.z); + + if (pkt.motion_type == ACCELERATION) { + std::get(*selected_pad).set_motion(inputtino::PS5Joypad::ACCELERATION, x, y, z); + } else if (pkt.motion_type == GYROSCOPE) { + std::get(*selected_pad) + .set_motion(inputtino::PS5Joypad::GYROSCOPE, deg2rad(x), deg2rad(y), deg2rad(z)); + } + } + } +} + +void controller_battery(const CONTROLLER_BATTERY_PACKET &pkt, state::StreamSession &session) { + auto joypads = session.joypads->load(); + std::shared_ptr selected_pad; + if (auto joypad = joypads->find(pkt.controller_number)) { + selected_pad = std::move(*joypad); + if (std::holds_alternative(*selected_pad)) { + // Battery values in Moonlight are in the range [0, 0xFF (255)] + // Inputtino expects them as a percentage [0, 100] + std::get(*selected_pad) + .set_battery(inputtino::PS5Joypad::BATTERY_STATE(pkt.battery_state), pkt.battery_percentage / 2.55); + } + } +} + void handle_input(state::StreamSession &session, const immer::atom &connected_clients, INPUT_PKT *pkt) { switch (pkt->type) { - /* - * MOUSE - */ case MOUSE_MOVE_REL: { logs::log(logs::trace, "[INPUT] Received input of type: MOUSE_MOVE_REL"); auto move_pkt = static_cast(pkt); - short delta_x = boost::endian::big_to_native(move_pkt->delta_x); - short delta_y = boost::endian::big_to_native(move_pkt->delta_y); - session.mouse->move(delta_x, delta_y); + mouse_move_rel(*move_pkt, session); break; } case MOUSE_MOVE_ABS: { logs::log(logs::trace, "[INPUT] Received input of type: MOUSE_MOVE_ABS"); auto move_pkt = static_cast(pkt); - float x = boost::endian::big_to_native(move_pkt->x); - float y = boost::endian::big_to_native(move_pkt->y); - float width = boost::endian::big_to_native(move_pkt->width); - float height = boost::endian::big_to_native(move_pkt->height); - session.mouse->move_abs(x, y, width, height); + mouse_move_abs(*move_pkt, session); break; } case MOUSE_BUTTON_PRESS: case MOUSE_BUTTON_RELEASE: { logs::log(logs::trace, "[INPUT] Received input of type: MOUSE_BUTTON_PACKET"); auto btn_pkt = static_cast(pkt); - Mouse::MOUSE_BUTTON btn_type; - - switch (btn_pkt->button) { - case 1: - btn_type = Mouse::LEFT; - break; - case 2: - btn_type = Mouse::MIDDLE; - break; - case 3: - btn_type = Mouse::RIGHT; - break; - case 4: - btn_type = Mouse::SIDE; - break; - default: - btn_type = Mouse::EXTRA; - break; - } - if (btn_pkt->type == MOUSE_BUTTON_PRESS) { - session.mouse->press(btn_type); - } else { - session.mouse->release(btn_type); - } + mouse_button(*btn_pkt, session); break; } case MOUSE_SCROLL: { logs::log(logs::trace, "[INPUT] Received input of type: MOUSE_SCROLL_PACKET"); auto scroll_pkt = (static_cast(pkt)); - session.mouse->vertical_scroll(boost::endian::big_to_native(scroll_pkt->scroll_amt1)); + mouse_scroll(*scroll_pkt, session); break; } case MOUSE_HSCROLL: { logs::log(logs::trace, "[INPUT] Received input of type: MOUSE_HSCROLL_PACKET"); auto scroll_pkt = (static_cast(pkt)); - session.mouse->horizontal_scroll(boost::endian::big_to_native(scroll_pkt->scroll_amount)); + mouse_h_scroll(*scroll_pkt, session); break; } - /* - * KEYBOARD - */ case KEY_PRESS: case KEY_RELEASE: { logs::log(logs::trace, "[INPUT] Received input of type: KEYBOARD_PACKET"); auto key_pkt = static_cast(pkt); - // moonlight always sets the high bit; not sure why but mask it off here - short moonlight_key = (short)boost::endian::little_to_native(key_pkt->key_code) & (short)0x7fff; - if (key_pkt->type == KEY_PRESS) { - session.keyboard->press(moonlight_key); - } else { - session.keyboard->release(moonlight_key); - } + keyboard_key(*key_pkt, session); break; } case UTF8_TEXT: { logs::log(logs::trace, "[INPUT] Received input of type: UTF8_TEXT"); - /* Here we receive a single UTF-8 encoded char at a time, - * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+ in order to produce any - * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input - * - * ex: - * - when receiving UTF-8 [0xF0 0x9F 0x92 0xA9] (which is '💩') - * - we'll convert it to UTF-32 [0x1F4A9] - * - then type: CTRL+SHIFT+U+1F4A9 - * see the conversion at: https://www.compart.com/en/unicode/U+1F4A9 - */ auto txt_pkt = static_cast(pkt); - auto size = boost::endian::big_to_native(txt_pkt->data_size) - sizeof(txt_pkt->packet_type) - 2; - /* Reading input text as UTF-8 */ - auto utf8 = boost::locale::conv::to_utf(txt_pkt->text, txt_pkt->text + size, "UTF-8"); - /* Converting to UTF-32 */ - auto utf32 = boost::locale::conv::utf_to_utf(utf8); - wolf::platforms::input::paste_utf(session.keyboard, utf32); + utf8_text(*txt_pkt, session); break; } case TOUCH: { logs::log(logs::trace, "[INPUT] Received input of type: TOUCH"); - if (!session.touch_screen) { - create_touch_screen(session); - } auto touch_pkt = static_cast(pkt); - - auto finger_id = boost::endian::little_to_native(touch_pkt->pointer_id); - auto x = netfloat_to_0_1(touch_pkt->x); - auto y = netfloat_to_0_1(touch_pkt->y); - auto pressure_or_distance = netfloat_to_0_1(touch_pkt->pressure_or_distance); - switch (touch_pkt->event_type) { - case pkts::TOUCH_EVENT_HOVER: - case pkts::TOUCH_EVENT_DOWN: - case pkts::TOUCH_EVENT_MOVE: { - // Convert our 0..360 range to -90..90 relative to Y axis - int adjusted_angle = touch_pkt->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; - } - session.touch_screen->place_finger(finger_id, x, y, pressure_or_distance, adjusted_angle); - break; - } - case pkts::TOUCH_EVENT_UP: - case pkts::TOUCH_EVENT_HOVER_LEAVE: - case pkts::TOUCH_EVENT_CANCEL: - session.touch_screen->release_finger(finger_id); - break; - default: - logs::log(logs::warning, "[INPUT] Unknown touch event type {}", touch_pkt->event_type); - } + touch(*touch_pkt, session); break; } case PEN: { logs::log(logs::trace, "[INPUT] Received input of type: PEN"); - if (!session.pen_tablet) { - create_pen_tablet(session); - } auto pen_pkt = static_cast(pkt); - - // First set the buttons - session.pen_tablet->set_btn(PenTablet::PRIMARY, pen_pkt->pen_buttons & PEN_BUTTON_TYPE_PRIMARY); - session.pen_tablet->set_btn(PenTablet::SECONDARY, pen_pkt->pen_buttons & PEN_BUTTON_TYPE_SECONDARY); - session.pen_tablet->set_btn(PenTablet::TERTIARY, pen_pkt->pen_buttons & PEN_BUTTON_TYPE_TERTIARY); - - // Set the tool - PenTablet::TOOL_TYPE tool; - switch (pen_pkt->tool_type) { - case moonlight::control::pkts::TOOL_TYPE_PEN: - tool = PenTablet::PEN; - break; - case moonlight::control::pkts::TOOL_TYPE_ERASER: - tool = PenTablet::ERASER; - break; - default: - tool = PenTablet::SAME_AS_BEFORE; - break; - } - - auto pressure_or_distance = netfloat_to_0_1(pen_pkt->pressure_or_distance); - - // Normalize rotation value to 0-359 degree range - auto rotation = boost::endian::little_to_native(pen_pkt->rotation); - if (rotation != PEN_ROTATION_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_pkt->tilt != PEN_TILT_UNKNOWN && rotation != PEN_ROTATION_UNKNOWN) { - auto rotation_rads = deg2rad(rotation); - auto tilt_rads = deg2rad(pen_pkt->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; - } - - session.pen_tablet->place_tool(tool, - netfloat_to_0_1(pen_pkt->x), - netfloat_to_0_1(pen_pkt->y), - pen_pkt->event_type == TOUCH_EVENT_DOWN ? pressure_or_distance : -1, - pen_pkt->event_type == TOUCH_EVENT_HOVER ? pressure_or_distance : -1, - tilt_x, - tilt_y); - + pen(*pen_pkt, session); break; } - /* - * CONTROLLER - */ case CONTROLLER_ARRIVAL: { + logs::log(logs::trace, "[INPUT] Received input of type: CONTROLLER_ARRIVAL"); auto new_controller = static_cast(pkt); - auto joypads = session.joypads->load(); - if (joypads->find(new_controller->controller_number)) { - // TODO: should we replace it instead? - logs::log(logs::debug, - "[INPUT] Received CONTROLLER_ARRIVAL for controller {} which is already present; skipping...", - new_controller->controller_number); - } else { - create_new_joypad(session, - connected_clients, - new_controller->controller_number, - (Joypad::CONTROLLER_TYPE)new_controller->controller_type, - new_controller->capabilities); - } + controller_arrival(*new_controller, session, connected_clients); break; } case CONTROLLER_MULTI: { logs::log(logs::trace, "[INPUT] Received input of type: CONTROLLER_MULTI"); auto controller_pkt = static_cast(pkt); - auto joypads = session.joypads->load(); - std::shared_ptr selected_pad; - if (auto joypad = joypads->find(controller_pkt->controller_number)) { - selected_pad = std::move(*joypad); - - // Check if Moonlight is sending the final packet for this pad - if (!(controller_pkt->active_gamepad_mask & (1 << controller_pkt->controller_number))) { - logs::log(logs::debug, "Removing joypad {}", controller_pkt->controller_number); - // Send the event downstream, Docker will pick it up and remove the device - session.event_bus->fire_event(immer::box( - state::UnplugDeviceEvent{.session_id = session.session_id, .device = selected_pad})); - // Remove the joypad, this will delete the last reference - session.joypads->update( - [&](state::JoypadList joypads) { return joypads.erase(controller_pkt->controller_number); }); - } - } else { - // Old Moonlight versions don't support CONTROLLER_ARRIVAL, we create a default pad when it's first mentioned - selected_pad = create_new_joypad(session, - connected_clients, - controller_pkt->controller_number, - Joypad::XBOX, - Joypad::ANALOG_TRIGGERS | Joypad::RUMBLE); - } - selected_pad->set_pressed_buttons(controller_pkt->button_flags | (controller_pkt->buttonFlags2 << 16)); - selected_pad->set_stick(Joypad::LS, controller_pkt->left_stick_x, controller_pkt->left_stick_y); - selected_pad->set_stick(Joypad::RS, controller_pkt->right_stick_x, controller_pkt->right_stick_y); - selected_pad->set_triggers(controller_pkt->left_trigger, controller_pkt->right_trigger); + controller_multi(*controller_pkt, session, connected_clients); break; } case CONTROLLER_TOUCH: { logs::log(logs::trace, "[INPUT] Received input of type: CONTROLLER_TOUCH"); auto touch_pkt = static_cast(pkt); - auto joypads = session.joypads->load(); - std::shared_ptr selected_pad; - if (auto joypad = joypads->find(touch_pkt->controller_number)) { - selected_pad = std::move(*joypad); - auto pointer_id = boost::endian::little_to_native(touch_pkt->pointer_id); - switch (touch_pkt->event_type) { - case TOUCH_EVENT_DOWN: - case TOUCH_EVENT_HOVER: - case TOUCH_EVENT_MOVE: { - // TODO: Moonlight seems to always pass 1.0 (0x0000803f little endian) - // Values too high will be discarded by libinput as detecting palm pressure - if (auto trackpad = selected_pad->get_trackpad()) { - auto pressure = std::clamp(utils::from_netfloat(touch_pkt->pressure), 0.0f, 0.5f); - trackpad->place_finger(pointer_id, netfloat_to_0_1(touch_pkt->x), netfloat_to_0_1(touch_pkt->y), pressure, 0); - } - break; - } - case TOUCH_EVENT_UP: - case TOUCH_EVENT_HOVER_LEAVE: - case TOUCH_EVENT_CANCEL: { - if (auto trackpad = selected_pad->get_trackpad()) { - trackpad->release_finger(pointer_id); - } - break; - } - case TOUCH_EVENT_CANCEL_ALL: - logs::log(logs::warning, "Received TOUCH_EVENT_CANCEL_ALL which isn't supported"); - break; // TODO: remove all fingers - case TOUCH_EVENT_BUTTON_ONLY: // TODO: ??? - logs::log(logs::warning, "Received TOUCH_EVENT_BUTTON_ONLY which isn't supported"); - break; - } - } else { - logs::log(logs::warning, "Received controller touch for unknown controller {}", touch_pkt->controller_number); - } + controller_touch(*touch_pkt, session); break; } case CONTROLLER_MOTION: { logs::log(logs::trace, "[INPUT] Received input of type: CONTROLLER_MOTION"); auto motion_pkt = static_cast(pkt); - auto joypads = session.joypads->load(); - std::shared_ptr selected_pad; - if (auto joypad = joypads->find(motion_pkt->controller_number)) { - selected_pad = std::move(*joypad); - selected_pad->set_motion(motion_pkt->motion_type, - utils::from_netfloat(motion_pkt->x), - utils::from_netfloat(motion_pkt->y), - utils::from_netfloat(motion_pkt->z)); - } + controller_motion(*motion_pkt, session); break; } - case CONTROLLER_BATTERY: + case CONTROLLER_BATTERY: { logs::log(logs::trace, "[INPUT] Received input of type: CONTROLLER_BATTERY"); + auto battery_pkt = static_cast(pkt); + controller_battery(*battery_pkt, session); break; + } case HAPTICS: logs::log(logs::trace, "[INPUT] Received input of type: HAPTICS"); break; } } - } // namespace control \ No newline at end of file diff --git a/src/moonlight-server/control/input_handler.hpp b/src/moonlight-server/control/input_handler.hpp index f6f869ed..d2c42d4d 100644 --- a/src/moonlight-server/control/input_handler.hpp +++ b/src/moonlight-server/control/input_handler.hpp @@ -15,4 +15,36 @@ void handle_input(state::StreamSession &session, const immer::atom &connected_clients, INPUT_PKT *pkt); +void mouse_move_rel(const MOUSE_MOVE_REL_PACKET &pkt, state::StreamSession &session); + +void mouse_move_abs(const MOUSE_MOVE_ABS_PACKET &pkt, state::StreamSession &session); + +void mouse_button(const MOUSE_BUTTON_PACKET &pkt, state::StreamSession &session); + +void mouse_scroll(const MOUSE_SCROLL_PACKET &pkt, state::StreamSession &session); + +void mouse_h_scroll(const MOUSE_HSCROLL_PACKET &pkt, state::StreamSession &session); + +void keyboard_key(const KEYBOARD_PACKET &pkt, state::StreamSession &session); + +void utf8_text(const UTF8_TEXT_PACKET &pkt, state::StreamSession &session); + +void touch(const TOUCH_PACKET &pkt, state::StreamSession &session); + +void pen(const PEN_PACKET &pkt, state::StreamSession &session); + +void controller_arrival(const CONTROLLER_ARRIVAL_PACKET &pkt, + state::StreamSession &session, + const immer::atom &connected_clients); + +void controller_multi(const CONTROLLER_MULTI_PACKET &pkt, + state::StreamSession &session, + const immer::atom &connected_clients); + +void controller_touch(const CONTROLLER_TOUCH_PACKET &pkt, state::StreamSession &session); + +void controller_motion(const CONTROLLER_MOTION_PACKET &pkt, state::StreamSession &session); + +void controller_battery(const CONTROLLER_BATTERY_PACKET &pkt, state::StreamSession &session); + } // namespace control diff --git a/src/moonlight-server/rest/endpoints.hpp b/src/moonlight-server/rest/endpoints.hpp index 33ef7dcd..9e3f3074 100644 --- a/src/moonlight-server/rest/endpoints.hpp +++ b/src/moonlight-server/rest/endpoints.hpp @@ -306,14 +306,14 @@ void launch(const std::shared_ptr:: if (!mouse) { logs::log(logs::error, "Failed to create mouse: {}", mouse.getErrorMessage()); } else { - new_session.mouse = std::make_shared(input::Mouse(**mouse)); + new_session.mouse = std::make_shared(std::move(*mouse)); } auto keyboard = input::Keyboard::create(); if (!keyboard) { logs::log(logs::error, "Failed to create keyboard: {}", keyboard.getErrorMessage()); } else { - new_session.keyboard = std::make_shared(input::Keyboard(**keyboard)); + new_session.keyboard = std::make_shared(std::move(*keyboard)); } // joypads will be created on-demand in the Control stream new_session.joypads = std::make_shared>(); diff --git a/src/moonlight-server/runners/docker.hpp b/src/moonlight-server/runners/docker.hpp index d7e028c1..3bf2ab5d 100644 --- a/src/moonlight-server/runners/docker.hpp +++ b/src/moonlight-server/runners/docker.hpp @@ -124,8 +124,9 @@ class RunDocker : public state::Runner { docker::DockerAPI docker_api; }; -void create_udev_hw_files(std::filesystem::path base_hw_db_path, std::shared_ptr device) { - for (const auto &[filename, content] : device->get_udev_hw_db_entries()) { +void create_udev_hw_files(std::filesystem::path base_hw_db_path, + std::vector>> udev_hw_db_entries) { + for (const auto &[filename, content] : udev_hw_db_entries) { auto host_file_path = (base_hw_db_path / filename).string(); logs::log(logs::debug, "[DOCKER] Writing hwdb file: {}", host_file_path); std::ofstream host_file(host_file_path); @@ -211,14 +212,19 @@ void RunDocker::run(std::size_t session_id, auto unplug_device_handler = this->ev_bus->register_handler>( [session_id, container_id, hw_db_path, this](const immer::box &ev) { if (ev->session_id == session_id) { - for (const auto &[filename, content] : ev->device->get_udev_hw_db_entries()) { + for (const auto &[filename, content] : ev->udev_hw_db_entries) { std::filesystem::remove(hw_db_path / filename); } - for (auto udev_ev : ev->device->get_udev_events()) { + for (auto udev_ev : ev->udev_events) { udev_ev["ACTION"] = "remove"; std::string udev_msg = base64_encode(map_to_string(udev_ev)); - auto cmd = fmt::format("fake-udev -m {} && rm {}", udev_msg, udev_ev["DEVNAME"]); + std::string cmd; + if (udev_ev.count("DEVNAME") == 0) { + cmd = fmt::format("fake-udev -m {}", udev_msg); + } else { + cmd = fmt::format("fake-udev -m {} && rm {}", udev_msg, udev_ev["DEVNAME"]); + } logs::log(logs::debug, "[DOCKER] Executing command: {}", cmd); docker_api.exec(container_id, {"/bin/bash", "-c", cmd}, "root"); } @@ -227,34 +233,31 @@ void RunDocker::run(std::size_t session_id, do { // Plug all devices that are waiting in the queue - plugged_devices_queue->update([this, container_id, use_fake_udev, hw_db_path](const auto devices) { - for (const auto device : devices) { - if (use_fake_udev) { - create_udev_hw_files(hw_db_path, device); - } + while (auto device_ev = plugged_devices_queue->pop(50ms)) { + if (use_fake_udev) { + create_udev_hw_files(hw_db_path, device_ev->get().udev_hw_db_entries); + } - for (auto udev_ev : device->get_udev_events()) { - std::string cmd; - std::string udev_msg = base64_encode(map_to_string(udev_ev)); - if (udev_ev.count("DEVNAME") == 0) { - cmd = fmt::format("fake-udev -m {}", udev_msg); - } else { - cmd = fmt::format("mkdir -p /dev/input && mknod {} c {} {} && chmod 777 {} && fake-udev -m {}", - udev_ev["DEVNAME"], - udev_ev["MAJOR"], - udev_ev["MINOR"], - udev_ev["DEVNAME"], - udev_msg); - } - logs::log(logs::debug, "[DOCKER] Executing command: {}", cmd); - docker_api.exec(container_id, {"/bin/bash", "-c", cmd}, "root"); + for (auto udev_ev : device_ev->get().udev_events) { + std::string cmd; + std::string udev_msg = base64_encode(map_to_string(udev_ev)); + if (udev_ev.count("DEVNAME") == 0) { + cmd = fmt::format("fake-udev -m {}", udev_msg); + } else { + cmd = fmt::format("mkdir -p /dev/input && mknod {} c {} {} && chmod 777 {} && fake-udev -m {}", + udev_ev["DEVNAME"], + udev_ev["MAJOR"], + udev_ev["MINOR"], + udev_ev["DEVNAME"], + udev_msg); } + logs::log(logs::debug, "[DOCKER] Executing command: {}", cmd); + docker_api.exec(container_id, {"/bin/bash", "-c", cmd}, "root"); } + } + + std::this_thread::sleep_for(500ms); - // Remove all devices that we have plugged - return immer::vector>{}; - }); - boost::this_thread::sleep_for(boost::chrono::milliseconds(500)); } while (docker_api.get_by_id(container_id)->status == RUNNING); logs::log(logs::debug, "[DOCKER] Container logs: \n{}", docker_api.get_logs(container_id)); diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index 4568c44a..dca0a625 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -289,6 +289,18 @@ Config load_or_default(const std::string &source, const std::shared_ptr(item, "title"), .id = std::to_string(idx + 1), .support_hdr = toml::find_or(item, "support_hdr", false)}, @@ -302,7 +314,8 @@ Config load_or_default(const std::string &source, const std::shared_ptr(item, "start_virtual_compositor", true), - .runner = get_runner(item, ev_bus)}; + .runner = get_runner(item, ev_bus), + .joypad_type = joypad_type_enum}; }) | // ranges::to>(); // diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index cf33efaa..336853ef 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,7 +24,20 @@ namespace state { using namespace std::chrono_literals; using namespace wolf::core; namespace ba = boost::asio; -using devices_atom_queue = immer::atom>>; + +struct PlugDeviceEvent { + std::size_t session_id; + std::vector> udev_events; + std::vector>> udev_hw_db_entries; +}; + +struct UnplugDeviceEvent { + std::size_t session_id; + std::vector> udev_events; + std::vector>> udev_hw_db_entries; +}; + +using devices_atom_queue = TSQueue>; struct Runner { @@ -98,6 +112,7 @@ struct App { std::string opus_gst_pipeline; bool start_virtual_compositor; std::shared_ptr runner; + moonlight::control::pkts::CONTROLLER_TYPE joypad_type; }; /** @@ -170,7 +185,8 @@ struct PairCache { std::optional client_hash; }; -using JoypadList = immer::map>; +using JoypadTypes = std::variant; +using JoypadList = immer::map>; /** * A StreamSession is created when a Moonlight user call `launch` @@ -209,16 +225,6 @@ struct StreamSession { std::shared_ptr touch_screen = nullptr; /* Optional, will be set on first use*/ }; -struct PlugDeviceEvent { - std::size_t session_id; - std::shared_ptr device; -}; - -struct UnplugDeviceEvent { - std::size_t session_id; - std::shared_ptr device; -}; - // TODO: unplug device event? Or should this be tied to the session? using SessionsAtoms = std::shared_ptr>>; diff --git a/src/moonlight-server/state/default/config.include.toml b/src/moonlight-server/state/default/config.include.toml index dde13c22..949794fe 100644 --- a/src/moonlight-server/state/default/config.include.toml +++ b/src/moonlight-server/state/default/config.include.toml @@ -35,7 +35,7 @@ base_create_json = """ "IpcMode": "host", "Privileged": false, "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN"], - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ @@ -63,7 +63,7 @@ base_create_json = """ "IpcMode": "host", "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ @@ -94,7 +94,7 @@ base_create_json = """ "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ @@ -122,7 +122,7 @@ base_create_json = """ "IpcMode": "host", "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ diff --git a/src/moonlight-server/state/default/config.v2.toml b/src/moonlight-server/state/default/config.v2.toml index da56b9cd..22dcdd80 100644 --- a/src/moonlight-server/state/default/config.v2.toml +++ b/src/moonlight-server/state/default/config.v2.toml @@ -34,7 +34,7 @@ base_create_json = """ "IpcMode": "host", "Privileged": false, "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN"], - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ @@ -62,7 +62,7 @@ base_create_json = """ "IpcMode": "host", "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ @@ -93,7 +93,7 @@ base_create_json = """ "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ @@ -121,7 +121,7 @@ base_create_json = """ "IpcMode": "host", "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ diff --git a/src/moonlight-server/wolf.cpp b/src/moonlight-server/wolf.cpp index 8035ab62..557300e6 100644 --- a/src/moonlight-server/wolf.cpp +++ b/src/moonlight-server/wolf.cpp @@ -176,18 +176,13 @@ auto setup_sessions_handlers(const immer::box &app_state, handlers.push_back(app_state->event_bus->register_handler>( [plugged_devices_queue](const immer::box &hotplug_ev) { - plugged_devices_queue->update([=](const session_devices map) { - logs::log(logs::debug, "{} received hot-plug device event", hotplug_ev->session_id); - - if (auto session_devices_queue = map.find(hotplug_ev->session_id)) { - session_devices_queue->get()->update( - [=](const auto queue) { return queue.push_back({hotplug_ev->device}); }); - } else { - logs::log(logs::warning, "Unable to find plugged_devices_queue for session {}", hotplug_ev->session_id); - } + logs::log(logs::debug, "{} received hot-plug device event", hotplug_ev->session_id); - return map; - }); + if (auto session_devices_queue = plugged_devices_queue->load()->find(hotplug_ev->session_id)) { + session_devices_queue->get()->push(hotplug_ev); + } else { + logs::log(logs::warning, "Unable to find plugged_devices_queue for session {}", hotplug_ev->session_id); + } })); // Run process and our custom wayland as soon as a new StreamSession is created @@ -294,12 +289,28 @@ auto setup_sessions_handlers(const immer::box &app_state, /* Initialise plugged device queue with mouse and keyboard */ plugged_devices_queue->update([=](const session_devices map) { - immer::vector> devices({session->mouse, session->keyboard}); - state::devices_atom_queue devices_atom = {devices}; - return map.set(session->session_id, std::make_shared(devices)); + auto devices = immer::vector>{ + state::PlugDeviceEvent{.session_id = session->session_id, + .udev_events = session->mouse->get_udev_events(), + .udev_hw_db_entries = session->mouse->get_udev_hw_db_entries()}, + state::PlugDeviceEvent{.session_id = session->session_id, + .udev_events = session->keyboard->get_udev_events(), + .udev_hw_db_entries = session->keyboard->get_udev_hw_db_entries()}}; + /* Update (or create) the queue with the plugged mouse and keyboard */ + if (auto session_devices_queue = map.find(session->session_id)) { + for (const auto device : devices) { + session_devices_queue->get()->push(device); + } + return map; + } else { + auto devices_q = std::make_shared(); + for (const auto device : devices) { + devices_q->push(device); + } + return map.set(session->session_id, devices_q); + } }); - std::shared_ptr session_devices_queue = - *plugged_devices_queue->load()->find(session->session_id); + auto session_devices_queue = *plugged_devices_queue->load()->find(session->session_id); /* Finally run the app, this will stop here until over */ session->app->runner->run(session->session_id, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a5daf7cc..85cf7133 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,11 @@ if (UNIX AND NOT APPLE) pkg_check_modules(LIBINPUT REQUIRED IMPORTED_TARGET libinput) target_link_libraries(wolftests PRIVATE PkgConfig::LIBINPUT) + option(TEST_UHID "Enable uhid test" ON) + if (TEST_UHID) + list(APPEND SRC_LIST "platforms/linux/uhid.cpp") + endif () + if (BUILD_FAKE_UDEV_CLI) list(APPEND SRC_LIST "platforms/linux/fake-udev.cpp") target_link_libraries(wolftests PRIVATE fake-udev::lib) @@ -55,20 +60,6 @@ if (TEST_EXCEPTIONS) list(APPEND SRC_LIST testExceptions.cpp) endif () -option(TEST_SDL "Enabled SDL tests" ON) -if (TEST_SDL) - option(SDL_CUSTOM_SRC "Use a custom SDL source location (useful to better debug)" OFF) - if (SDL_CUSTOM_SRC) - SET(SDL_TEST OFF) - add_subdirectory(${SDL_CUSTOM_SRC} ${CMAKE_CURRENT_BINARY_DIR}/sdl EXCLUDE_FROM_ALL) - else () - find_package(SDL2 REQUIRED CONFIG REQUIRED COMPONENTS SDL2) - endif () - - target_link_libraries(wolftests PRIVATE SDL2::SDL2) - list(APPEND SRC_LIST "testJoypads.cpp") -endif () - target_sources(wolftests PRIVATE ${SRC_LIST}) # I'm using C++17 in the test @@ -77,7 +68,7 @@ target_compile_features(wolftests PRIVATE cxx_std_17) # Should be linked to the main library, as well as the Catch2 testing library target_link_libraries_system(wolftests PRIVATE wolf::runner - Catch2::Catch2WithMain) + Catch2::Catch2) ## Test assets configure_file(assets/config.v2.toml ${CMAKE_CURRENT_BINARY_DIR}/config.v2.toml COPYONLY) diff --git a/tests/assets/config.v2.toml b/tests/assets/config.v2.toml index 794903d4..3408419d 100644 --- a/tests/assets/config.v2.toml +++ b/tests/assets/config.v2.toml @@ -51,6 +51,7 @@ base_create_json = """ title = "Test ball" start_virtual_compositor = false render_node = "/tmp/dead_beef" +joypad_type = "xbox" [apps.runner] type = "process" diff --git a/tests/docker/testDocker.cpp b/tests/docker/testDocker.cpp index a327ca74..2c0b257b 100644 --- a/tests/docker/testDocker.cpp +++ b/tests/docker/testDocker.cpp @@ -1,4 +1,8 @@ -#include "catch2/catch_all.hpp" +#include +#include +#include +#include +#include using Catch::Matchers::Contains; using Catch::Matchers::Equals; diff --git a/tests/main.cpp b/tests/main.cpp index 9d54a500..31ab8f7e 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,10 +1,13 @@ -#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file -#include "catch2/catch_all.hpp" - -/** - * THIS FILE NEEDS TO BE LEFT EMPTY - * This allows us to compile the catch main once and then, - * when changing any test, compiling only them without rebuild it all. - * - * Greatly decreases compilation times!!! - */ \ No newline at end of file +#define CATCH_CONFIG_FAST_COMPILE + +#include +#include +#include + +int main(int argc, char *argv[]) { + logs::init(logs::parse_level(utils::get_env("WOLF_LOG_LEVEL", "TRACE"))); + + int result = Catch::Session().run(argc, argv); + + return result; +} \ No newline at end of file diff --git a/tests/platforms/linux/fake-udev.cpp b/tests/platforms/linux/fake-udev.cpp index d4ecc3f0..34a46fc8 100644 --- a/tests/platforms/linux/fake-udev.cpp +++ b/tests/platforms/linux/fake-udev.cpp @@ -1,4 +1,5 @@ -#include "catch2/catch_all.hpp" +#include +#include #include using Catch::Matchers::Equals; diff --git a/tests/platforms/linux/input.cpp b/tests/platforms/linux/input.cpp index 9cc4ea86..2841380e 100644 --- a/tests/platforms/linux/input.cpp +++ b/tests/platforms/linux/input.cpp @@ -1,10 +1,12 @@ -#include "catch2/catch_all.hpp" #include "libinput.h" #include #include +#include +#include +#include +#include #include #include -#include #include #include #include @@ -18,28 +20,19 @@ using namespace wolf::core::input; using namespace moonlight::control; using namespace std::string_literals; -void link_devnode(libevdev *dev, const std::string &device_node) { - // We have to sleep in order to be able to read from the newly created device - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - - auto fd = open(device_node.c_str(), O_RDONLY | O_NONBLOCK); - REQUIRE(fd >= 0); - libevdev_set_fd(dev, fd); -} - -TEST_CASE("uinput - keyboard", "UINPUT") { +TEST_CASE("uinput - keyboard", "[UINPUT]") { libevdev_ptr keyboard_dev(libevdev_new(), ::libevdev_free); - auto session = state::StreamSession{.keyboard = std::make_shared(Keyboard(**Keyboard::create()))}; + auto session = state::StreamSession{.keyboard = std::make_shared(std::move(*Keyboard::create()))}; link_devnode(keyboard_dev.get(), session.keyboard->get_nodes()[0]); - auto events = fetch_events(keyboard_dev); + auto events = fetch_events_debug(keyboard_dev); REQUIRE(events.empty()); auto press_shift_key = pkts::KEYBOARD_PACKET{.key_code = boost::endian::native_to_little((short)0xA0)}; press_shift_key.type = pkts::KEY_PRESS; control::handle_input(session, {}, &press_shift_key); - events = fetch_events(keyboard_dev); + events = fetch_events_debug(keyboard_dev); REQUIRE(events.size() == 1); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_KEY")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("KEY_LEFTSHIFT")); @@ -49,7 +42,7 @@ TEST_CASE("uinput - keyboard", "UINPUT") { release_shift_key.type = pkts::KEY_RELEASE; control::handle_input(session, {}, &release_shift_key); - events = fetch_events(keyboard_dev); + events = fetch_events_debug(keyboard_dev); REQUIRE(events.size() == 1); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_KEY")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("KEY_LEFTSHIFT")); @@ -57,7 +50,7 @@ TEST_CASE("uinput - keyboard", "UINPUT") { } TEST_CASE("uinput - pen tablet", "[UINPUT]") { - auto session = state::StreamSession{.pen_tablet = std::make_shared(PenTablet(**PenTablet::create()))}; + auto session = state::StreamSession{.pen_tablet = std::make_shared(std::move(*PenTablet::create()))}; auto li = create_libinput_context(session.pen_tablet->get_nodes()); auto event = get_event(li); REQUIRE(libinput_event_get_type(event.get()) == LIBINPUT_EVENT_DEVICE_ADDED); @@ -124,8 +117,7 @@ TEST_CASE("uinput - pen tablet", "[UINPUT]") { } TEST_CASE("uinput - touch screen", "[UINPUT]") { - auto session = - state::StreamSession{.touch_screen = std::make_shared(TouchScreen(**TouchScreen::create()))}; + auto session = state::StreamSession{.touch_screen = std::make_shared(std::move(*TouchScreen::create()))}; auto li = create_libinput_context(session.touch_screen->get_nodes()); auto event = get_event(li); REQUIRE(libinput_event_get_type(event.get()) == LIBINPUT_EVENT_DEVICE_ADDED); @@ -173,18 +165,18 @@ TEST_CASE("uinput - touch screen", "[UINPUT]") { } } -TEST_CASE("uinput - mouse", "UINPUT") { +TEST_CASE("uinput - mouse", "[UINPUT]") { libevdev_ptr mouse_rel_dev(libevdev_new(), ::libevdev_free); libevdev_ptr mouse_abs_dev(libevdev_new(), ::libevdev_free); - wolf::core::input::Mouse mouse = **Mouse::create(); - auto session = state::StreamSession{.mouse = std::make_shared(mouse)}; + auto mouse = std::make_shared(std::move(*Mouse::create())); + auto session = state::StreamSession{.mouse = mouse}; - link_devnode(mouse_rel_dev.get(), mouse.get_nodes()[0]); - link_devnode(mouse_abs_dev.get(), mouse.get_nodes()[1]); + link_devnode(mouse_rel_dev.get(), mouse->get_nodes()[0]); + link_devnode(mouse_abs_dev.get(), mouse->get_nodes()[1]); - auto events = fetch_events(mouse_rel_dev); + auto events = fetch_events_debug(mouse_rel_dev); REQUIRE(events.empty()); - events = fetch_events(mouse_abs_dev); + events = fetch_events_debug(mouse_abs_dev); REQUIRE(events.empty()); SECTION("Mouse move") { @@ -192,7 +184,7 @@ TEST_CASE("uinput - mouse", "UINPUT") { mv_packet.type = pkts::MOUSE_MOVE_REL; control::handle_input(session, {}, &mv_packet); - events = fetch_events(mouse_rel_dev); + events = fetch_events_debug(mouse_rel_dev); REQUIRE(events.size() == 2); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_REL")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("REL_X")); @@ -211,7 +203,7 @@ TEST_CASE("uinput - mouse", "UINPUT") { mv_packet.type = pkts::MOUSE_MOVE_ABS; control::handle_input(session, {}, &mv_packet); - events = fetch_events(mouse_abs_dev); + events = fetch_events_debug(mouse_abs_dev); REQUIRE(events.size() == 2); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_X")); @@ -225,7 +217,7 @@ TEST_CASE("uinput - mouse", "UINPUT") { pressed_packet.type = pkts::MOUSE_BUTTON_PRESS; control::handle_input(session, {}, &pressed_packet); - events = fetch_events(mouse_rel_dev); + events = fetch_events_debug(mouse_rel_dev); REQUIRE(events.size() == 2); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_MSC")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("MSC_SCAN")); @@ -242,7 +234,7 @@ TEST_CASE("uinput - mouse", "UINPUT") { scroll_packet.type = pkts::MOUSE_SCROLL; control::handle_input(session, {}, &scroll_packet); - events = fetch_events(mouse_rel_dev); + events = fetch_events_debug(mouse_rel_dev); REQUIRE(events.size() == 1); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_REL")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("REL_WHEEL_HI_RES")); @@ -255,7 +247,7 @@ TEST_CASE("uinput - mouse", "UINPUT") { scroll_packet.type = pkts::MOUSE_HSCROLL; control::handle_input(session, {}, &scroll_packet); - events = fetch_events(mouse_rel_dev); + events = fetch_events_debug(mouse_rel_dev); REQUIRE(events.size() == 1); REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_REL")); REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("REL_HWHEEL_HI_RES")); @@ -263,7 +255,7 @@ TEST_CASE("uinput - mouse", "UINPUT") { } SECTION("UDEV") { - auto udev_events = mouse.get_udev_events(); + auto udev_events = mouse->get_udev_events(); REQUIRE(udev_events.size() == 2); @@ -281,228 +273,71 @@ TEST_CASE("uinput - mouse", "UINPUT") { } } -TEST_CASE("uinput - joypad", "UINPUT") { +TEST_CASE("uinput - joypad", "[UINPUT]") { SECTION("OLD Moonlight: create joypad on first packet arrival") { + state::App app = {.joypad_type = moonlight::control::pkts::CONTROLLER_TYPE::AUTO}; auto session = state::StreamSession{.event_bus = std::make_shared(), + .app = std::make_shared(app), .joypads = std::make_shared>()}; short controller_number = 1; auto c_pkt = - pkts::CONTROLLER_MULTI_PACKET{.controller_number = controller_number, .button_flags = Joypad::RIGHT_STICK}; + pkts::CONTROLLER_MULTI_PACKET{.controller_number = controller_number, .button_flags = pkts::RIGHT_STICK}; c_pkt.type = pkts::CONTROLLER_MULTI; control::handle_input(session, {}, &c_pkt); REQUIRE(session.joypads->load()->size() == 1); - REQUIRE(session.joypads->load()->at(controller_number)->get_nodes().size() == 2); + auto joypad = session.joypads->load()->at(controller_number); + std::visit([](auto &joypad) { REQUIRE(joypad.get_nodes().size() == 2); }, *joypad); } SECTION("NEW Moonlight: create joypad with CONTROLLER_ARRIVAL") { + state::App app = {.joypad_type = moonlight::control::pkts::CONTROLLER_TYPE::AUTO}; auto session = state::StreamSession{.event_bus = std::make_shared(), + .app = std::make_shared(app), .joypads = std::make_shared>()}; uint8_t controller_number = 1; - auto c_pkt = pkts::CONTROLLER_ARRIVAL_PACKET{ - .controller_number = controller_number, - .controller_type = pkts::PS, - .capabilities = Joypad::ANALOG_TRIGGERS | Joypad::RUMBLE | Joypad::TOUCHPAD | Joypad::GYRO}; + auto c_pkt = pkts::CONTROLLER_ARRIVAL_PACKET{.controller_number = controller_number, + .controller_type = pkts::XBOX, + .capabilities = pkts::ANALOG_TRIGGERS}; c_pkt.type = pkts::CONTROLLER_ARRIVAL; control::handle_input(session, {}, &c_pkt); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); - auto dev_nodes = session.joypads->load()->at(controller_number)->get_nodes(); + auto joypad = session.joypads->load()->at(controller_number); + std::vector dev_nodes; + std::visit([&dev_nodes](auto &joypad) { dev_nodes = joypad.get_nodes(); }, *joypad); REQUIRE(session.joypads->load()->size() == 1); - REQUIRE(dev_nodes.size() == 5); - - libevdev_ptr touch_rel_dev(libevdev_new(), ::libevdev_free); - // We know that the 3rd device is the touchpad - link_devnode(touch_rel_dev.get(), dev_nodes[2]); - - SECTION("Joypad touchpad") { - { // Touch finger one - auto touch_packet = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, - .event_type = moonlight::control::pkts::TOUCH_EVENT_DOWN, - .pointer_id = 0, - .x = {255, 255, 255, 0}, - .y = {0, 255, 255, 255}}; - touch_packet.type = pkts::CONTROLLER_TOUCH; - - control::handle_input(session, {}, &touch_packet); - auto events = fetch_events(touch_rel_dev); - REQUIRE(events.size() == 4); // TODO: why there are no ABS_X and ABS_Y? - - REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); - REQUIRE(events[0]->value == 1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); - REQUIRE(events[1]->value == 1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); - REQUIRE(events[2]->value == 1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOUCH")); - REQUIRE(events[3]->value == 1); - } + REQUIRE(dev_nodes.size() >= 2); - { // Touch finger 2 - auto touch_2_pkt = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, - .event_type = moonlight::control::pkts::TOUCH_EVENT_DOWN, - .pointer_id = boost::endian::native_to_little(1), - .x = {255, 255, 255, 0}, - .y = {0, 255, 255, 255}}; - touch_2_pkt.type = pkts::CONTROLLER_TOUCH; - - control::handle_input(session, {}, &touch_2_pkt); - auto events = fetch_events(touch_rel_dev); - REQUIRE(events.size() == 4); // TODO: why there are no ABS_X and ABS_Y? - - REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); - REQUIRE(events[0]->value == 2); - - REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); - REQUIRE(events[1]->value == 2); - - REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); - REQUIRE(events[2]->value == 0); - - REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOOL_DOUBLETAP")); - REQUIRE(events[3]->value == 1); - } + // TODO: test pressing buttons - { // Remove finger one - auto touch_2_pkt = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, - .event_type = moonlight::control::pkts::TOUCH_EVENT_UP, - .pointer_id = 0, - .x = {0}, - .y = {0}}; - touch_2_pkt.type = pkts::CONTROLLER_TOUCH; - - control::handle_input(session, {}, &touch_2_pkt); - auto events = fetch_events(touch_rel_dev); - REQUIRE(events.size() == 4); // TODO: why there are no ABS_X and ABS_Y? - - REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); - REQUIRE(events[0]->value == 1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); - REQUIRE(events[1]->value == -1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); - REQUIRE(events[2]->value == 1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOOL_DOUBLETAP")); - REQUIRE(events[3]->value == 0); - } + { // UDEV + std::vector> udev_events; + std::visit([&udev_events](auto &joypad) { udev_events = joypad.get_udev_events(); }, *joypad); - { // Remove finger two, no fingers left on the touchpad - auto touch_2_pkt = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, - .event_type = moonlight::control::pkts::TOUCH_EVENT_UP, - .pointer_id = boost::endian::native_to_little(1), - .x = {0}, - .y = {0}}; - touch_2_pkt.type = pkts::CONTROLLER_TOUCH; - - control::handle_input(session, {}, &touch_2_pkt); - auto events = fetch_events(touch_rel_dev); - REQUIRE(events.size() == 4); // TODO: why there are no ABS_X and ABS_Y? - - REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); - REQUIRE(events[0]->value == 2); - - REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); - REQUIRE(events[1]->value == -1); - - REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); - REQUIRE(events[2]->value == 0); - - REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); - REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOUCH")); - REQUIRE(events[3]->value == 0); + for (auto event : udev_events) { + std::stringstream ss; + for (auto [key, value] : event) { + ss << key << "=" << value << ", "; + } + logs::log(logs::debug, "UDEV: {}", ss.str()); } - } - libevdev_ptr motion_dev(libevdev_new(), ::libevdev_free); - // We know that the last node is the motion sensor - link_devnode(motion_dev.get(), dev_nodes[3]); - SECTION("Motion sensor") { - auto motion_pkt = pkts::CONTROLLER_MOTION_PACKET{.controller_number = controller_number, - .motion_type = Joypad::ACCELERATION, - .x = {255, 255, 255, 0}, - .y = {0, 255, 255, 255}, - .z = {0, 0, 0, 0}}; - motion_pkt.type = pkts::CONTROLLER_MOTION; - - control::handle_input(session, {}, &motion_pkt); - auto events = fetch_events(motion_dev); - REQUIRE(events.size() == 4); - - REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_X")); - REQUIRE(events[0]->value == 0); - - REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_Y")); - REQUIRE(events[1]->value == -32768); // DS_ACC_RANGE - - REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("ABS_Z")); - REQUIRE(events[2]->value == 0); - - REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_MSC")); - REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("MSC_TIMESTAMP")); - } + REQUIRE(udev_events.size() == 2); - SECTION("UDEV") { - auto udev_events = session.joypads->load()->at(controller_number)->get_udev_events(); - - REQUIRE(udev_events.size() == 5); - - REQUIRE_THAT(udev_events[0]["ACTION"], Equals("add")); - REQUIRE_THAT(udev_events[0]["ID_INPUT_JOYSTICK"], Equals("1")); - REQUIRE_THAT(udev_events[0][".INPUT_CLASS"], Equals("joystick")); - REQUIRE_THAT(udev_events[0]["DEVNAME"], ContainsSubstring("/dev/input/")); - REQUIRE_THAT(udev_events[0]["DEVPATH"], StartsWith("/devices/virtual/input/input")); - - REQUIRE_THAT(udev_events[1]["ACTION"], Equals("add")); - REQUIRE_THAT(udev_events[1]["ID_INPUT_JOYSTICK"], Equals("1")); - REQUIRE_THAT(udev_events[1][".INPUT_CLASS"], Equals("joystick")); - REQUIRE_THAT(udev_events[1]["DEVNAME"], ContainsSubstring("/dev/input/")); - REQUIRE_THAT(udev_events[1]["DEVPATH"], StartsWith("/devices/virtual/input/input")); - - REQUIRE_THAT(udev_events[2]["ACTION"], Equals("add")); - REQUIRE_THAT(udev_events[2]["ID_INPUT_TOUCHPAD"], Equals("1")); - REQUIRE_THAT(udev_events[2][".INPUT_CLASS"], Equals("mouse")); - REQUIRE_THAT(udev_events[2]["DEVNAME"], ContainsSubstring("/dev/input/")); - // TODO: missing trackpad devpath - // REQUIRE_THAT(udev_events[2]["DEVPATH"], StartsWith("/devices/virtual/input/input")); - - REQUIRE_THAT(udev_events[3]["ACTION"], Equals("add")); - REQUIRE_THAT(udev_events[3]["ID_INPUT_ACCELEROMETER"], Equals("1")); - REQUIRE_THAT(udev_events[3]["DEVNAME"], ContainsSubstring("/dev/input/")); - REQUIRE_THAT(udev_events[3]["DEVPATH"], StartsWith("/devices/virtual/input/input")); - - REQUIRE_THAT(udev_events[4]["ACTION"], Equals("add")); - REQUIRE_THAT(udev_events[4]["ID_INPUT_ACCELEROMETER"], Equals("1")); - REQUIRE_THAT(udev_events[4]["DEVNAME"], ContainsSubstring("/dev/input/")); - REQUIRE_THAT(udev_events[4]["DEVPATH"], StartsWith("/devices/virtual/input/input")); + for (auto &event : udev_events) { + REQUIRE_THAT(event["ACTION"], Equals("add")); + REQUIRE_THAT(event["DEVNAME"], ContainsSubstring("/dev/input/")); + REQUIRE_THAT(event["DEVPATH"], StartsWith("/devices/virtual/input/input")); + REQUIRE_THAT(event[".INPUT_CLASS"], StartsWith("joystick")); + } } } } -TEST_CASE("uinput - paste UTF8", "UINPUT") { +TEST_CASE("uinput - paste UTF8", "[UINPUT]") { SECTION("UTF8 to HEX") { auto utf8 = boost::locale::conv::to_utf("\xF0\x9F\x92\xA9", "UTF-8"); // UTF-8 '💩' @@ -519,10 +354,10 @@ TEST_CASE("uinput - paste UTF8", "UINPUT") { SECTION("Paste UTF8") { libevdev_ptr keyboard_dev(libevdev_new(), ::libevdev_free); - auto session = state::StreamSession{.keyboard = std::make_shared(**Keyboard::create())}; + auto session = state::StreamSession{.keyboard = std::make_shared(std::move(*Keyboard::create()))}; link_devnode(keyboard_dev.get(), session.keyboard->get_nodes()[0]); - auto events = fetch_events(keyboard_dev); + auto events = fetch_events_debug(keyboard_dev); REQUIRE(events.empty()); auto utf8_pkt = pkts::UTF8_TEXT_PACKET{.text = "\xF0\x9F\x92\xA9"}; @@ -530,7 +365,7 @@ TEST_CASE("uinput - paste UTF8", "UINPUT") { utf8_pkt.data_size = boost::endian::native_to_big(8); control::handle_input(session, {}, &utf8_pkt); - events = fetch_events(keyboard_dev); + events = fetch_events_debug(keyboard_dev); REQUIRE(events.size() == 16); /** diff --git a/tests/platforms/linux/libinput.h b/tests/platforms/linux/libinput.h index 9d0d0e4a..9060218d 100644 --- a/tests/platforms/linux/libinput.h +++ b/tests/platforms/linux/libinput.h @@ -1,12 +1,14 @@ #pragma once +#include +#include +#include #include #include +#include #include -#include #include -#include -#include +#include static int open_restricted(const char *path, int flags, void *user_data) { int fd = open(path, flags); @@ -48,4 +50,26 @@ static std::shared_ptr get_event(std::shared_ptr li) { libinput_dispatch(li.get()); struct libinput_event *event = libinput_get_event(li.get()); return std::shared_ptr(event, [](libinput_event *event) { libinput_event_destroy(event); }); +} + +static void link_devnode(libevdev *dev, const std::string &device_node) { + // We have to sleep in order to be able to read from the newly created device + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + auto fd = open(device_node.c_str(), O_RDONLY | O_NONBLOCK); + assert(fd >= 0 && "Unable to open device node"); + libevdev_set_fd(dev, fd); +} + +static std::vector fetch_events_debug(const wolf::core::input::libevdev_ptr &dev, + int max_events = 50) { + auto events = wolf::core::input::fetch_events(dev, max_events); + for (auto event : events) { + logs::log(logs::debug, + "Event: type={}, code={}, value={}", + libevdev_event_type_get_name(event->type), + libevdev_event_code_get_name(event->type, event->code), + event->value); + } + return events; } \ No newline at end of file diff --git a/tests/platforms/linux/nvidia.cpp b/tests/platforms/linux/nvidia.cpp index 0a87c841..6c1537de 100644 --- a/tests/platforms/linux/nvidia.cpp +++ b/tests/platforms/linux/nvidia.cpp @@ -1,4 +1,8 @@ -#include "catch2/catch_all.hpp" +#include +#include +#include +#include +#include #include using Catch::Matchers::Contains; diff --git a/tests/platforms/linux/uhid.cpp b/tests/platforms/linux/uhid.cpp new file mode 100644 index 00000000..458e8835 --- /dev/null +++ b/tests/platforms/linux/uhid.cpp @@ -0,0 +1,221 @@ +#include "libinput.h" +#include +#include +#include +#include +#include + +using Catch::Matchers::ContainsSubstring; +using Catch::Matchers::Equals; +using Catch::Matchers::StartsWith; + +using namespace wolf::core::input; +using namespace moonlight::control; +using namespace std::string_literals; + +TEST_CASE("Create PS5 pad with CONTROLLER_ARRIVAL", "[UHID]") { + state::App app = {.joypad_type = moonlight::control::pkts::CONTROLLER_TYPE::AUTO}; + auto session = state::StreamSession{ + .event_bus = std::make_shared(), + .app = std::make_shared(app), + .joypads = std::make_shared>()}; + uint8_t controller_number = 1; + auto c_pkt = pkts::CONTROLLER_ARRIVAL_PACKET{ + .controller_number = controller_number, + .controller_type = pkts::PS, + .capabilities = pkts::ANALOG_TRIGGERS | pkts::RUMBLE | pkts::TOUCHPAD | pkts::GYRO}; + c_pkt.type = pkts::CONTROLLER_ARRIVAL; + + control::handle_input(session, {}, &c_pkt); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + auto joypad = session.joypads->load()->at(controller_number); + std::vector dev_nodes; + std::visit([&dev_nodes](auto &joypad) { dev_nodes = joypad.get_nodes(); }, *joypad); + REQUIRE(session.joypads->load()->size() == 1); + REQUIRE(dev_nodes.size() >= 4); + + // Search dev_nodes /dev/input/eventXX device and turn them into libevdev devices + std::sort(dev_nodes.begin(), dev_nodes.end()); // ranges::actions::sort doesn't work for some reason + auto devices = + dev_nodes | // + ranges::views::filter([](const std::string &node) { return node.find("event") != std::string::npos; }) | // + ranges::views::transform([](const std::string &node) { + libevdev_ptr el(libevdev_new(), ::libevdev_free); + link_devnode(el.get(), node); + return el; + }) | + ranges::to_vector; + + // We know the 3rd device is the touchpad + auto touch_rel_dev = devices[2]; + { // "Joypad touchpad" + { // Touch finger one + auto touch_packet = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, + .event_type = moonlight::control::pkts::TOUCH_EVENT_DOWN, + .pointer_id = 0, + .x = {255, 255, 255, 0}, + .y = {0, 255, 255, 255}}; + touch_packet.type = pkts::CONTROLLER_TOUCH; + + control::handle_input(session, {}, &touch_packet); + auto events = fetch_events_debug(touch_rel_dev); + REQUIRE(events.size() == 3); + + REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_TRACKING_ID")); + REQUIRE(events[0]->value == 0); + + REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("BTN_TOUCH")); + REQUIRE(events[1]->value == 1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); + REQUIRE(events[2]->value == 1); + } + + { // Touch finger 2 + auto touch_2_pkt = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, + .event_type = moonlight::control::pkts::TOUCH_EVENT_DOWN, + .pointer_id = boost::endian::native_to_little(1), + .x = {255, 255, 255, 0}, + .y = {0, 255, 255, 255}}; + touch_2_pkt.type = pkts::CONTROLLER_TOUCH; + + control::handle_input(session, {}, &touch_2_pkt); + auto events = fetch_events_debug(touch_rel_dev); + REQUIRE(events.size() == 4); + + REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); + REQUIRE(events[0]->value == 1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); + REQUIRE(events[1]->value == 1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); + REQUIRE(events[2]->value == 0); + + REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOOL_DOUBLETAP")); + REQUIRE(events[3]->value == 1); + } + + { // Remove finger one + auto touch_2_pkt = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, + .event_type = moonlight::control::pkts::TOUCH_EVENT_UP, + .pointer_id = 0, + .x = {0}, + .y = {0}}; + touch_2_pkt.type = pkts::CONTROLLER_TOUCH; + + control::handle_input(session, {}, &touch_2_pkt); + auto events = fetch_events_debug(touch_rel_dev); + REQUIRE(events.size() == 4); + + REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); + REQUIRE(events[0]->value == 0); + + REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); + REQUIRE(events[1]->value == -1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOOL_FINGER")); + REQUIRE(events[2]->value == 1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOOL_DOUBLETAP")); + REQUIRE(events[3]->value == 0); + } + + { // Remove finger two, no fingers left on the touchpad + auto touch_2_pkt = pkts::CONTROLLER_TOUCH_PACKET{.controller_number = controller_number, + .event_type = moonlight::control::pkts::TOUCH_EVENT_UP, + .pointer_id = boost::endian::native_to_little(1), + .x = {0}, + .y = {0}}; + touch_2_pkt.type = pkts::CONTROLLER_TOUCH; + + control::handle_input(session, {}, &touch_2_pkt); + auto events = fetch_events_debug(touch_rel_dev); + REQUIRE(events.size() == 4); // TODO: why there are no ABS_X and ABS_Y? + + REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_MT_SLOT")); + REQUIRE(events[0]->value == 1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); + REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_MT_TRACKING_ID")); + REQUIRE(events[1]->value == -1); + + REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("BTN_TOUCH")); + REQUIRE(events[2]->value == 0); + + REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_KEY")); + REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("BTN_TOOL_FINGER")); + REQUIRE(events[3]->value == 0); + } + } + + // We know the 2nd device is the motion sensor + auto motion_dev = devices[1]; + { // Motion sensor + auto motion_pkt = pkts::CONTROLLER_MOTION_PACKET{.controller_number = controller_number, + .motion_type = pkts::ACCELERATION, + .x = {255, 255, 255, 0}, + .y = {0, 255, 255, 255}, + .z = {0, 0, 0, 0}}; + motion_pkt.type = pkts::CONTROLLER_MOTION; + + control::handle_input(session, {}, &motion_pkt); + auto events = fetch_events_debug(motion_dev); + REQUIRE(events.size() == 5); + // TODO: seems that I only get MSC_TIMESTAMP here + // + // REQUIRE_THAT(libevdev_event_type_get_name(events[0]->type), Equals("EV_ABS")); + // REQUIRE_THAT(libevdev_event_code_get_name(events[0]->type, events[0]->code), Equals("ABS_X")); + // REQUIRE(events[0]->value == 0); + // + // REQUIRE_THAT(libevdev_event_type_get_name(events[1]->type), Equals("EV_ABS")); + // REQUIRE_THAT(libevdev_event_code_get_name(events[1]->type, events[1]->code), Equals("ABS_Y")); + // REQUIRE(events[1]->value == -32768); // DS_ACC_RANGE + // + // REQUIRE_THAT(libevdev_event_type_get_name(events[2]->type), Equals("EV_ABS")); + // REQUIRE_THAT(libevdev_event_code_get_name(events[2]->type, events[2]->code), Equals("ABS_Z")); + // REQUIRE(events[2]->value == 0); + // + // REQUIRE_THAT(libevdev_event_type_get_name(events[3]->type), Equals("EV_MSC")); + // REQUIRE_THAT(libevdev_event_code_get_name(events[3]->type, events[3]->code), Equals("MSC_TIMESTAMP")); + } + + { // UDEV + std::vector> udev_events; + std::visit([&udev_events](auto &joypad) { udev_events = joypad.get_udev_events(); }, *joypad); + + for (auto event : udev_events) { + std::stringstream ss; + for (auto [key, value] : event) { + ss << key << "=" << value << ", "; + } + logs::log(logs::debug, "UDEV: {}", ss.str()); + } + + REQUIRE(udev_events.size() == 7); + + for (auto &event : udev_events) { + REQUIRE_THAT(event["ACTION"], Equals("add")); + REQUIRE_THAT(event["DEVPATH"], StartsWith("/devices/virtual/misc/uhid/0003:054C")); + if (event["SUBSYSTEM"] == "input") { + REQUIRE_THAT(event["DEVNAME"], ContainsSubstring("/dev/input/")); + } else if (event["SUBSYSTEM"] == "hidraw") { + REQUIRE_THAT(event["DEVNAME"], ContainsSubstring("/dev/hidraw")); + } + } + } +} \ No newline at end of file diff --git a/tests/platforms/linux/wayland-display.cpp b/tests/platforms/linux/wayland-display.cpp index 2da6c31e..8cc5dd7e 100644 --- a/tests/platforms/linux/wayland-display.cpp +++ b/tests/platforms/linux/wayland-display.cpp @@ -1,4 +1,8 @@ -#include "catch2/catch_all.hpp" +#include +#include +#include +#include +#include #include #include diff --git a/tests/testControl.cpp b/tests/testControl.cpp index fe73a236..a1c1a114 100644 --- a/tests/testControl.cpp +++ b/tests/testControl.cpp @@ -1,4 +1,6 @@ -#include "catch2/catch_all.hpp" +#include +#include + using Catch::Matchers::Equals; #include @@ -85,5 +87,5 @@ TEST_CASE("control joypad input packets") { REQUIRE(input_data->type == pkts::CONTROLLER_MULTI); REQUIRE(input_data->active_gamepad_mask == 1); - REQUIRE(pressed_btns & wolf::core::input::Joypad::CONTROLLER_BTN::A); + REQUIRE(pressed_btns & pkts::CONTROLLER_BTN::A); } \ No newline at end of file diff --git a/tests/testCrypto.cpp b/tests/testCrypto.cpp index 150f9111..def0ec15 100644 --- a/tests/testCrypto.cpp +++ b/tests/testCrypto.cpp @@ -1,4 +1,6 @@ -#include "catch2/catch_all.hpp" +#include +#include + using Catch::Matchers::Equals; #include diff --git a/tests/testExceptions.cpp b/tests/testExceptions.cpp index e523efce..a7cbc37d 100644 --- a/tests/testExceptions.cpp +++ b/tests/testExceptions.cpp @@ -1,4 +1,7 @@ -#include "catch2/catch_all.hpp" +#include +#include +#include + using Catch::Matchers::Contains; using Catch::Matchers::Equals; diff --git a/tests/testGSTPlugin.cpp b/tests/testGSTPlugin.cpp index d3992718..ef902c54 100644 --- a/tests/testGSTPlugin.cpp +++ b/tests/testGSTPlugin.cpp @@ -1,4 +1,9 @@ -#include "catch2/catch_all.hpp" +#include +#include +#include +#include +#include + using Catch::Matchers::Equals; #include diff --git a/tests/testJoypads.cpp b/tests/testJoypads.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/testMoonlight.cpp b/tests/testMoonlight.cpp index 50c7b91a..4dfc3a2b 100644 --- a/tests/testMoonlight.cpp +++ b/tests/testMoonlight.cpp @@ -1,4 +1,9 @@ -#include "catch2/catch_all.hpp" +#include +#include +#include +#include +#include + using Catch::Matchers::Equals; #include @@ -28,6 +33,7 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { REQUIRE_THAT(first_app.base.id, Equals("1")); REQUIRE_THAT(first_app.h264_gst_pipeline, Equals("video_source ! params ! h264_pipeline ! video_sink")); REQUIRE_THAT(first_app.hevc_gst_pipeline, Equals("video_source ! params ! hevc_pipeline ! video_sink")); + REQUIRE(first_app.joypad_type == moonlight::control::pkts::CONTROLLER_TYPE::AUTO); REQUIRE(first_app.start_virtual_compositor); REQUIRE(first_app.hevc_encoder == state::UNKNOWN); REQUIRE(first_app.h264_encoder == state::UNKNOWN); @@ -40,6 +46,7 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { REQUIRE_THAT(second_app.h264_gst_pipeline, Equals("override DEFAULT SOURCE ! params ! h264_pipeline ! video_sink")); REQUIRE_THAT(second_app.hevc_gst_pipeline, Equals("override DEFAULT SOURCE ! params ! hevc_pipeline ! video_sink")); REQUIRE(!second_app.start_virtual_compositor); + REQUIRE(second_app.joypad_type == moonlight::control::pkts::CONTROLLER_TYPE::XBOX); REQUIRE(second_app.hevc_encoder == state::UNKNOWN); REQUIRE(second_app.h264_encoder == state::UNKNOWN); REQUIRE(second_app.render_node == "/tmp/dead_beef"); diff --git a/tests/testRTSP.cpp b/tests/testRTSP.cpp index d419619e..1f6202df 100644 --- a/tests/testRTSP.cpp +++ b/tests/testRTSP.cpp @@ -1,4 +1,6 @@ -#include "catch2/catch_all.hpp" +#include +#include + using Catch::Matchers::Equals; #include