Skip to content

Commit

Permalink
feat: implemented virtual touch screen
Browse files Browse the repository at this point in the history
  • Loading branch information
ABeltramo committed Feb 9, 2024
1 parent aafed06 commit a131d7f
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 8 deletions.
33 changes: 33 additions & 0 deletions src/core/src/core/input.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,39 @@ class Trackpad : public VirtualDevice {
void set_left_btn(bool pressed);
};

/**
* A virtual touchscreen
*/
class TouchScreen : public VirtualDevice {
protected:
typedef struct TouchScreenState TouchScreenState;

private:
std::shared_ptr<TouchScreenState> _state;

public:
TouchScreen();
TouchScreen(const TouchScreen &j) : _state(j._state) {}
TouchScreen(TouchScreen &&j) : _state(std::move(j._state)) {}
~TouchScreen() override;

std::vector<std::string> get_nodes() const override;

std::vector<std::map<std::string, std::string>> get_udev_events() const override;
std::vector<std::pair<std::string, std::vector<std::string>>> get_udev_hw_db_entries() const override;

/**
* We expect (x,y) to be in the range [0.0, 1.0]; x and y values are normalised device coordinates
* from the top-left corner (0.0, 0.0) to bottom-right corner (1.0, 1.0)
*
* @param finger_nr
* @param pressure A value between 0 and 1
*/
void place_finger(int finger_nr, float x, float y, float pressure);

void release_finger(int finger_nr);
};

/**
* A virtual pen tablet
*
Expand Down
1 change: 1 addition & 0 deletions src/core/src/platforms/linux/uinput/pentablet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ std::optional<libevdev_uinput *> create_tablet() {
libevdev_enable_event_code(dev, EV_ABS, ABS_TILT_X, &abs_tilt);
libevdev_enable_event_code(dev, EV_ABS, ABS_TILT_Y, &abs_tilt);

// https://docs.kernel.org/input/event-codes.html#tablets
libevdev_enable_property(dev, INPUT_PROP_POINTER);
libevdev_enable_property(dev, INPUT_PROP_DIRECT);
auto err = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uidev);
Expand Down
180 changes: 180 additions & 0 deletions src/core/src/platforms/linux/uinput/touchscreen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#include "uinput.hpp"
#include <core/input.hpp>
#include <linux/input.h>

namespace wolf::core::input {

struct TouchScreenState {
libevdev_uinput_ptr touch_screen = nullptr;

/**
* Multi touch protocol type B is stateful; see: https://docs.kernel.org/input/multi-touch-protocol.html
* Slots are numbered starting from 0 up to <number of currently connected fingers> (max: 4)
*
* The way it works:
* - first time a new finger_id arrives we'll create a new slot and call MT_TRACKING_ID = slot_number
* - we can keep updating ABS_X and ABS_Y as long as the finger_id stays the same
* - if we want to update a different finger we'll have to call ABS_MT_SLOT = slot_number
* - when a finger is released we'll call ABS_MT_SLOT = slot_number && MT_TRACKING_ID = -1
*
* The other thing that needs to be kept in sync is the EV_KEY.
* EX: enabling BTN_TOOL_DOUBLETAP will result in scrolling instead of moving the mouse
*/
/* The MT_SLOT we are currently updating */
int current_slot = -1;
/* A map of finger_id to MT_SLOT */
std::map<int /* finger_id */, int /* MT_SLOT */> fingers;
};

std::vector<std::string> TouchScreen::get_nodes() const {
std::vector<std::string> nodes;

if (auto kb = _state->touch_screen.get()) {
nodes.emplace_back(libevdev_uinput_get_devnode(kb));
}

return nodes;
}

std::vector<std::map<std::string, std::string>> TouchScreen::get_udev_events() const {
std::vector<std::map<std::string, std::string>> events;

if (_state->touch_screen.get()) {
auto event = gen_udev_base_event(_state->touch_screen);
event["ID_INPUT_TOUCHSCREEN"] = "1";
events.emplace_back(event);
}

return events;
}

std::vector<std::pair<std::string, std::vector<std::string>>> TouchScreen::get_udev_hw_db_entries() const {
std::vector<std::pair<std::string, std::vector<std::string>>> result;

if (_state->touch_screen.get()) {
result.push_back({gen_udev_hw_db_filename(_state->touch_screen),
{"E:ID_INPUT=1",
"E:ID_INPUT_TOUCHSCREEN=1",
"E:ID_BUS=usb",
"G:seat",
"G:uaccess",
"Q:seat",
"Q:uaccess",
"V:1"}});
}

return result;
}

static constexpr int TOUCH_MAX_X = 19200;
static constexpr int TOUCH_MAX_Y = 10800;
static constexpr int NUM_FINGERS = 16;
static constexpr int PRESSURE_MAX = 253;

std::optional<libevdev_uinput *> create_touch_screen() {
libevdev *dev = libevdev_new();
libevdev_uinput *uidev;

libevdev_set_name(dev, "Wolf (virtual) touch screen");
libevdev_set_id_version(dev, 0xAB00);

libevdev_set_id_product(dev, 0xAB01);
libevdev_set_id_version(dev, 0xAB00);
libevdev_set_id_bustype(dev, BUS_USB);

libevdev_enable_event_type(dev, EV_KEY);
libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, nullptr);
libevdev_enable_event_code(dev, EV_KEY, BTN_TOUCH, nullptr);

libevdev_enable_event_type(dev, EV_ABS);
input_absinfo mt_slot{0, 0, NUM_FINGERS - 1, 0, 0, 0};
libevdev_enable_event_code(dev, EV_ABS, ABS_MT_SLOT, &mt_slot);

input_absinfo abs_x{0, 0, TOUCH_MAX_X, 0, 0, 0};
libevdev_enable_event_code(dev, EV_ABS, ABS_X, &abs_x);
libevdev_enable_event_code(dev, EV_ABS, ABS_MT_POSITION_X, &abs_x);

input_absinfo abs_y{0, 0, TOUCH_MAX_Y, 0, 0, 0};
libevdev_enable_event_code(dev, EV_ABS, ABS_Y, &abs_y);
libevdev_enable_event_code(dev, EV_ABS, ABS_MT_POSITION_Y, &abs_y);

input_absinfo tracking{0, 0, 65535, 0, 0, 0};
libevdev_enable_event_code(dev, EV_ABS, ABS_MT_TRACKING_ID, &tracking);

input_absinfo abs_pressure{0, 0, PRESSURE_MAX, 0, 0, 0};
libevdev_enable_event_code(dev, EV_ABS, ABS_PRESSURE, &abs_pressure);
libevdev_enable_event_code(dev, EV_ABS, ABS_MT_PRESSURE, &abs_pressure);
// TODO:
// input_absinfo touch{0, 0, TOUCH_MAX, 4, 0, 0};
// libevdev_enable_event_code(dev, EV_ABS, ABS_MT_TOUCH_MAJOR, &touch);
// libevdev_enable_event_code(dev, EV_ABS, ABS_MT_TOUCH_MINOR, &touch);
// input_absinfo orientation{0, -3, 4, 0, 0, 0};
// libevdev_enable_event_code(dev, EV_ABS, ABS_MT_ORIENTATION, &orientation);

// https://docs.kernel.org/input/event-codes.html#touchscreens
libevdev_enable_property(dev, INPUT_PROP_DIRECT);

auto err = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uidev);
if (err != 0) {
logs::log(logs::error, "Unable to create touch screen device, error code: {}", strerror(-err));
return {};
}

logs::log(logs::debug, "[INPUT] Created touch screen touchpad {}", libevdev_uinput_get_devnode(uidev));

return uidev;
}

TouchScreen::TouchScreen() {
this->_state = std::make_shared<TouchScreenState>();
if (auto touch_screen = create_touch_screen()) {
this->_state->touch_screen = {*touch_screen, ::libevdev_uinput_destroy};
}
}
TouchScreen::~TouchScreen() {}
void TouchScreen::place_finger(int finger_nr, float x, float y, float pressure) {
if (auto ts = this->_state->touch_screen.get()) {
int scaled_x = (int)std::lround(TOUCH_MAX_X * x);
int scaled_y = (int)std::lround(TOUCH_MAX_Y * y);

if (_state->fingers.find(finger_nr) == _state->fingers.end()) {
// Wow, a wild finger appeared!
auto finger_slot = _state->fingers.size() + 1;
_state->fingers[finger_nr] = finger_slot;
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_SLOT, finger_slot);
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_TRACKING_ID, finger_slot);
} else {
// I already know this finger, let's check the slot
auto finger_slot = _state->fingers[finger_nr];
if (_state->current_slot != finger_slot) {
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_SLOT, finger_slot);
_state->current_slot = finger_slot;
}
}

libevdev_uinput_write_event(ts, EV_ABS, ABS_X, scaled_x);
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_POSITION_X, scaled_x);
libevdev_uinput_write_event(ts, EV_ABS, ABS_Y, scaled_y);
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_POSITION_Y, scaled_y);
libevdev_uinput_write_event(ts, EV_ABS, ABS_PRESSURE, (int) std::lround(pressure * PRESSURE_MAX));
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_PRESSURE, (int) std::lround(pressure * PRESSURE_MAX));

libevdev_uinput_write_event(ts, EV_SYN, SYN_REPORT, 0);
}
}

void TouchScreen::release_finger(int finger_nr) {
if (auto ts = this->_state->touch_screen.get()) {
auto finger_slot = _state->fingers[finger_nr];
if (_state->current_slot != finger_slot) {
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_SLOT, finger_slot);
_state->current_slot = -1;
}
_state->fingers.erase(finger_nr);
libevdev_uinput_write_event(ts, EV_ABS, ABS_MT_TRACKING_ID, -1);

libevdev_uinput_write_event(ts, EV_SYN, SYN_REPORT, 0);
}
}

} // namespace wolf::core::input
8 changes: 4 additions & 4 deletions src/core/src/platforms/linux/uinput/trackpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ std::optional<libevdev_uinput *> create_trackpad() {
input_absinfo abs_pressure{0, 0, PRESSURE_MAX, 0, 0, 0};
libevdev_enable_event_code(dev, EV_ABS, ABS_PRESSURE, &abs_pressure);
libevdev_enable_event_code(dev, EV_ABS, ABS_MT_PRESSURE, &abs_pressure);
//
// TODO:
// input_absinfo touch{0, 0, TOUCH_MAX, 4, 0, 0};
// libevdev_enable_event_code(dev, EV_ABS, ABS_MT_TOUCH_MAJOR, &touch);
// libevdev_enable_event_code(dev, EV_ABS, ABS_MT_TOUCH_MINOR, &touch);

// input_absinfo orientation{0, -3, 4, 0, 0, 0};
// libevdev_enable_event_code(dev, EV_ABS, ABS_MT_ORIENTATION, &orientation);

// https://docs.kernel.org/input/event-codes.html#trackpads
libevdev_enable_property(dev, INPUT_PROP_POINTER);
libevdev_enable_property(dev, INPUT_PROP_BUTTONPAD);

Expand Down Expand Up @@ -186,8 +186,8 @@ void Trackpad::place_finger(int finger_nr, float x, float y, float pressure) {
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_MT_POSITION_X, scaled_x);
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_Y, scaled_y);
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_MT_POSITION_Y, scaled_y);
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_PRESSURE, pressure * PRESSURE_MAX);
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_MT_PRESSURE, pressure * PRESSURE_MAX);
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_PRESSURE, (int) std::lround(pressure * PRESSURE_MAX));
libevdev_uinput_write_event(touchpad, EV_ABS, ABS_MT_PRESSURE, (int) std::lround(pressure * PRESSURE_MAX));

libevdev_uinput_write_event(touchpad, EV_SYN, SYN_REPORT, 0);
}
Expand Down
39 changes: 38 additions & 1 deletion src/moonlight-server/control/input_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ std::shared_ptr<PenTablet> create_pen_tablet(state::StreamSession &session) {
return tablet;
}

/**
* Creates a new Touch screen and saves it into the session;
* will also trigger a PlugDeviceEvent
*/
std::shared_ptr<TouchScreen> create_touch_screen(state::StreamSession &session) {
logs::log(logs::debug, "[INPUT] Creating new touch screen");
auto touch_screen = std::make_shared<TouchScreen>();
session.event_bus->fire_event(immer::box<state::PlugDeviceEvent>(
state::PlugDeviceEvent{.session_id = session.session_id, .device = touch_screen}));
session.touch_screen = touch_screen;
return touch_screen;
}

float netfloat_to_0_1(const utils::netfloat &f) {
return std::clamp(utils::from_netfloat(f), 0.0f, 1.0f);
}
Expand Down Expand Up @@ -179,9 +192,33 @@ void handle_input(state::StreamSession &session,
session.keyboard->paste_utf(utf32);
break;
}
case TOUCH:
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<TOUCH_PACKET *>(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:
session.touch_screen->place_finger(finger_id, x, y, pressure_or_distance);
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);
}
break;
}
case PEN: {
logs::log(logs::trace, "[INPUT] Received input of type: PEN");
if (!session.pen_tablet) {
Expand Down
3 changes: 2 additions & 1 deletion src/moonlight-server/state/data-structures.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ struct StreamSession {
std::shared_ptr<input::Mouse> mouse;
std::shared_ptr<input::Keyboard> keyboard;
std::shared_ptr<immer::atom<JoypadList>> joypads;
std::shared_ptr<input::PenTablet> pen_tablet = nullptr; /* Optional, will be set on first use*/
std::shared_ptr<input::PenTablet> pen_tablet = nullptr; /* Optional, will be set on first use*/
std::shared_ptr<input::TouchScreen> touch_screen = nullptr; /* Optional, will be set on first use*/
};

struct PlugDeviceEvent {
Expand Down
50 changes: 49 additions & 1 deletion tests/platforms/linux/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ TEST_CASE("uinput - keyboard", "UINPUT") {
}

TEST_CASE("uinput - pen tablet", "[UINPUT]") {
libevdev_ptr tablet_dev(libevdev_new(), ::libevdev_free);
auto session = state::StreamSession{.pen_tablet = std::make_shared<PenTablet>()};
auto li = create_libinput_context(session.pen_tablet->get_nodes());
auto event = get_event(li);
Expand Down Expand Up @@ -124,6 +123,55 @@ TEST_CASE("uinput - pen tablet", "[UINPUT]") {
}
}

TEST_CASE("uinput - touch screen", "[UINPUT]") {
auto session = state::StreamSession{.touch_screen = std::make_shared<TouchScreen>()};
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);
REQUIRE(libinput_device_has_capability(libinput_event_get_device(event.get()), LIBINPUT_DEVICE_CAP_TOUCH));

auto packet = pkts::TOUCH_PACKET{
.event_type = pkts::TOUCH_EVENT_HOVER,
.pointer_id = boost::endian::native_to_little(0),
.x = {0, 0, 0, 63}, // 0.5 in float (little endian)
.y = {0, 0, 0, 63}, // 0.5 in float (little endian)
.pressure_or_distance = {0, 0, 0, 63}, // 0.5 in float (little endian)
};
packet.type = pkts::TOUCH;

control::handle_input(session, {}, &packet);

float TARGET_W = 1920;
float TARGET_H = 1080;
{
event = get_event(li);
REQUIRE(libinput_event_get_type(event.get()) == LIBINPUT_EVENT_TOUCH_DOWN);
auto t_event = libinput_event_get_touch_event(event.get());
REQUIRE(libinput_event_touch_get_slot(t_event) == 1);
REQUIRE_THAT(libinput_event_touch_get_x_transformed(t_event, TARGET_W), WithinRel(TARGET_W * 0.5f, 0.5f));
REQUIRE_THAT(libinput_event_touch_get_y_transformed(t_event, TARGET_H), WithinRel(TARGET_H * 0.5f, 0.5f));
event = get_event(li);
REQUIRE(libinput_event_get_type(event.get()) == LIBINPUT_EVENT_TOUCH_FRAME);
}

packet = pkts::TOUCH_PACKET{
.event_type = pkts::TOUCH_EVENT_UP,
.pointer_id = boost::endian::native_to_little(0),
};
packet.type = pkts::TOUCH;

control::handle_input(session, {}, &packet);

{
event = get_event(li);
REQUIRE(libinput_event_get_type(event.get()) == LIBINPUT_EVENT_TOUCH_UP);
auto t_event = libinput_event_get_touch_event(event.get());
REQUIRE(libinput_event_touch_get_slot(t_event) == 1);
event = get_event(li);
REQUIRE(libinput_event_get_type(event.get()) == LIBINPUT_EVENT_TOUCH_FRAME);
}
}

TEST_CASE("uinput - mouse", "UINPUT") {
libevdev_ptr mouse_rel_dev(libevdev_new(), ::libevdev_free);
libevdev_ptr mouse_abs_dev(libevdev_new(), ::libevdev_free);
Expand Down
Loading

0 comments on commit a131d7f

Please sign in to comment.