From e51bd44326a896b8eaab7b924c91642cbbffa5aa Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Wed, 17 Jul 2024 18:32:10 +0100 Subject: [PATCH 1/9] feat: direct mouse and keyboard events without udev fixes #73 fixes https://github.com/games-on-whales/gst-wayland-display/issues/4 --- docker/wolf.Dockerfile | 2 +- src/core/CMakeLists.txt | 2 +- src/core/src/core/virtual-display.hpp | 33 ++++ .../linux/virtual-display/wayland-display.cpp | 112 ++++++++++++++ .../control/input_handler.cpp | 145 ++++++++++++------ src/moonlight-server/platforms/input.hpp | 2 +- .../platforms/input_linux.cpp | 18 +-- src/moonlight-server/rest/endpoints.hpp | 18 +-- .../state/data-structures.hpp | 8 +- src/moonlight-server/wolf.cpp | 34 +++- tests/platforms/linux/input.cpp | 18 ++- 11 files changed, 300 insertions(+), 92 deletions(-) diff --git a/docker/wolf.Dockerfile b/docker/wolf.Dockerfile index 1c3d4e34..92e856f0 100644 --- a/docker/wolf.Dockerfile +++ b/docker/wolf.Dockerfile @@ -37,7 +37,7 @@ RUN <<_GST_WAYLAND_DISPLAY git clone https://github.com/games-on-whales/gst-wayland-display cd gst-wayland-display - git checkout 49529dc + git checkout 48382dc cargo install cargo-c cargo cinstall -p c-bindings --prefix=/usr/local --libdir=/usr/local/lib/ _GST_WAYLAND_DISPLAY diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 734d3074..3dfa647b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -16,7 +16,7 @@ set(CORE_SRC "") find_package(PkgConfig) option(LINK_RUST_WAYLAND "Link to our custom Rust wayland compositor" ON) if (LINK_RUST_WAYLAND AND UNIX AND NOT APPLE) - pkg_check_modules(libgstwaylanddisplay REQUIRED IMPORTED_TARGET libgstwaylanddisplay) + pkg_check_modules(libgstwaylanddisplay REQUIRED IMPORTED_TARGET libgstwaylanddisplay>=0.3.0) target_link_libraries(wolf_core PUBLIC PkgConfig::libgstwaylanddisplay) endif () diff --git a/src/core/src/core/virtual-display.hpp b/src/core/src/core/virtual-display.hpp index e9849151..466203a0 100644 --- a/src/core/src/core/virtual-display.hpp +++ b/src/core/src/core/virtual-display.hpp @@ -31,4 +31,37 @@ immer::vector get_env(const WaylandState &w_state); static void destroy(WaylandState *w_state); GstBuffer *get_frame(WaylandState &w_state); bool add_input_device(WaylandState &w_state, const std::string &device_path); + +class WaylandMouse { +public: + WaylandMouse(wl_state_ptr w_state) : w_state(w_state){}; + + void move(int delta_x, int delta_y); + + void move_abs(int x, int y, int screen_width, int screen_height); + + void press(unsigned int button); + + void release(unsigned int button); + + void vertical_scroll(int high_res_distance); + + void horizontal_scroll(int high_res_distance); + +private: + wl_state_ptr w_state; +}; + +class WaylandKeyboard { +public: + WaylandKeyboard(wl_state_ptr w_state) : w_state(w_state){}; + + void press(unsigned int key_code); + + void release(unsigned int key_code); + +private: + wl_state_ptr w_state; +}; + } // namespace wolf::core::virtual_display \ No newline at end of file diff --git a/src/core/src/platforms/linux/virtual-display/wayland-display.cpp b/src/core/src/platforms/linux/virtual-display/wayland-display.cpp index 1fd4ec95..a67899cb 100644 --- a/src/core/src/platforms/linux/virtual-display/wayland-display.cpp +++ b/src/core/src/platforms/linux/virtual-display/wayland-display.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" { #include @@ -96,4 +97,115 @@ bool add_input_device(WaylandState &w_state, const std::string &device_path) { return true; } +void WaylandMouse::move(int delta_x, int delta_y) { + display_pointer_motion(w_state->display, delta_x, delta_y); +} + +void WaylandMouse::move_abs(int x, int y, int screen_width, int screen_height) { + display_pointer_motion_absolute(w_state->display, x, y); +} + +unsigned int moonlight_button_to_linux(unsigned int button) { + switch (button) { + case 1: + return BTN_LEFT; + case 2: + return BTN_MIDDLE; + case 3: + return BTN_RIGHT; + case 4: + return BTN_SIDE; + default: + return BTN_EXTRA; + } +} + +void WaylandMouse::press(unsigned int button) { + display_pointer_button(w_state->display, moonlight_button_to_linux(button), true); +} + +void WaylandMouse::release(unsigned int button) { + display_pointer_button(w_state->display, moonlight_button_to_linux(button), false); +} + +void WaylandMouse::vertical_scroll(int high_res_distance) { + display_pointer_axis(w_state->display, 0, -high_res_distance); +} + +void WaylandMouse::horizontal_scroll(int high_res_distance) { + display_pointer_axis(w_state->display, high_res_distance, 0); +} + +/** + * A map of Moonlight key codes to Linux key codes + */ +static const std::map key_mappings = { + {0x08, KEY_BACKSPACE}, {0x09, KEY_TAB}, + {0x0C, KEY_CLEAR}, {0x0D, KEY_ENTER}, + {0x10, KEY_LEFTSHIFT}, {0x11, KEY_LEFTCTRL}, + {0x12, KEY_LEFTALT}, {0x13, KEY_PAUSE}, + {0x14, KEY_CAPSLOCK}, {0x15, KEY_KATAKANAHIRAGANA}, + {0x16, KEY_HANGEUL}, {0x17, KEY_HANJA}, + {0x19, KEY_KATAKANA}, {0x1B, KEY_ESC}, + {0x20, KEY_SPACE}, {0x21, KEY_PAGEUP}, + {0x22, KEY_PAGEDOWN}, {0x23, KEY_END}, + {0x24, KEY_HOME}, {0x25, KEY_LEFT}, + {0x26, KEY_UP}, {0x27, KEY_RIGHT}, + {0x28, KEY_DOWN}, {0x29, KEY_SELECT}, + {0x2A, KEY_PRINT}, {0x2C, KEY_SYSRQ}, + {0x2D, KEY_INSERT}, {0x2E, KEY_DELETE}, + {0x2F, KEY_HELP}, {0x30, KEY_0}, + {0x31, KEY_1}, {0x32, KEY_2}, + {0x33, KEY_3}, {0x34, KEY_4}, + {0x35, KEY_5}, {0x36, KEY_6}, + {0x37, KEY_7}, {0x38, KEY_8}, + {0x39, KEY_9}, {0x41, KEY_A}, + {0x42, KEY_B}, {0x43, KEY_C}, + {0x44, KEY_D}, {0x45, KEY_E}, + {0x46, KEY_F}, {0x47, KEY_G}, + {0x48, KEY_H}, {0x49, KEY_I}, + {0x4A, KEY_J}, {0x4B, KEY_K}, + {0x4C, KEY_L}, {0x4D, KEY_M}, + {0x4E, KEY_N}, {0x4F, KEY_O}, + {0x50, KEY_P}, {0x51, KEY_Q}, + {0x52, KEY_R}, {0x53, KEY_S}, + {0x54, KEY_T}, {0x55, KEY_U}, + {0x56, KEY_V}, {0x57, KEY_W}, + {0x58, KEY_X}, {0x59, KEY_Y}, + {0x5A, KEY_Z}, {0x5B, KEY_LEFTMETA}, + {0x5C, KEY_RIGHTMETA}, {0x5F, KEY_SLEEP}, + {0x60, KEY_KP0}, {0x61, KEY_KP1}, + {0x62, KEY_KP2}, {0x63, KEY_KP3}, + {0x64, KEY_KP4}, {0x65, KEY_KP5}, + {0x66, KEY_KP6}, {0x67, KEY_KP7}, + {0x68, KEY_KP8}, {0x69, KEY_KP9}, + {0x6A, KEY_KPASTERISK}, {0x6B, KEY_KPPLUS}, + {0x6C, KEY_KPCOMMA}, {0x6D, KEY_KPMINUS}, + {0x6E, KEY_KPDOT}, {0x6F, KEY_KPSLASH}, + {0x70, KEY_F1}, {0x71, KEY_F2}, + {0x72, KEY_F3}, {0x73, KEY_F4}, + {0x74, KEY_F5}, {0x75, KEY_F6}, + {0x76, KEY_F7}, {0x77, KEY_F8}, + {0x78, KEY_F9}, {0x79, KEY_F10}, + {0x7A, KEY_F11}, {0x7B, KEY_F12}, + {0x90, KEY_NUMLOCK}, {0x91, KEY_SCROLLLOCK}, + {0xA0, KEY_LEFTSHIFT}, {0xA1, KEY_RIGHTSHIFT}, + {0xA2, KEY_LEFTCTRL}, {0xA3, KEY_RIGHTCTRL}, + {0xA4, KEY_LEFTALT}, {0xA5, KEY_RIGHTALT}, + {0xBA, KEY_SEMICOLON}, {0xBB, KEY_EQUAL}, + {0xBC, KEY_COMMA}, {0xBD, KEY_MINUS}, + {0xBE, KEY_DOT}, {0xBF, KEY_SLASH}, + {0xC0, KEY_GRAVE}, {0xDB, KEY_LEFTBRACE}, + {0xDC, KEY_BACKSLASH}, {0xDD, KEY_RIGHTBRACE}, + {0xDE, KEY_APOSTROPHE}, {0xE2, KEY_102ND}, +}; + +void WaylandKeyboard::press(unsigned int key_code) { + display_keyboard_input(w_state->display, key_mappings.at(key_code), true); +} + +void WaylandKeyboard::release(unsigned int key_code) { + display_keyboard_input(w_state->display, key_mappings.at(key_code), false); +} + } // namespace wolf::core::virtual_display \ No newline at end of file diff --git a/src/moonlight-server/control/input_handler.cpp b/src/moonlight-server/control/input_handler.cpp index 4d47822d..de69ce41 100644 --- a/src/moonlight-server/control/input_handler.cpp +++ b/src/moonlight-server/control/input_handler.cpp @@ -213,81 +213,126 @@ static inline float deg2rad(float degree) { } 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); + if (session.mouse->has_value()) { + short delta_x = boost::endian::big_to_native(pkt.delta_x); + short delta_y = boost::endian::big_to_native(pkt.delta_y); + std::visit([delta_x, delta_y](auto &mouse) { mouse.move(delta_x, delta_y); }, session.mouse->value()); + } else { + logs::log(logs::warning, "Received MOUSE_MOVE_REL_PACKET but no mouse device is present"); + } } 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); + if (session.mouse->has_value()) { + 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); + std::visit([x, y, width, height](auto &mouse) { mouse.move_abs(x, y, width, height); }, session.mouse->value()); + } else { + logs::log(logs::warning, "Received MOUSE_MOVE_ABS_PACKET but no mouse device is present"); + } } void mouse_button(const MOUSE_BUTTON_PACKET &pkt, state::StreamSession &session) { - Mouse::MOUSE_BUTTON btn_type; + if (session.mouse->has_value()) { + if (std::holds_alternative(session.mouse->value())) { + 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); + 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) { + std::get(session.mouse->value()).press(btn_type); + } else { + std::get(session.mouse->value()).release(btn_type); + } + } else if (std::holds_alternative(session.mouse->value())) { + if (pkt.type == MOUSE_BUTTON_PRESS) { + std::get(session.mouse->value()).press(pkt.button); + } else { + std::get(session.mouse->value()).release(pkt.button); + } + } } else { - session.mouse->release(btn_type); + logs::log(logs::warning, "Received MOUSE_BUTTON_PACKET but no mouse device is present"); } } void mouse_scroll(const MOUSE_SCROLL_PACKET &pkt, state::StreamSession &session) { - session.mouse->vertical_scroll(boost::endian::big_to_native(pkt.scroll_amt1)); + if (session.mouse->has_value()) { + std::visit([scroll_amount = boost::endian::big_to_native(pkt.scroll_amt1)]( + auto &mouse) { mouse.vertical_scroll(scroll_amount); }, + session.mouse->value()); + } else { + logs::log(logs::warning, "Received MOUSE_SCROLL_PACKET but no mouse device is present"); + } } void mouse_h_scroll(const MOUSE_HSCROLL_PACKET &pkt, state::StreamSession &session) { - session.mouse->horizontal_scroll(boost::endian::big_to_native(pkt.scroll_amount)); + if (session.mouse->has_value()) { + std::visit([scroll_amount = boost::endian::big_to_native(pkt.scroll_amount)]( + auto &mouse) { mouse.horizontal_scroll(scroll_amount); }, + session.mouse->value()); + } else { + logs::log(logs::warning, "Received MOUSE_HSCROLL_PACKET but no mouse device is present"); + } } 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); + if (session.keyboard->has_value()) { + if (pkt.type == KEY_PRESS) { + std::visit([moonlight_key](auto &keyboard) { keyboard.press(moonlight_key); }, session.keyboard->value()); + } else { + std::visit([moonlight_key](auto &keyboard) { keyboard.release(moonlight_key); }, session.keyboard->value()); + } } else { - session.keyboard->release(moonlight_key); + logs::log(logs::warning, "Received KEYBOARD_PACKET but no keyboard device is present"); } } 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); + if (session.keyboard->has_value()) { + if (std::holds_alternative(session.keyboard->value())) { + /* 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(std::get(session.keyboard->value()), utf32); + } else { + // TODO: implement this in our custom comp + logs::log(logs::warning, "Direct paste of UTF-8 text is not supported yet"); + } + } else { + logs::log(logs::warning, "Received UTF8_TEXT_PACKET but no keyboard device is present"); + } } void touch(const TOUCH_PACKET &pkt, state::StreamSession &session) { diff --git a/src/moonlight-server/platforms/input.hpp b/src/moonlight-server/platforms/input.hpp index 38e83198..dad022b5 100644 --- a/src/moonlight-server/platforms/input.hpp +++ b/src/moonlight-server/platforms/input.hpp @@ -26,5 +26,5 @@ static std::string to_hex(const std::basic_string &str) { return hex_unicode; } -void paste_utf(std::shared_ptr keyboard, const std::basic_string &utf32); +void paste_utf(wolf::core::input::Keyboard &keyboard, const std::basic_string &utf32); } // namespace wolf::platforms::input \ No newline at end of file diff --git a/src/moonlight-server/platforms/input_linux.cpp b/src/moonlight-server/platforms/input_linux.cpp index 63793bf9..a96025b1 100644 --- a/src/moonlight-server/platforms/input_linux.cpp +++ b/src/moonlight-server/platforms/input_linux.cpp @@ -37,15 +37,15 @@ static const std::map key_mappings = { {KEY_DOT, 0xBE}, {KEY_SLASH, 0xBF}, {KEY_GRAVE, 0xC0}, {KEY_LEFTBRACE, 0xDB}, {KEY_BACKSLASH, 0xDC}, {KEY_RIGHTBRACE, 0xDD}, {KEY_APOSTROPHE, 0xDE}, {KEY_102ND, 0xE2}}; -void paste_utf(std::shared_ptr keyboard, const std::basic_string &utf32) { +void paste_utf(wolf::core::input::Keyboard &keyboard, const std::basic_string &utf32) { /* To HEX string */ auto hex_unicode = to_hex(utf32); logs::log(logs::debug, "[INPUT] Typing U+{}", hex_unicode); - keyboard->press(0xA2); // LEFTCTRL - keyboard->press(0xA0); // LEFTSHIFT - keyboard->press(0x55); // U - keyboard->release(0x55); // U + keyboard.press(0xA2); // LEFTCTRL + keyboard.press(0xA0); // LEFTSHIFT + keyboard.press(0x55); // U + keyboard.release(0x55); // U for (auto &ch : hex_unicode) { auto key_str = "KEY_"s + ch; @@ -53,12 +53,12 @@ void paste_utf(std::shared_ptr keyboard, const std: if (keycode == -1) { logs::log(logs::warning, "[INPUT] Unable to find keycode for: {}", ch); } else { - keyboard->press(key_mappings.at(keycode)); - keyboard->release(key_mappings.at(keycode)); + keyboard.press(key_mappings.at(keycode)); + keyboard.release(key_mappings.at(keycode)); } } - keyboard->release(0xA0); // LEFTSHIFT - keyboard->release(0xA2); // LEFTCTRL + keyboard.release(0xA0); // LEFTSHIFT + keyboard.release(0xA2); // LEFTCTRL } } // namespace wolf::platforms::input \ No newline at end of file diff --git a/src/moonlight-server/rest/endpoints.hpp b/src/moonlight-server/rest/endpoints.hpp index e7b5f23c..3ee0c9bf 100644 --- a/src/moonlight-server/rest/endpoints.hpp +++ b/src/moonlight-server/rest/endpoints.hpp @@ -301,20 +301,6 @@ void launch(const std::shared_ptr:: SimpleWeb::CaseInsensitiveMultimap headers = request->parse_query_string(); auto app = state::get_app_by_id(state->config, get_header(headers, "appid").value()); auto new_session = create_run_session(request, current_client, state->event_bus, app); - // virtual devices - auto mouse = input::Mouse::create(); - if (!mouse) { - logs::log(logs::error, "Failed to create mouse: {}", mouse.getErrorMessage()); - } else { - 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(std::move(*keyboard)); - } state->event_bus->fire_event(immer::box(new_session)); state->running_sessions->update( [&new_session](const immer::vector &ses_v) { return ses_v.push_back(new_session); }); @@ -336,10 +322,14 @@ void resume(const std::shared_ptr:: auto old_session = get_session_by_ip(state->running_sessions->load(), client_ip); if (old_session) { auto new_session = create_run_session(request, current_client, state->event_bus, *old_session->app); + // Carry over the old session display handle + new_session.wayland_display = std::move(old_session->wayland_display); // Carry over the old session devices, they'll be already plugged into the container new_session.mouse = std::move(old_session->mouse); new_session.keyboard = std::move(old_session->keyboard); new_session.joypads = std::move(old_session->joypads); + new_session.pen_tablet = std::move(old_session->pen_tablet); + new_session.touch_screen = std::move(old_session->touch_screen); start_rtp_ping(state); diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index 461fd40a..2b01f9a5 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -185,6 +185,8 @@ struct PairCache { std::optional client_hash; }; +using MouseTypes = std::variant; +using KeyboardTypes = std::variant; using JoypadTypes = std::variant; using JoypadList = immer::map>; @@ -218,9 +220,11 @@ struct StreamSession { std::make_shared>(); // virtual devices - std::shared_ptr mouse; - std::shared_ptr keyboard; + std::shared_ptr> mouse = std::make_shared>(); + std::shared_ptr> keyboard = std::make_shared>(); + std::shared_ptr> joypads = std::make_shared>(); + std::shared_ptr> pen_tablet = std::make_shared>(); /* Optional, will be set on first use */ std::shared_ptr> touch_screen = diff --git a/src/moonlight-server/wolf.cpp b/src/moonlight-server/wolf.cpp index 3c6a0e5a..e6b00e80 100644 --- a/src/moonlight-server/wolf.cpp +++ b/src/moonlight-server/wolf.cpp @@ -231,13 +231,9 @@ auto setup_sessions_handlers(const immer::box &app_state, if (session->app->start_virtual_compositor) { logs::log(logs::debug, "[STREAM_SESSION] Create wayland compositor"); - auto mouse_nodes = session->mouse->get_nodes(); - auto kb_nodes = session->keyboard->get_nodes(); - auto input_nodes = immer::array_transient(); - std::copy(mouse_nodes.begin(), mouse_nodes.end(), std::back_inserter(input_nodes)); - std::copy(kb_nodes.begin(), kb_nodes.end(), std::back_inserter(input_nodes)); + // TODO: allow for old inputtino mouse and keyboard - auto wl_state = virtual_display::create_wayland_display(input_nodes.persistent(), render_node); + auto wl_state = virtual_display::create_wayland_display({}, render_node); virtual_display::set_resolution( *wl_state, {session->display_mode.width, session->display_mode.height, session->display_mode.refreshRate}); @@ -264,7 +260,33 @@ auto setup_sessions_handlers(const immer::box &app_state, // Set the wayland display session->wayland_display->store(wl_state); + + // Set virtual devices + session->mouse->emplace(virtual_display::WaylandMouse(wl_state)); + session->keyboard->emplace(virtual_display::WaylandKeyboard(wl_state)); + wl_promise->set_value(std::move(wl_state)); + } else { + // Create virtual devices + auto mouse = input::Mouse::create(); + if (!mouse) { + logs::log(logs::error, "Failed to create mouse: {}", mouse.getErrorMessage()); + } else { + for (auto &path : (*mouse).get_nodes()) { + all_devices.push_back(path); + } + session->mouse->emplace(std::move(*mouse)); + } + + auto keyboard = input::Keyboard::create(); + if (!keyboard) { + logs::log(logs::error, "Failed to create keyboard: {}", keyboard.getErrorMessage()); + } else { + for (auto &path : (*keyboard).get_nodes()) { + all_devices.push_back(path); + } + session->keyboard->emplace(std::move(*keyboard)); + } } /* Adding custom state folder */ diff --git a/tests/platforms/linux/input.cpp b/tests/platforms/linux/input.cpp index 438aa4cf..60ed3120 100644 --- a/tests/platforms/linux/input.cpp +++ b/tests/platforms/linux/input.cpp @@ -22,8 +22,9 @@ using namespace std::string_literals; TEST_CASE("uinput - keyboard", "[UINPUT]") { libevdev_ptr keyboard_dev(libevdev_new(), ::libevdev_free); - auto session = state::StreamSession{.keyboard = std::make_shared(std::move(*Keyboard::create()))}; - link_devnode(keyboard_dev.get(), session.keyboard->get_nodes()[0]); + auto session = state::StreamSession{ + .keyboard = std::make_shared>(std::move(*Keyboard::create()))}; + link_devnode(keyboard_dev.get(), std::get(session.keyboard->value()).get_nodes()[0]); auto events = fetch_events_debug(keyboard_dev); REQUIRE(events.empty()); @@ -181,11 +182,11 @@ TEST_CASE("uinput - touch screen", "[UINPUT]") { TEST_CASE("uinput - mouse", "[UINPUT]") { libevdev_ptr mouse_rel_dev(libevdev_new(), ::libevdev_free); libevdev_ptr mouse_abs_dev(libevdev_new(), ::libevdev_free); - auto mouse = std::make_shared(std::move(*Mouse::create())); + 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(), std::get(mouse->value()).get_nodes()[0]); + link_devnode(mouse_abs_dev.get(), std::get(mouse->value()).get_nodes()[1]); auto events = fetch_events_debug(mouse_rel_dev); REQUIRE(events.empty()); @@ -268,7 +269,7 @@ TEST_CASE("uinput - mouse", "[UINPUT]") { } SECTION("UDEV") { - auto udev_events = mouse->get_udev_events(); + auto udev_events = std::get(mouse->value()).get_udev_events(); REQUIRE(udev_events.size() == 2); @@ -365,8 +366,9 @@ 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(std::move(*Keyboard::create()))}; - link_devnode(keyboard_dev.get(), session.keyboard->get_nodes()[0]); + auto session = state::StreamSession{ + .keyboard = std::make_shared>(std::move(*Keyboard::create()))}; + link_devnode(keyboard_dev.get(), std::get(session.keyboard->value()).get_nodes()[0]); auto events = fetch_events_debug(keyboard_dev); REQUIRE(events.empty()); From 800de41b2fceb770aa7cfee30dcf3f954e2a51ae Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Wed, 17 Jul 2024 18:43:57 +0100 Subject: [PATCH 2/9] CI: pull latest gst-wayland-display --- .github/workflows/linux-build-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/linux-build-test.yml b/.github/workflows/linux-build-test.yml index 18c74b67..7e9b73af 100644 --- a/.github/workflows/linux-build-test.yml +++ b/.github/workflows/linux-build-test.yml @@ -43,6 +43,7 @@ jobs: run: | git clone https://github.com/games-on-whales/gst-wayland-display cd gst-wayland-display + git checkout 48382dc cargo install cargo-c cargo cinstall -p c-bindings --prefix=/usr/local @@ -113,6 +114,7 @@ jobs: run: | git clone https://github.com/games-on-whales/gst-wayland-display cd gst-wayland-display + git checkout 48382dc cargo install cargo-c cargo cinstall -p c-bindings --prefix=/usr/local --destdir=${{runner.workspace}} From c859e154e7294db376e5d01d9de776fc38852900 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Thu, 18 Jul 2024 18:10:12 +0100 Subject: [PATCH 3/9] fix: added UTF paste --- .../control/input_handler.cpp | 37 ++++++++--------- src/moonlight-server/platforms/input.hpp | 3 +- .../platforms/input_linux.cpp | 40 +++++++++++-------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/moonlight-server/control/input_handler.cpp b/src/moonlight-server/control/input_handler.cpp index de69ce41..072e1f8b 100644 --- a/src/moonlight-server/control/input_handler.cpp +++ b/src/moonlight-server/control/input_handler.cpp @@ -309,27 +309,22 @@ void keyboard_key(const KEYBOARD_PACKET &pkt, state::StreamSession &session) { void utf8_text(const UTF8_TEXT_PACKET &pkt, state::StreamSession &session) { if (session.keyboard->has_value()) { - if (std::holds_alternative(session.keyboard->value())) { - /* 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(std::get(session.keyboard->value()), utf32); - } else { - // TODO: implement this in our custom comp - logs::log(logs::warning, "Direct paste of UTF-8 text is not supported yet"); - } + /* 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->value(), utf32); } else { logs::log(logs::warning, "Received UTF8_TEXT_PACKET but no keyboard device is present"); } diff --git a/src/moonlight-server/platforms/input.hpp b/src/moonlight-server/platforms/input.hpp index dad022b5..f5340cf1 100644 --- a/src/moonlight-server/platforms/input.hpp +++ b/src/moonlight-server/platforms/input.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace wolf::platforms::input { /** @@ -26,5 +27,5 @@ static std::string to_hex(const std::basic_string &str) { return hex_unicode; } -void paste_utf(wolf::core::input::Keyboard &keyboard, const std::basic_string &utf32); +void paste_utf(state::KeyboardTypes &keyboard, const std::basic_string &utf32); } // namespace wolf::platforms::input \ No newline at end of file diff --git a/src/moonlight-server/platforms/input_linux.cpp b/src/moonlight-server/platforms/input_linux.cpp index a96025b1..ddb6bbea 100644 --- a/src/moonlight-server/platforms/input_linux.cpp +++ b/src/moonlight-server/platforms/input_linux.cpp @@ -1,3 +1,4 @@ +#include "core/virtual-display.hpp" #include "input.hpp" #include #include @@ -37,28 +38,33 @@ static const std::map key_mappings = { {KEY_DOT, 0xBE}, {KEY_SLASH, 0xBF}, {KEY_GRAVE, 0xC0}, {KEY_LEFTBRACE, 0xDB}, {KEY_BACKSLASH, 0xDC}, {KEY_RIGHTBRACE, 0xDD}, {KEY_APOSTROPHE, 0xDE}, {KEY_102ND, 0xE2}}; -void paste_utf(wolf::core::input::Keyboard &keyboard, const std::basic_string &utf32) { +void paste_utf(state::KeyboardTypes &keyboard, const std::basic_string &utf32) { /* To HEX string */ auto hex_unicode = to_hex(utf32); logs::log(logs::debug, "[INPUT] Typing U+{}", hex_unicode); - keyboard.press(0xA2); // LEFTCTRL - keyboard.press(0xA0); // LEFTSHIFT - keyboard.press(0x55); // U - keyboard.release(0x55); // U + std::visit( + [hex_unicode](auto &kb) { + kb.press(0xA2); // LEFTCTRL + kb.press(0xA0); // LEFTSHIFT + kb.press(0x55); // U + kb.release(0x55); // U - for (auto &ch : hex_unicode) { - auto key_str = "KEY_"s + ch; - auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); - if (keycode == -1) { - logs::log(logs::warning, "[INPUT] Unable to find keycode for: {}", ch); - } else { - keyboard.press(key_mappings.at(keycode)); - keyboard.release(key_mappings.at(keycode)); - } - } + for (auto &ch : hex_unicode) { + auto key_str = "KEY_"s + ch; + auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); + if (keycode == -1) { + logs::log(logs::warning, "[INPUT] Unable to find keycode for: {}", ch); + } else { + kb.press(key_mappings.at(keycode)); + kb.release(key_mappings.at(keycode)); + } + } - keyboard.release(0xA0); // LEFTSHIFT - keyboard.release(0xA2); // LEFTCTRL + kb.release(0xA0); // LEFTSHIFT + kb.release(0xA2); // LEFTCTRL + }, + keyboard); } + } // namespace wolf::platforms::input \ No newline at end of file From 832d959f1f6a5bb7b03013aa082c13b179ca58bc Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 19 Jul 2024 18:35:12 +0100 Subject: [PATCH 4/9] tests: added simple wayland client to properly test events --- cmake/ECMFindModuleHelpers.cmake | 277 ++++++++++++ cmake/FindWaylandScanner.cmake | 170 ++++++++ tests/CMakeLists.txt | 29 +- tests/main.cpp | 6 + .../linux/{input.cpp => inputtino.cpp} | 0 tests/platforms/linux/wayland-client.hpp | 399 ++++++++++++++++++ tests/platforms/linux/wayland-display.cpp | 93 +++- tests/testMoonlight.cpp | 4 - 8 files changed, 969 insertions(+), 9 deletions(-) create mode 100644 cmake/ECMFindModuleHelpers.cmake create mode 100644 cmake/FindWaylandScanner.cmake rename tests/platforms/linux/{input.cpp => inputtino.cpp} (100%) create mode 100644 tests/platforms/linux/wayland-client.hpp diff --git a/cmake/ECMFindModuleHelpers.cmake b/cmake/ECMFindModuleHelpers.cmake new file mode 100644 index 00000000..33433315 --- /dev/null +++ b/cmake/ECMFindModuleHelpers.cmake @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: 2014 Alex Merry +# +# SPDX-License-Identifier: BSD-3-Clause + +#[=======================================================================[.rst: +ECMFindModuleHelpers +-------------------- + +Helper macros for find modules: ``ecm_find_package_version_check()``, +``ecm_find_package_parse_components()`` and +``ecm_find_package_handle_library_components()``. + +:: + + ecm_find_package_version_check() + +Prints warnings if the CMake version or the project's required CMake version +is older than that required by extra-cmake-modules. + +:: + + ecm_find_package_parse_components( + RESULT_VAR + KNOWN_COMPONENTS [ [...]] + [SKIP_DEPENDENCY_HANDLING]) + +This macro will populate with a list of components found in +_FIND_COMPONENTS, after checking that all those components are in the +list of ``KNOWN_COMPONENTS``; if there are any unknown components, it will print +an error or warning (depending on the value of _FIND_REQUIRED) and call +``return()``. + +The order of components in is guaranteed to match the order they +are listed in the ``KNOWN_COMPONENTS`` argument. + +If ``SKIP_DEPENDENCY_HANDLING`` is not set, for each component the variable +__component_deps will be checked for dependent components. +If is listed in _FIND_COMPONENTS, then all its (transitive) +dependencies will also be added to . + +:: + + ecm_find_package_handle_library_components( + COMPONENTS [ [...]] + [SKIP_DEPENDENCY_HANDLING]) + [SKIP_PKG_CONFIG]) + +Creates an imported library target for each component. The operation of this +macro depends on the presence of a number of CMake variables. + +The __lib variable should contain the name of this library, +and __header variable should contain the name of a header +file associated with it (whatever relative path is normally passed to +'#include'). __header_subdir variable can be used to specify +which subdirectory of the include path the headers will be found in. +``ecm_find_package_components()`` will then search for the library +and include directory (creating appropriate cache variables) and create an +imported library target named ::. + +Additional variables can be used to provide additional information: + +If ``SKIP_PKG_CONFIG``, the __pkg_config variable is set, and +pkg-config is found, the pkg-config module given by +__pkg_config will be searched for and used to help locate the +library and header file. It will also be used to set +__VERSION. + +Note that if version information is found via pkg-config, +__FIND_VERSION can be set to require a particular version +for each component. + +If ``SKIP_DEPENDENCY_HANDLING`` is not set, the ``INTERFACE_LINK_LIBRARIES`` property +of the imported target for will be set to contain the imported +targets for the components listed in __component_deps. +_FOUND will also be set to ``FALSE`` if any of the components in +__component_deps are not found. This requires the components +in __component_deps to be listed before in the +``COMPONENTS`` argument. + +The following variables will be set: + +``_TARGETS`` + the imported targets +``_LIBRARIES`` + the found libraries +``_INCLUDE_DIRS`` + the combined required include directories for the components +``_DEFINITIONS`` + the "other" CFLAGS provided by pkg-config, if any +``_VERSION`` + the value of ``__VERSION`` for the first component that + has this variable set (note that components are searched for in the order + they are passed to the macro), although if it is already set, it will not + be altered + +.. note:: + These variables are never cleared, so if + ``ecm_find_package_handle_library_components()`` is called multiple times with + different components (typically because of multiple ``find_package()`` calls) then + ``_TARGETS``, for example, will contain all the targets found in any + call (although no duplicates). + +Since pre-1.0.0. +#]=======================================================================] + +macro(ecm_find_package_version_check module_name) + if(CMAKE_VERSION VERSION_LESS 3.16.0) + message(FATAL_ERROR "CMake 3.16.0 is required by Find${module_name}.cmake") + endif() + if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 3.16.0) + message(AUTHOR_WARNING "Your project should require at least CMake 3.16.0 to use Find${module_name}.cmake") + endif() +endmacro() + +macro(ecm_find_package_parse_components module_name) + set(ecm_fppc_options SKIP_DEPENDENCY_HANDLING) + set(ecm_fppc_oneValueArgs RESULT_VAR) + set(ecm_fppc_multiValueArgs KNOWN_COMPONENTS DEFAULT_COMPONENTS) + cmake_parse_arguments(ECM_FPPC "${ecm_fppc_options}" "${ecm_fppc_oneValueArgs}" "${ecm_fppc_multiValueArgs}" ${ARGN}) + + if(ECM_FPPC_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unexpected arguments to ecm_find_package_parse_components: ${ECM_FPPC_UNPARSED_ARGUMENTS}") + endif() + if(NOT ECM_FPPC_RESULT_VAR) + message(FATAL_ERROR "Missing RESULT_VAR argument to ecm_find_package_parse_components") + endif() + if(NOT ECM_FPPC_KNOWN_COMPONENTS) + message(FATAL_ERROR "Missing KNOWN_COMPONENTS argument to ecm_find_package_parse_components") + endif() + if(NOT ECM_FPPC_DEFAULT_COMPONENTS) + set(ECM_FPPC_DEFAULT_COMPONENTS ${ECM_FPPC_KNOWN_COMPONENTS}) + endif() + + if(${module_name}_FIND_COMPONENTS) + set(ecm_fppc_requestedComps ${${module_name}_FIND_COMPONENTS}) + + if(NOT ECM_FPPC_SKIP_DEPENDENCY_HANDLING) + # Make sure deps are included + foreach(ecm_fppc_comp ${ecm_fppc_requestedComps}) + foreach(ecm_fppc_dep_comp ${${module_name}_${ecm_fppc_comp}_component_deps}) + list(FIND ecm_fppc_requestedComps "${ecm_fppc_dep_comp}" ecm_fppc_index) + if("${ecm_fppc_index}" STREQUAL "-1") + if(NOT ${module_name}_FIND_QUIETLY) + message(STATUS "${module_name}: ${ecm_fppc_comp} requires ${${module_name}_${ecm_fppc_comp}_component_deps}") + endif() + list(APPEND ecm_fppc_requestedComps "${ecm_fppc_dep_comp}") + endif() + endforeach() + endforeach() + else() + message(STATUS "Skipping dependency handling for ${module_name}") + endif() + list(REMOVE_DUPLICATES ecm_fppc_requestedComps) + + # This makes sure components are listed in the same order as + # KNOWN_COMPONENTS (potentially important for inter-dependencies) + set(${ECM_FPPC_RESULT_VAR}) + foreach(ecm_fppc_comp ${ECM_FPPC_KNOWN_COMPONENTS}) + list(FIND ecm_fppc_requestedComps "${ecm_fppc_comp}" ecm_fppc_index) + if(NOT "${ecm_fppc_index}" STREQUAL "-1") + list(APPEND ${ECM_FPPC_RESULT_VAR} "${ecm_fppc_comp}") + list(REMOVE_AT ecm_fppc_requestedComps ${ecm_fppc_index}) + endif() + endforeach() + # if there are any left, they are unknown components + if(ecm_fppc_requestedComps) + set(ecm_fppc_msgType STATUS) + if(${module_name}_FIND_REQUIRED) + set(ecm_fppc_msgType FATAL_ERROR) + endif() + if(NOT ${module_name}_FIND_QUIETLY) + message(${ecm_fppc_msgType} "${module_name}: requested unknown components ${ecm_fppc_requestedComps}") + endif() + return() + endif() + else() + set(${ECM_FPPC_RESULT_VAR} ${ECM_FPPC_DEFAULT_COMPONENTS}) + endif() +endmacro() + +macro(ecm_find_package_handle_library_components module_name) + set(ecm_fpwc_options SKIP_PKG_CONFIG SKIP_DEPENDENCY_HANDLING) + set(ecm_fpwc_oneValueArgs) + set(ecm_fpwc_multiValueArgs COMPONENTS) + cmake_parse_arguments(ECM_FPWC "${ecm_fpwc_options}" "${ecm_fpwc_oneValueArgs}" "${ecm_fpwc_multiValueArgs}" ${ARGN}) + + if(ECM_FPWC_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unexpected arguments to ecm_find_package_handle_components: ${ECM_FPWC_UNPARSED_ARGUMENTS}") + endif() + if(NOT ECM_FPWC_COMPONENTS) + message(FATAL_ERROR "Missing COMPONENTS argument to ecm_find_package_handle_components") + endif() + + include(FindPackageHandleStandardArgs) + find_package(PkgConfig QUIET) + foreach(ecm_fpwc_comp ${ECM_FPWC_COMPONENTS}) + set(ecm_fpwc_dep_vars) + set(ecm_fpwc_dep_targets) + if(NOT SKIP_DEPENDENCY_HANDLING) + foreach(ecm_fpwc_dep ${${module_name}_${ecm_fpwc_comp}_component_deps}) + list(APPEND ecm_fpwc_dep_vars "${module_name}_${ecm_fpwc_dep}_FOUND") + list(APPEND ecm_fpwc_dep_targets "${module_name}::${ecm_fpwc_dep}") + endforeach() + endif() + + if(NOT ECM_FPWC_SKIP_PKG_CONFIG AND ${module_name}_${ecm_fpwc_comp}_pkg_config) + pkg_check_modules(PKG_${module_name}_${ecm_fpwc_comp} QUIET + ${${module_name}_${ecm_fpwc_comp}_pkg_config}) + endif() + + find_path(${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR + NAMES ${${module_name}_${ecm_fpwc_comp}_header} + HINTS ${PKG_${module_name}_${ecm_fpwc_comp}_INCLUDE_DIRS} + PATH_SUFFIXES ${${module_name}_${ecm_fpwc_comp}_header_subdir} + ) + find_library(${module_name}_${ecm_fpwc_comp}_LIBRARY + NAMES ${${module_name}_${ecm_fpwc_comp}_lib} + HINTS ${PKG_${module_name}_${ecm_fpwc_comp}_LIBRARY_DIRS} + ) + + set(${module_name}_${ecm_fpwc_comp}_VERSION "${PKG_${module_name}_${ecm_fpwc_comp}_VERSION}") + if(NOT ${module_name}_VERSION) + set(${module_name}_VERSION ${${module_name}_${ecm_fpwc_comp}_VERSION}) + endif() + + set(FPHSA_NAME_MISMATCHED 1) + find_package_handle_standard_args(${module_name}_${ecm_fpwc_comp} + FOUND_VAR + ${module_name}_${ecm_fpwc_comp}_FOUND + REQUIRED_VARS + ${module_name}_${ecm_fpwc_comp}_LIBRARY + ${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR + ${ecm_fpwc_dep_vars} + VERSION_VAR + ${module_name}_${ecm_fpwc_comp}_VERSION + ) + unset(FPHSA_NAME_MISMATCHED) + + mark_as_advanced( + ${module_name}_${ecm_fpwc_comp}_LIBRARY + ${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR + ) + + if(${module_name}_${ecm_fpwc_comp}_FOUND) + list(APPEND ${module_name}_LIBRARIES + "${${module_name}_${ecm_fpwc_comp}_LIBRARY}") + list(APPEND ${module_name}_INCLUDE_DIRS + "${${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR}") + set(${module_name}_DEFINITIONS + ${${module_name}_DEFINITIONS} + ${PKG_${module_name}_${ecm_fpwc_comp}_DEFINITIONS}) + if(NOT TARGET ${module_name}::${ecm_fpwc_comp}) + add_library(${module_name}::${ecm_fpwc_comp} UNKNOWN IMPORTED) + set_target_properties(${module_name}::${ecm_fpwc_comp} PROPERTIES + IMPORTED_LOCATION "${${module_name}_${ecm_fpwc_comp}_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${PKG_${module_name}_${ecm_fpwc_comp}_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${ecm_fpwc_dep_targets}" + ) + endif() + list(APPEND ${module_name}_TARGETS + "${module_name}::${ecm_fpwc_comp}") + endif() + endforeach() + if(${module_name}_LIBRARIES) + list(REMOVE_DUPLICATES ${module_name}_LIBRARIES) + endif() + if(${module_name}_INCLUDE_DIRS) + list(REMOVE_DUPLICATES ${module_name}_INCLUDE_DIRS) + endif() + if(${module_name}_DEFINITIONS) + list(REMOVE_DUPLICATES ${module_name}_DEFINITIONS) + endif() + if(${module_name}_TARGETS) + list(REMOVE_DUPLICATES ${module_name}_TARGETS) + endif() +endmacro() \ No newline at end of file diff --git a/cmake/FindWaylandScanner.cmake b/cmake/FindWaylandScanner.cmake new file mode 100644 index 00000000..2579b3e4 --- /dev/null +++ b/cmake/FindWaylandScanner.cmake @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: 2012-2014 Pier Luigi Fiorini +# +# SPDX-License-Identifier: BSD-3-Clause + +#[=======================================================================[.rst: +FindWaylandScanner +------------------ + +Try to find wayland-scanner. + +If the wayland-scanner executable is not in your PATH, you can provide +an alternative name or full path location with the ``WaylandScanner_EXECUTABLE`` +variable. + +This will define the following variables: + +``WaylandScanner_FOUND`` + True if wayland-scanner is available. + +``WaylandScanner_EXECUTABLE`` + The wayland-scanner executable. + +If ``WaylandScanner_FOUND`` is TRUE, it will also define the following imported +target: + +``Wayland::Scanner`` + The wayland-scanner executable. + +This module provides the following functions to generate C protocol +implementations: + + - ``ecm_add_wayland_client_protocol`` + - ``ecm_add_wayland_server_protocol`` + +:: + + ecm_add_wayland_client_protocol( + PROTOCOL + BASENAME + [PRIVATE_CODE]) + + ecm_add_wayland_client_protocol( + PROTOCOL + BASENAME ) + +Generate Wayland client protocol files from ```` XML +definition for the ```` interface and append those files +to ```` or ````. + +``PRIVATE_CODE`` instructs wayland-scanner to hide marshalling code +from the compiled DSO for use in other DSOs. The default is to +export this code. + +:: + + ecm_add_wayland_server_protocol( + PROTOCOL + BASENAME ) + + ecm_add_wayland_server_protocol( + PROTOCOL + BASENAME ) + +Generate Wayland server protocol files from ```` XML +definition for the ```` interface and append those files +to ```` or ````. + +Since 1.4.0. +#]=======================================================================] + +include(${CMAKE_CURRENT_LIST_DIR}/ECMFindModuleHelpers.cmake) + +ecm_find_package_version_check(WaylandScanner) + +# Find wayland-scanner +find_program(WaylandScanner_EXECUTABLE NAMES wayland-scanner) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(WaylandScanner + FOUND_VAR + WaylandScanner_FOUND + REQUIRED_VARS + WaylandScanner_EXECUTABLE +) + +mark_as_advanced(WaylandScanner_EXECUTABLE) + +if(NOT TARGET Wayland::Scanner AND WaylandScanner_FOUND) + add_executable(Wayland::Scanner IMPORTED) + set_target_properties(Wayland::Scanner PROPERTIES + IMPORTED_LOCATION "${WaylandScanner_EXECUTABLE}" + ) +endif() + +include(FeatureSummary) +set_package_properties(WaylandScanner PROPERTIES + URL "https://wayland.freedesktop.org/" + DESCRIPTION "Executable that converts XML protocol files to C code" +) + +function(ecm_add_wayland_client_protocol target_or_sources_var) + # Parse arguments + set(options PRIVATE_CODE) + set(oneValueArgs PROTOCOL BASENAME) + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "" ${ARGN}) + + if(ARGS_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown keywords given to ecm_add_wayland_client_protocol(): \"${ARGS_UNPARSED_ARGUMENTS}\"") + endif() + + get_filename_component(_infile ${ARGS_PROTOCOL} ABSOLUTE) + set(_client_header "${CMAKE_CURRENT_BINARY_DIR}/wayland-${ARGS_BASENAME}-client-protocol.h") + set(_code "${CMAKE_CURRENT_BINARY_DIR}/wayland-${ARGS_BASENAME}-protocol.c") + if(ARGS_PRIVATE_CODE) + set(_code_type private-code) + else() + set(_code_type public-code) + endif() + + set_source_files_properties(${_client_header} GENERATED) + set_source_files_properties(${_code} GENERATED) + set_property(SOURCE ${_client_header} ${_code} PROPERTY SKIP_AUTOMOC ON) + + add_custom_command(OUTPUT "${_client_header}" + COMMAND ${WaylandScanner_EXECUTABLE} client-header ${_infile} ${_client_header} + DEPENDS ${_infile} VERBATIM) + + add_custom_command(OUTPUT "${_code}" + COMMAND ${WaylandScanner_EXECUTABLE} ${_code_type} ${_infile} ${_code} + DEPENDS ${_infile} ${_client_header} VERBATIM) + + if (TARGET ${target_or_sources_var}) + target_sources(${target_or_sources_var} PRIVATE "${_client_header}" "${_code}") + else() + list(APPEND ${target_or_sources_var} "${_client_header}" "${_code}") + set(${target_or_sources_var} ${${target_or_sources_var}} PARENT_SCOPE) + endif() +endfunction() + + +function(ecm_add_wayland_server_protocol target_or_sources_var) + # Parse arguments + set(oneValueArgs PROTOCOL BASENAME) + cmake_parse_arguments(ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(ARGS_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown keywords given to ecm_add_wayland_server_protocol(): \"${ARGS_UNPARSED_ARGUMENTS}\"") + endif() + + ecm_add_wayland_client_protocol(${target_or_sources_var} + PROTOCOL ${ARGS_PROTOCOL} + BASENAME ${ARGS_BASENAME}) + + get_filename_component(_infile ${ARGS_PROTOCOL} ABSOLUTE) + set(_server_header "${CMAKE_CURRENT_BINARY_DIR}/wayland-${ARGS_BASENAME}-server-protocol.h") + set(_server_code "${CMAKE_CURRENT_BINARY_DIR}/wayland-${ARGS_BASENAME}-protocol.c") + set_property(SOURCE ${_server_header} ${_server_code} PROPERTY SKIP_AUTOMOC ON) + set_source_files_properties(${_server_header} GENERATED) + + add_custom_command(OUTPUT "${_server_header}" + COMMAND ${WaylandScanner_EXECUTABLE} server-header ${_infile} ${_server_header} + DEPENDS ${_infile} VERBATIM) + + if (TARGET ${target_or_sources_var}) + target_sources(${target_or_sources_var} PRIVATE "${_server_header}") + else() + list(APPEND ${target_or_sources_var} "${_server_header}") + set(${target_or_sources_var} ${${target_or_sources_var}} PARENT_SCOPE) + endif() +endfunction() \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 85cf7133..6e227c8c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,8 @@ FetchContent_Declare( FetchContent_MakeAvailable(Catch2) +project(wolftests CXX C) + # Tests need to be added as executables first add_executable(wolftests main.cpp) @@ -21,7 +23,30 @@ set(SRC_LIST if (UNIX AND NOT APPLE) option(TEST_RUST_WAYLAND "Enable custom wayland test" ON) if (TEST_RUST_WAYLAND) - list(APPEND SRC_LIST "platforms/linux/wayland-display.cpp") + find_package(PkgConfig) + pkg_check_modules(wayland-client REQUIRED IMPORTED_TARGET wayland-client) + + # Add the wayland protocols + find_package(WaylandScanner REQUIRED QUIET) + pkg_check_modules(WAYLAND_PROTOCOLS REQUIRED wayland-protocols) + pkg_get_variable(WAYLAND_PROTOCOLS_PKGDATADIR wayland-protocols pkgdatadir) + # xdg-shell + ecm_add_wayland_client_protocol(wolftests + PROTOCOL ${WAYLAND_PROTOCOLS_PKGDATADIR}/stable/xdg-shell/xdg-shell.xml + BASENAME xdg-shell) + # relative-pointer + ecm_add_wayland_client_protocol(wolftests + PROTOCOL ${WAYLAND_PROTOCOLS_PKGDATADIR}/unstable/relative-pointer/relative-pointer-unstable-v1.xml + BASENAME relative-pointer) + + # Add the generated headers and files + include_directories(${CMAKE_CURRENT_BINARY_DIR}) + + list(APPEND SRC_LIST + "platforms/linux/wayland-client.hpp" + "platforms/linux/wayland-display.cpp" + ) + target_link_libraries(wolftests PRIVATE PkgConfig::wayland-client) endif () option(TEST_NVIDIA "Enable Nvidia tests" ON) @@ -46,7 +71,7 @@ if (UNIX AND NOT APPLE) endif () list(APPEND SRC_LIST "platforms/linux/libinput.h" - "platforms/linux/input.cpp") + "platforms/linux/inputtino.cpp") endif () endif () diff --git a/tests/main.cpp b/tests/main.cpp index 31ab8f7e..5f73d127 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,11 +1,17 @@ #define CATCH_CONFIG_FAST_COMPILE #include +#include +#include #include #include +#include int main(int argc, char *argv[]) { logs::init(logs::parse_level(utils::get_env("WOLF_LOG_LEVEL", "TRACE"))); + streaming::init(); + control::init(); // Need to initialise enet once + state::docker::init(); // Need to initialise libcurl once int result = Catch::Session().run(argc, argv); diff --git a/tests/platforms/linux/input.cpp b/tests/platforms/linux/inputtino.cpp similarity index 100% rename from tests/platforms/linux/input.cpp rename to tests/platforms/linux/inputtino.cpp diff --git a/tests/platforms/linux/wayland-client.hpp b/tests/platforms/linux/wayland-client.hpp new file mode 100644 index 00000000..f509e6ae --- /dev/null +++ b/tests/platforms/linux/wayland-client.hpp @@ -0,0 +1,399 @@ +#pragma once + +extern "C" { +#include "wayland-relative-pointer-client-protocol.h" +#include "wayland-xdg-shell-client-protocol.h" +} +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wolf::core::virtual_display { + +struct WClientState { // The trick here to use shared_ptr is so that it'll automatically call the destroy function + std::shared_ptr seat = {}; + std::shared_ptr compositor = {}; + std::shared_ptr shm = {}; + std::shared_ptr xwm_base = {}; + std::shared_ptr relative_pointer_manager = {}; + + std::shared_ptr surface = {}; + std::shared_ptr xsurface = {}; + + std::shared_ptr keyboard = {}; + std::shared_ptr pointer = {}; + std::shared_ptr relative_pointer = {}; +}; + +constexpr int WINDOW_WIDTH = 640; +constexpr int WINDOW_HEIGHT = 480; + +std::shared_ptr w_connect(std::shared_ptr w_state) { + auto display_name = utils::split(get_env(*w_state)[0], '=')[1]; + auto display = wl_display_connect(display_name.data()); + REQUIRE(display != nullptr); + return std::shared_ptr(display, &wl_display_disconnect); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = [](void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial) { + xdg_wm_base_pong(xdg_wm_base, serial); + }}; + +std::shared_ptr w_get_state(std::shared_ptr wd) { + struct wl_registry *registry = wl_display_get_registry(wd.get()); + struct wl_registry_listener listener = { + [](void *data, struct wl_registry *registry, uint32_t id, const char *interface, uint32_t version) { + logs::log(logs::debug, "Got registry event: id={}, interface={}, version={}", id, interface, version); + auto state = (WClientState *)data; + if (strcmp(interface, "wl_seat") == 0) { + state->seat = std::shared_ptr((wl_seat *)wl_registry_bind(registry, id, &wl_seat_interface, version), + &wl_seat_destroy); + } else if (strcmp(interface, "wl_compositor") == 0) { + state->compositor = std::shared_ptr( + (wl_compositor *)wl_registry_bind(registry, id, &wl_compositor_interface, version), + &wl_compositor_destroy); + } else if (strcmp(interface, "wl_shm") == 0) { + state->shm = std::shared_ptr((wl_shm *)wl_registry_bind(registry, id, &wl_shm_interface, version), + &wl_shm_destroy); + } else if (strcmp(interface, "xdg_wm_base") == 0) { + state->xwm_base = std::shared_ptr( + (xdg_wm_base *)wl_registry_bind(registry, id, &xdg_wm_base_interface, version), + &xdg_wm_base_destroy); + xdg_wm_base_add_listener(state->xwm_base.get(), &xdg_wm_base_listener, state); + } else if (strcmp(interface, "zwp_relative_pointer_manager_v1") == 0) { + state->relative_pointer_manager = std::shared_ptr( + (zwp_relative_pointer_manager_v1 *) + wl_registry_bind(registry, id, &zwp_relative_pointer_manager_v1_interface, version), + &zwp_relative_pointer_manager_v1_destroy); + } + }, + [](void *data, struct wl_registry *registry, uint32_t id) {}}; + + auto w_state = std::make_shared(); + wl_registry_add_listener(registry, &listener, w_state.get()); + wl_display_dispatch(wd.get()); + wl_display_roundtrip(wd.get()); + + REQUIRE(w_state->seat != nullptr); + REQUIRE(w_state->compositor != nullptr); + REQUIRE(w_state->shm != nullptr); + + return w_state; +} + +// SHM helpers, taken from: https://wayland-book.com/surfaces/shared-memory.html + +static void randname(char *buf) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + long r = ts.tv_nsec; + for (int i = 0; i < 6; ++i) { + buf[i] = 'A' + (r & 15) + (r & 16) * 2; + r >>= 5; + } +} + +static int create_shm_file(void) { + int retries = 100; + do { + char name[] = "/wl_shm-XXXXXX"; + randname(name + sizeof(name) - 7); + --retries; + int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + return fd; + } + } while (retries > 0 && errno == EEXIST); + return -1; +} + +int allocate_shm_file(size_t size) { + int fd = create_shm_file(); + if (fd < 0) + return -1; + int ret; + do { + ret = ftruncate(fd, size); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + close(fd); + return -1; + } + return fd; +} + +static const struct wl_buffer_listener wl_buffer_listener = { + .release = [](void *data, struct wl_buffer *wl_buffer) { wl_buffer_destroy(wl_buffer); }, +}; + +static struct wl_buffer *draw_frame(WClientState *state) { + int stride = WINDOW_WIDTH * 4; + int size = stride * WINDOW_HEIGHT; + + int fd = allocate_shm_file(size); + if (fd == -1) { + return NULL; + } + + uint32_t *data = static_cast(mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); + if (data == MAP_FAILED) { + close(fd); + return NULL; + } + + struct wl_shm_pool *pool = wl_shm_create_pool(state->shm.get(), fd, size); + struct wl_buffer *buffer = + wl_shm_pool_create_buffer(pool, 0, WINDOW_WIDTH, WINDOW_HEIGHT, stride, WL_SHM_FORMAT_XRGB8888); + wl_shm_pool_destroy(pool); + close(fd); + + /* Draw checkerboxed background */ + for (int y = 0; y < WINDOW_HEIGHT; ++y) { + for (int x = 0; x < WINDOW_WIDTH; ++x) { + if ((x + y / 8 * 8) % 16 < 8) + data[y * WINDOW_WIDTH + x] = 0xFF666666; + else + data[y * WINDOW_WIDTH + x] = 0xFFEEEEEE; + } + } + + munmap(data, size); + wl_buffer_add_listener(buffer, &wl_buffer_listener, NULL); + return buffer; +} + +static void commit_frame(WClientState *state) { + struct wl_buffer *buffer = draw_frame(state); + wl_surface_attach(state->surface.get(), buffer, 0, 0); + wl_surface_damage_buffer(state->surface.get(), 0, 0, INT32_MAX, INT32_MAX); + wl_surface_commit(state->surface.get()); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = + [](void *data, struct xdg_surface *xdg_surface, uint32_t serial) { + WClientState *state = static_cast(data); + xdg_surface_ack_configure(xdg_surface, serial); + commit_frame(state); + }, +}; + +void w_display_create_window(WClientState &w_state) { + auto surface = wl_compositor_create_surface(w_state.compositor.get()); + w_state.surface = std::shared_ptr(surface, &wl_surface_destroy); + + // xdg_surface + auto xdg_surface_ptr = xdg_wm_base_get_xdg_surface(w_state.xwm_base.get(), surface); + w_state.xsurface = std::shared_ptr(xdg_surface_ptr, &xdg_surface_destroy); + xdg_surface_add_listener(xdg_surface_ptr, &xdg_surface_listener, &w_state); + + auto xdg_toplevel = xdg_surface_get_toplevel(w_state.xsurface.get()); + xdg_toplevel_set_title(xdg_toplevel, "Wolf Wayland Client"); + xdg_toplevel_set_app_id(xdg_toplevel, "wolf-client"); + + wl_surface_commit(surface); +} + +// The "wl_array_for_each" C macro for C++ +// https://github.com/libretro/RetroArch/blob/a9125fffaa981cab811ba6caf4d756fa6ef9a561/input/common/wayland_common.h#L50-L53 +#define WL_ARRAY_FOR_EACH(pos, array, type) \ + for (pos = (type)(array)->data; (const char *)pos < ((const char *)(array)->data + (array)->size); (pos)++) + +struct KeyEvent { + uint32_t keycode; + bool pressed; +}; + +static const struct wl_keyboard_listener wl_keyboard_listener = { + .keymap = + [](void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size) { + logs::log(logs::debug, "[KEYBOARD] keymap event: format={}, fd={}, size={}", format, fd, size); + }, + .enter = + [](void *data, + struct wl_keyboard *wl_keyboard, + uint32_t serial, + struct wl_surface *surface, + struct wl_array *keys) { + logs::log(logs::debug, "[KEYBOARD] Got enter event: serial={}", serial); + auto queue = static_cast *>(data); + uint32_t *key; + WL_ARRAY_FOR_EACH(key, keys, unsigned int *) { + queue->push({*key, true}); + } + }, + .leave = + [](void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface) { + logs::log(logs::debug, "[KEYBOARD] Got leave event: serial={}", serial); + }, + .key = + [](void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { + logs::log(logs::debug, "[KEYBOARD] Got key event: time={}, key={}, state={}", time, key, state); + auto queue = static_cast *>(data); + queue->push({key, state == WL_KEYBOARD_KEY_STATE_PRESSED}); + }, + .modifiers = + [](void *data, + struct wl_keyboard *wl_keyboard, + uint32_t serial, + uint32_t mods_depressed, + uint32_t mods_latched, + uint32_t mods_locked, + uint32_t group) { + logs::log(logs::debug, + "[KEYBOARD] Got modifiers event: mods_depressed={}, mods_latched={}, mods_locked={}", + mods_depressed, + mods_latched, + mods_locked); + }, + .repeat_info = + [](void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) { + logs::log(logs::debug, "[KEYBOARD] Got repeat info event: rate={}, delay={}", rate, delay); + }}; + +std::shared_ptr> w_get_keyboard_queue(WClientState &w_state) { + auto w_kb = wl_seat_get_keyboard(w_state.seat.get()); + REQUIRE(w_kb != nullptr); + w_state.keyboard = std::shared_ptr(w_kb, &wl_keyboard_destroy); + auto queue = std::make_shared>(); + wl_keyboard_add_listener(w_kb, &wl_keyboard_listener, queue.get()); + return queue; +} + +enum MouseEventType { + ENTER, + LEAVE, + MOTION, + RELATIVE_MOTION, + BUTTON, + AXIS, + FRAME, + AXIS_SOURCE, + AXIS_STOP, + AXIS_DISCRETE, + AXIS_VALUE120 +}; +struct MouseEvent { + MouseEventType type; + + wl_fixed_t x; + wl_fixed_t y; + + uint32_t button; + bool button_pressed; + + uint32_t axis; + wl_fixed_t axis_value; +}; + +static const struct wl_pointer_listener wl_pointer_listener = { + .enter = + [](void *data, + struct wl_pointer *wl_pointer, + uint32_t serial, + struct wl_surface *surface, + wl_fixed_t surface_x, + wl_fixed_t surface_y) { + logs::log(logs::debug, "[MOUSE] Got mouse enter event: surface_x={}, surface_y={}", surface_x, surface_y); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::ENTER, .x = surface_x, .y = surface_y}); + // TODO: wl_pointer_set_cursor() here + }, + .leave = + [](void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { + logs::log(logs::debug, "[MOUSE] Got mouse leave event"); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::LEAVE}); + }, + .motion = + [](void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { + logs::log(logs::debug, "[MOUSE] Got mouse motion event: surface_x={}, surface_y={}", surface_x, surface_y); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::MOTION, .x = surface_x, .y = surface_y}); + }, + .button = + [](void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { + logs::log(logs::debug, "[MOUSE] Got mouse button event: button={}, state={}", button, state); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::BUTTON, + .button = button, + .button_pressed = state == WL_POINTER_BUTTON_STATE_PRESSED}); + }, + .axis = + [](void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { + logs::log(logs::debug, "[MOUSE] Got mouse axis event: axis={}, value={}", axis, value); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::AXIS, .axis = axis, .axis_value = value}); + }, + .frame = + [](void *data, struct wl_pointer *wl_pointer) { + logs::log(logs::debug, "[MOUSE] Got mouse frame event"); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::FRAME}); + }, + .axis_source = + [](void *data, struct wl_pointer *wl_pointer, uint32_t axis_source) { + logs::log(logs::debug, "[MOUSE] Got mouse axis source event: axis_source={}", axis_source); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::AXIS_SOURCE}); + }, + .axis_stop = + [](void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { + logs::log(logs::debug, "[MOUSE] Got mouse axis stop event: time={}, axis={}", time, axis); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::AXIS_STOP}); + }, + .axis_discrete = + [](void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { + logs::log(logs::debug, "[MOUSE] Got mouse axis discrete event: axis={}, discrete={}", axis, discrete); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::AXIS_DISCRETE}); + }, + .axis_value120 = + [](void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120) { + logs::log(logs::debug, "[MOUSE] Got mouse axis value120 event: axis={}, value120={}", axis, value120); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::AXIS_VALUE120}); + }}; + +static const struct zwp_relative_pointer_v1_listener zwp_relative_pointer_v1_listener = { + .relative_motion = [](void *data, + struct zwp_relative_pointer_v1 *zwp_relative_pointer_v1, + uint32_t utime_hi, + uint32_t utime_lo, + wl_fixed_t dx, + wl_fixed_t dy, + wl_fixed_t dx_unaccel, + wl_fixed_t dy_unaccel) { + logs::log(logs::debug, "[MOUSE] Got mouse relative motion event: dx={}, dy={}", dx, dy); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::MOTION, .x = dx, .y = dy}); + }}; + +std::shared_ptr> w_get_mouse_queue(WClientState &w_state) { + auto w_pointer = wl_seat_get_pointer(w_state.seat.get()); + REQUIRE(w_pointer != nullptr); + w_state.pointer = std::shared_ptr(w_pointer, &wl_pointer_destroy); + auto queue = std::make_shared>(); + wl_pointer_add_listener(w_pointer, &wl_pointer_listener, queue.get()); + + auto zwp_pointer = + zwp_relative_pointer_manager_v1_get_relative_pointer(w_state.relative_pointer_manager.get(), w_pointer); + w_state.relative_pointer = std::shared_ptr(zwp_pointer, &zwp_relative_pointer_v1_destroy); + zwp_relative_pointer_v1_add_listener(zwp_pointer, &zwp_relative_pointer_v1_listener, queue.get()); + + return queue; +} + +} // namespace wolf::core::virtual_display \ No newline at end of file diff --git a/tests/platforms/linux/wayland-display.cpp b/tests/platforms/linux/wayland-display.cpp index 8cc5dd7e..e6e4af55 100644 --- a/tests/platforms/linux/wayland-display.cpp +++ b/tests/platforms/linux/wayland-display.cpp @@ -1,8 +1,10 @@ +#include "wayland-client.hpp" #include -#include -#include #include #include +#include +#include +#include #include #include @@ -12,7 +14,7 @@ using Catch::Matchers::SizeIs; using namespace wolf::core::virtual_display; TEST_CASE("Wayland C APIs", "[WAYLAND]") { - auto w_state = create_wayland_display({"/dev/input/event0"}); + auto w_state = create_wayland_display({}); auto env_vars = get_env(*w_state); REQUIRE_THAT(env_vars, SizeIs(1)); @@ -48,4 +50,89 @@ TEST_CASE("Wayland C APIs", "[WAYLAND]") { gst_buffer_unref(gst_buffer); } +} + +using namespace moonlight::control; + +TEST_CASE("Wayland virtual inputs", "[WAYLAND]") { + auto w_state = create_wayland_display({}); + const auto FPS = 60; + set_resolution(*w_state, {WINDOW_WIDTH, WINDOW_HEIGHT, FPS}); + auto mouse = wolf::core::virtual_display::WaylandMouse(w_state); + auto keyboard = wolf::core::virtual_display::WaylandKeyboard(w_state); + auto session = state::StreamSession{.mouse = std::make_shared>(mouse), + .keyboard = std::make_shared>(keyboard)}; + + auto wd = w_connect(w_state); + auto w_objects = w_get_state(wd); + + w_display_create_window(*w_objects); + wl_display_roundtrip(wd.get()); + + auto mouse_events_q = w_get_mouse_queue(*w_objects); + auto kb_events_q = w_get_keyboard_queue(*w_objects); + wl_display_roundtrip(wd.get()); + + { // simulate the window being displayed + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / FPS)); + commit_frame(w_objects.get()); + wl_display_roundtrip(wd.get()); + } + + { // First move the mouse, this way our virtual window will get into focus + auto mv_packet = pkts::MOUSE_MOVE_REL_PACKET{.delta_x = 10, .delta_y = 20}; + mv_packet.type = pkts::MOUSE_MOVE_REL; + control::handle_input(session, {}, &mv_packet); + wl_display_roundtrip(wd.get()); + + auto m_ev = mouse_events_q->pop(); + REQUIRE(m_ev.has_value()); + REQUIRE(m_ev.value().type == MouseEventType::ENTER); + + m_ev = mouse_events_q->pop(); + REQUIRE(m_ev.has_value()); + REQUIRE(m_ev.value().type == MouseEventType::MOTION); + // TODO: why are dx=655360, dy=1310720 ??? + + m_ev = mouse_events_q->pop(); + REQUIRE(m_ev.has_value()); + REQUIRE(m_ev.value().type == MouseEventType::FRAME); + } + + // Keyboard tests + { + auto press_A_key = pkts::KEYBOARD_PACKET{.key_code = boost::endian::native_to_little((short)0x41)}; + press_A_key.type = pkts::KEY_PRESS; + control::handle_input(session, {}, &press_A_key); + wl_display_roundtrip(wd.get()); + + auto k_ev = kb_events_q->pop(); + REQUIRE(k_ev.has_value()); + REQUIRE(k_ev->keycode == 30); + REQUIRE(k_ev->pressed); + } + + { + auto release_A_key = pkts::KEYBOARD_PACKET{.key_code = boost::endian::native_to_little((short)0x41)}; + release_A_key.type = pkts::KEY_RELEASE; + control::handle_input(session, {}, &release_A_key); + wl_display_roundtrip(wd.get()); + + auto k_ev = kb_events_q->pop(); + REQUIRE(k_ev.has_value()); + REQUIRE(k_ev->keycode == 30); + REQUIRE(!k_ev->pressed); + } + + // Mouse tests: scroll + { + short scroll_amt = 10; + auto scroll_packet = pkts::MOUSE_SCROLL_PACKET{.scroll_amt1 = boost::endian::native_to_big(scroll_amt)}; + scroll_packet.type = pkts::MOUSE_SCROLL; + control::handle_input(session, {}, &scroll_packet); + wl_display_roundtrip(wd.get()); + + // TODO: seems that I don't get those events + // > interface 'wl_pointer' has no event 10 + } } \ No newline at end of file diff --git a/tests/testMoonlight.cpp b/tests/testMoonlight.cpp index 4dfc3a2b..e48f478e 100644 --- a/tests/testMoonlight.cpp +++ b/tests/testMoonlight.cpp @@ -18,7 +18,6 @@ using namespace state; using namespace ranges; TEST_CASE("LocalState load TOML", "[LocalState]") { - streaming::init(); // So that we can load encoders auto event_bus = std::make_shared(); auto state = state::load_or_default("config.v2.toml", event_bus); REQUIRE(state.hostname == "Wolf"); @@ -124,7 +123,6 @@ TEST_CASE("LocalState pairing information", "[LocalState]") { } TEST_CASE("Mocked serverinfo", "[MoonlightProtocol]") { - streaming::init(); // So that we can load encoders auto event_bus = std::make_shared(); auto cfg = state::load_or_default("config.v2.toml", event_bus); immer::array displayModes = {{1920, 1080, 60}, {1024, 768, 30}}; @@ -329,7 +327,6 @@ TEST_CASE("Pairing moonlight", "[MoonlightProtocol]") { } TEST_CASE("applist", "[MoonlightProtocol]") { - streaming::init(); // So that we can load encoders auto event_bus = std::make_shared(); auto cfg = state::load_or_default("config.v2.toml", event_bus); auto base_apps = cfg.apps | views::transform([](auto app) { return app.base; }) | to>(); @@ -342,7 +339,6 @@ TEST_CASE("applist", "[MoonlightProtocol]") { } TEST_CASE("launch", "[MoonlightProtocol]") { - streaming::init(); // So that we can load encoders auto event_bus = std::make_shared(); auto cfg = state::load_or_default("config.v2.toml", event_bus); auto result = launch_success("192.168.1.1", "3021"); From ade86cc80db32956ed77bd3de1ff3628e19d102e Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Tue, 23 Jul 2024 19:00:36 +0100 Subject: [PATCH 5/9] feat: implemented Moonlight keyboard modifiers --- src/moonlight-protocol/moonlight/control.hpp | 17 +++++++++++- .../control/input_handler.cpp | 22 ++++++++++++++++ tests/platforms/linux/wayland-display.cpp | 26 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/moonlight-protocol/moonlight/control.hpp b/src/moonlight-protocol/moonlight/control.hpp index bb05bad1..71212d7d 100644 --- a/src/moonlight-protocol/moonlight/control.hpp +++ b/src/moonlight-protocol/moonlight/control.hpp @@ -98,6 +98,21 @@ enum CONTROLLER_BTN : unsigned int { Y = 0x8000 }; +enum KEYBOARD_MODIFIERS : char { + NONE = 0x00, + SHIFT = 0x01, + CTRL = 0x02, + ALT = 0x04, + META = 0x08 +}; + +enum MOONLIGHT_MODIFIERS : short { + M_SHIFT = 0x10, + M_CTRL = 0x11, + M_ALT = 0xA4, + M_META = 0x5B +}; + // make sure these structs are allocated in 1-byte blocks so the data aligns // right #pragma pack(push, 1) @@ -141,7 +156,7 @@ struct MOUSE_HSCROLL_PACKET : INPUT_PKT { struct KEYBOARD_PACKET : INPUT_PKT { unsigned char flags; short key_code; - unsigned char modifiers; + char modifiers; short zero1; }; diff --git a/src/moonlight-server/control/input_handler.cpp b/src/moonlight-server/control/input_handler.cpp index 072e1f8b..34650f7d 100644 --- a/src/moonlight-server/control/input_handler.cpp +++ b/src/moonlight-server/control/input_handler.cpp @@ -298,7 +298,29 @@ void keyboard_key(const KEYBOARD_PACKET &pkt, state::StreamSession &session) { short moonlight_key = (short)boost::endian::little_to_native(pkt.key_code) & (short)0x7fff; if (session.keyboard->has_value()) { if (pkt.type == KEY_PRESS) { + // Press the virtual modifiers + if (pkt.modifiers & KEYBOARD_MODIFIERS::SHIFT && moonlight_key != M_SHIFT) + std::visit([](auto &keyboard) { keyboard.press(M_SHIFT); }, session.keyboard->value()); + if (pkt.modifiers & KEYBOARD_MODIFIERS::CTRL && moonlight_key != M_CTRL) + std::visit([](auto &keyboard) { keyboard.press(M_CTRL); }, session.keyboard->value()); + if (pkt.modifiers & KEYBOARD_MODIFIERS::ALT && moonlight_key != M_ALT) + std::visit([](auto &keyboard) { keyboard.press(M_ALT); }, session.keyboard->value()); + if (pkt.modifiers & KEYBOARD_MODIFIERS::META && moonlight_key != M_META) + std::visit([](auto &keyboard) { keyboard.press(M_META); }, session.keyboard->value()); + + // Press the actual key std::visit([moonlight_key](auto &keyboard) { keyboard.press(moonlight_key); }, session.keyboard->value()); + + // Release the virtual modifiers + if (pkt.modifiers & KEYBOARD_MODIFIERS::SHIFT && moonlight_key != M_SHIFT) + std::visit([](auto &keyboard) { keyboard.release(M_SHIFT); }, session.keyboard->value()); + if (pkt.modifiers & KEYBOARD_MODIFIERS::CTRL && moonlight_key != M_CTRL) + std::visit([](auto &keyboard) { keyboard.release(M_CTRL); }, session.keyboard->value()); + if (pkt.modifiers & KEYBOARD_MODIFIERS::ALT && moonlight_key != M_ALT) + std::visit([](auto &keyboard) { keyboard.release(M_ALT); }, session.keyboard->value()); + if (pkt.modifiers & KEYBOARD_MODIFIERS::META && moonlight_key != M_META) + std::visit([](auto &keyboard) { keyboard.release(M_META); }, session.keyboard->value()); + } else { std::visit([moonlight_key](auto &keyboard) { keyboard.release(moonlight_key); }, session.keyboard->value()); } diff --git a/tests/platforms/linux/wayland-display.cpp b/tests/platforms/linux/wayland-display.cpp index e6e4af55..fc04c2b5 100644 --- a/tests/platforms/linux/wayland-display.cpp +++ b/tests/platforms/linux/wayland-display.cpp @@ -124,6 +124,32 @@ TEST_CASE("Wayland virtual inputs", "[WAYLAND]") { REQUIRE(!k_ev->pressed); } + { // Testing modifiers + auto press_SHIFT_A = + pkts::KEYBOARD_PACKET{.key_code = boost::endian::native_to_little((short)0x41), .modifiers = pkts::SHIFT}; + press_SHIFT_A.type = pkts::KEY_PRESS; + control::handle_input(session, {}, &press_SHIFT_A); + wl_display_roundtrip(wd.get()); + + auto k_ev = kb_events_q->pop(); + // Press SHIFT + REQUIRE(k_ev.has_value()); + REQUIRE(k_ev->keycode == 42); + REQUIRE(k_ev->pressed); + + // Press A + k_ev = kb_events_q->pop(); + REQUIRE(k_ev.has_value()); + REQUIRE(k_ev->keycode == 30); + REQUIRE(k_ev->pressed); + + // Release SHIFT + k_ev = kb_events_q->pop(); + REQUIRE(k_ev.has_value()); + REQUIRE(k_ev->keycode == 42); + REQUIRE(!k_ev->pressed); + } + // Mouse tests: scroll { short scroll_amt = 10; From 0dbb7d568f4c519c742c16e581454fe2d235f62c Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Tue, 23 Jul 2024 19:42:05 +0100 Subject: [PATCH 6/9] CI: fix ARM Wayland tests --- .github/workflows/linux-build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux-build-test.yml b/.github/workflows/linux-build-test.yml index 7e9b73af..94da3913 100644 --- a/.github/workflows/linux-build-test.yml +++ b/.github/workflows/linux-build-test.yml @@ -27,7 +27,7 @@ jobs: libboost-locale-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libboost-stacktrace-dev \ libssl-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ - libwayland-dev libwayland-server0 libinput-dev libxkbcommon-dev libgbm-dev \ + libwayland-dev wayland-protocols libinput-dev libxkbcommon-dev libgbm-dev \ libcurl4-openssl-dev \ libpulse-dev \ libevdev-dev \ From eccf69fa04a6a8dd25914dab1e36f2adffd55e4e Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Tue, 23 Jul 2024 20:22:55 +0100 Subject: [PATCH 7/9] CI: removed missing `axis_value120` from ARM build --- tests/platforms/linux/wayland-client.hpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/platforms/linux/wayland-client.hpp b/tests/platforms/linux/wayland-client.hpp index f509e6ae..a1033f72 100644 --- a/tests/platforms/linux/wayland-client.hpp +++ b/tests/platforms/linux/wayland-client.hpp @@ -359,12 +359,6 @@ static const struct wl_pointer_listener wl_pointer_listener = { logs::log(logs::debug, "[MOUSE] Got mouse axis discrete event: axis={}, discrete={}", axis, discrete); auto queue = static_cast *>(data); queue->push({.type = MouseEventType::AXIS_DISCRETE}); - }, - .axis_value120 = - [](void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120) { - logs::log(logs::debug, "[MOUSE] Got mouse axis value120 event: axis={}, value120={}", axis, value120); - auto queue = static_cast *>(data); - queue->push({.type = MouseEventType::AXIS_VALUE120}); }}; static const struct zwp_relative_pointer_v1_listener zwp_relative_pointer_v1_listener = { From b5d11a9e88bb0e6e8d4c6b08256c38944a831ad8 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Tue, 23 Jul 2024 21:09:52 +0100 Subject: [PATCH 8/9] CI: ARM test --- tests/platforms/linux/wayland-client.hpp | 6 ++++++ tests/platforms/linux/wayland-display.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/platforms/linux/wayland-client.hpp b/tests/platforms/linux/wayland-client.hpp index a1033f72..f509e6ae 100644 --- a/tests/platforms/linux/wayland-client.hpp +++ b/tests/platforms/linux/wayland-client.hpp @@ -359,6 +359,12 @@ static const struct wl_pointer_listener wl_pointer_listener = { logs::log(logs::debug, "[MOUSE] Got mouse axis discrete event: axis={}, discrete={}", axis, discrete); auto queue = static_cast *>(data); queue->push({.type = MouseEventType::AXIS_DISCRETE}); + }, + .axis_value120 = + [](void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120) { + logs::log(logs::debug, "[MOUSE] Got mouse axis value120 event: axis={}, value120={}", axis, value120); + auto queue = static_cast *>(data); + queue->push({.type = MouseEventType::AXIS_VALUE120}); }}; static const struct zwp_relative_pointer_v1_listener zwp_relative_pointer_v1_listener = { diff --git a/tests/platforms/linux/wayland-display.cpp b/tests/platforms/linux/wayland-display.cpp index fc04c2b5..b6767e9a 100644 --- a/tests/platforms/linux/wayland-display.cpp +++ b/tests/platforms/linux/wayland-display.cpp @@ -147,7 +147,7 @@ TEST_CASE("Wayland virtual inputs", "[WAYLAND]") { k_ev = kb_events_q->pop(); REQUIRE(k_ev.has_value()); REQUIRE(k_ev->keycode == 42); - REQUIRE(!k_ev->pressed); + // REQUIRE(!k_ev->pressed); } // Mouse tests: scroll From f2d2b7ad6f5964d89221c09e8a7dc8bfa09757b6 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Tue, 23 Jul 2024 21:20:11 +0100 Subject: [PATCH 9/9] CI: ARM tests --- tests/platforms/linux/wayland-display.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/platforms/linux/wayland-display.cpp b/tests/platforms/linux/wayland-display.cpp index b6767e9a..0f61800e 100644 --- a/tests/platforms/linux/wayland-display.cpp +++ b/tests/platforms/linux/wayland-display.cpp @@ -147,7 +147,7 @@ TEST_CASE("Wayland virtual inputs", "[WAYLAND]") { k_ev = kb_events_q->pop(); REQUIRE(k_ev.has_value()); REQUIRE(k_ev->keycode == 42); - // REQUIRE(!k_ev->pressed); + REQUIRE(!k_ev->pressed); } // Mouse tests: scroll @@ -156,7 +156,7 @@ TEST_CASE("Wayland virtual inputs", "[WAYLAND]") { auto scroll_packet = pkts::MOUSE_SCROLL_PACKET{.scroll_amt1 = boost::endian::native_to_big(scroll_amt)}; scroll_packet.type = pkts::MOUSE_SCROLL; control::handle_input(session, {}, &scroll_packet); - wl_display_roundtrip(wd.get()); + // wl_display_roundtrip(wd.get()); // TODO: seems that I don't get those events // > interface 'wl_pointer' has no event 10