From dad446ea41bbbae24e6f05fe98892568a92bea78 Mon Sep 17 00:00:00 2001 From: loki Date: Wed, 21 Jul 2021 20:24:23 +0200 Subject: [PATCH] Print request for rumble on the terminal for Linux --- sunshine/input.cpp | 14 +- sunshine/input.h | 2 +- sunshine/main.cpp | 35 +- sunshine/platform/linux/input.cpp | 537 +++++++++++++++++++++++++++++- sunshine/thread_safe.h | 2 +- 5 files changed, 564 insertions(+), 26 deletions(-) diff --git a/sunshine/input.cpp b/sunshine/input.cpp index 09ddbe2f..d04c8b14 100644 --- a/sunshine/input.cpp +++ b/sunshine/input.cpp @@ -150,7 +150,8 @@ void print(PNV_KEYBOARD_PACKET packet) { } void print(PNV_MULTI_CONTROLLER_PACKET packet) { - BOOST_LOG(debug) + // Moonlight spams controller packet even when not necessary + BOOST_LOG(verbose) << "--begin controller packet--"sv << std::endl << "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl << "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl @@ -575,8 +576,17 @@ void reset(std::shared_ptr &input) { }); } -void init() { +class deinit_t : public platf::deinit_t { +public: + ~deinit_t() override { + platf_input.reset(); + } +}; + +[[nodiscard]] std::unique_ptr init() { platf_input = platf::input(); + + return std::make_unique(); } std::shared_ptr alloc(safe::mail_t mail) { diff --git a/sunshine/input.h b/sunshine/input.h index e45cbc75..c9c0590e 100644 --- a/sunshine/input.h +++ b/sunshine/input.h @@ -18,7 +18,7 @@ void reset(std::shared_ptr &input); void passthrough(std::shared_ptr &input, std::vector &&input_data); -void init(); +[[nodiscard]] std::unique_ptr init(); std::shared_ptr alloc(safe::mail_t mail); diff --git a/sunshine/main.cpp b/sunshine/main.cpp index 59e010e8..d8d18f54 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -115,6 +115,23 @@ std::map(); if(config::parse(argc, argv)) { @@ -189,9 +206,6 @@ int main(int argc, char *argv[]) { task_pool.start(1); - bool shutdown_by_interrupt = false; - - util::TaskPool::task_id_t force_shutdown = nullptr; // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); on_signal(SIGINT, [&shutdown_by_interrupt, &force_shutdown, shutdown_event]() { @@ -221,19 +235,6 @@ int main(int argc, char *argv[]) { shutdown_event->raise(true); }); - auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() { - if(!shutdown_by_interrupt) { - return; - } - - task_pool.cancel(force_shutdown); - - std::cout << "Sunshine exited: Press enter to continue"sv << std::endl; - - std::string _; - std::getline(std::cin, _); - }); - proc::refresh(config::stream.file_apps); auto deinit_guard = platf::init(); @@ -242,7 +243,7 @@ int main(int argc, char *argv[]) { } reed_solomon_init(); - input::init(); + auto input_deinit_guard = input::init(); if(video::init()) { return 2; } diff --git a/sunshine/platform/linux/input.cpp b/sunshine/platform/linux/input.cpp index 78e08024..1edb2777 100644 --- a/sunshine/platform/linux/input.cpp +++ b/sunshine/platform/linux/input.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include #include @@ -23,18 +27,344 @@ #define REL_WHEEL_HI_RES 0x0b #endif -namespace platf { using namespace std::literals; +namespace platf { +constexpr auto mail_evdev = "platf::evdev"sv; + using evdev_t = util::safe_ptr; using uinput_t = util::safe_ptr; using keyboard_t = util::safe_ptr_v2; +constexpr pollfd read_pollfd { -1, 0, 0 }; +KITTY_USING_MOVE_T(pollfd_t, pollfd, read_pollfd, { + if(el.fd >= 0) { + ioctl(el.fd, EVIOCGRAB, (void *)0); + + close(el.fd); + } +}); + +using mail_evdev_t = std::tuple; + constexpr touch_port_t target_touch_port { 0, 0, 19200, 12000 }; +static std::pair operator*(const std::pair &l, int r) { + return { + l.first * r, + l.second * r, + }; +} + +static std::pair operator/(const std::pair &l, int r) { + return { + l.first / r, + l.second / r, + }; +} + +static std::pair &operator+=(std::pair &l, const std::pair &r) { + l.first += r.first; + l.second += r.second; + + return l; +} + +static inline void print(const ff_envelope &envelope) { + BOOST_LOG(debug) + << "Envelope:"sv << std::endl + << " attack_length: " << envelope.attack_length << std::endl + << " attack_level: " << envelope.attack_level << std::endl + << " fade_length: " << envelope.fade_length << std::endl + << " fade_level: " << envelope.fade_level; +} + +static inline void print(const ff_replay &replay) { + BOOST_LOG(debug) + << "Replay:"sv << std::endl + << " length: "sv << replay.length << std::endl + << " delay: "sv << replay.delay; +} + +static inline void print(const ff_trigger &trigger) { + BOOST_LOG(debug) + << "Trigger:"sv << std::endl + << " button: "sv << trigger.button << std::endl + << " interval: "sv << trigger.interval; +} + +static inline void print(const ff_effect &effect) { + BOOST_LOG(debug) + << std::endl + << std::endl + << "Received rumble effect with id: ["sv << effect.id << ']'; + + switch(effect.type) { + case FF_CONSTANT: + BOOST_LOG(debug) + << "FF_CONSTANT:"sv << std::endl + << " direction: "sv << effect.direction << std::endl + << " level: "sv << effect.u.constant.level; + + print(effect.u.constant.envelope); + break; + + case FF_PERIODIC: + BOOST_LOG(debug) + << "FF_CONSTANT:"sv << std::endl + << " direction: "sv << effect.direction << std::endl + << " waveform: "sv << effect.u.periodic.waveform << std::endl + << " period: "sv << effect.u.periodic.period << std::endl + << " magnitude: "sv << effect.u.periodic.magnitude << std::endl + << " offset: "sv << effect.u.periodic.offset << std::endl + << " phase: "sv << effect.u.periodic.phase; + + print(effect.u.periodic.envelope); + break; + + case FF_RAMP: + BOOST_LOG(debug) + << "FF_RAMP:"sv << std::endl + << " direction: "sv << effect.direction << std::endl + << " start_level:" << effect.u.ramp.start_level << std::endl + << " end_level:" << effect.u.ramp.end_level; + + print(effect.u.ramp.envelope); + break; + + case FF_RUMBLE: + BOOST_LOG(debug) + << "FF_RUMBLE:" << std::endl + << " direction: "sv << effect.direction << std::endl + << " strong_magnitude: " << effect.u.rumble.strong_magnitude << std::endl + << " weak_magnitude: " << effect.u.rumble.weak_magnitude; + break; + + + case FF_SPRING: + BOOST_LOG(debug) + << "FF_SPRING:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_FRICTION: + BOOST_LOG(debug) + << "FF_FRICTION:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_DAMPER: + BOOST_LOG(debug) + << "FF_DAMPER:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_INERTIA: + BOOST_LOG(debug) + << "FF_INERTIA:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_CUSTOM: + BOOST_LOG(debug) + << "FF_CUSTOM:" << std::endl + << " direction: "sv << effect.direction; + break; + + default: + BOOST_LOG(debug) + << "FF_UNKNOWN:" << std::endl + << " direction: "sv << effect.direction; + break; + } + + print(effect.replay); + print(effect.trigger); +} + +// Emulate rumble effects +class effect_t { +public: + KITTY_DEFAULT_CONSTR_MOVE(effect_t) + + effect_t(uinput_t::pointer dev, rumble_queue_t &&q) + : dev { dev }, rumble_queue { std::move(q) }, gain { 0xFFFFFF }, id_to_data {} {} + + class data_t { + public: + KITTY_DEFAULT_CONSTR(data_t) + + data_t(const ff_effect &effect) + : delay { effect.replay.delay }, + length { effect.replay.length }, + end_point {}, + envelope {}, + start {}, + end {} { + + switch(effect.type) { + case FF_CONSTANT: + start.weak = effect.u.constant.level; + start.strong = effect.u.constant.level; + end.weak = effect.u.constant.level; + end.strong = effect.u.constant.level; + + envelope = effect.u.constant.envelope; + break; + case FF_PERIODIC: + start.weak = effect.u.periodic.magnitude; + start.strong = effect.u.periodic.magnitude; + end.weak = effect.u.periodic.magnitude; + end.strong = effect.u.periodic.magnitude; + + envelope = effect.u.periodic.envelope; + break; + + case FF_RAMP: + start.weak = effect.u.ramp.start_level; + start.strong = effect.u.ramp.start_level; + end.weak = effect.u.ramp.end_level; + end.strong = effect.u.ramp.end_level; + + envelope = effect.u.ramp.envelope; + break; + + case FF_RUMBLE: + start.weak = effect.u.rumble.weak_magnitude; + start.strong = effect.u.rumble.strong_magnitude; + end.weak = effect.u.rumble.weak_magnitude; + end.strong = effect.u.rumble.strong_magnitude; + break; + + default: + BOOST_LOG(warning) << "Effect type ["sv << effect.id << "] not implemented"sv; + } + } + + int magnitude(std::chrono::milliseconds time_left, int start, int end) { + auto rel = end - start; + + return start + (rel * time_left.count() / length.count()); + } + + std::pair rumble(std::chrono::steady_clock::time_point tp) { + if(end_point < tp) { + return {}; + } + + auto time_left = + std::chrono::duration_cast( + end_point - tp); + + // If it needs to be delayed' + if(time_left > length) { + return {}; + } + + auto t = length - time_left; + + auto weak = magnitude(t, start.weak, end.weak); + auto strong = magnitude(t, start.strong, end.strong); + + if(t.count() < envelope.attack_length) { + weak = (envelope.attack_level * t.count() + weak * (envelope.attack_length - t.count())) / envelope.attack_length; + strong = (envelope.attack_level * t.count() + strong * (envelope.attack_length - t.count())) / envelope.attack_length; + } + else if(time_left.count() < envelope.fade_length) { + auto dt = (t - length).count() + envelope.fade_length; + + weak = (envelope.fade_level * dt + weak * (envelope.fade_length - dt)) / envelope.fade_length; + strong = (envelope.fade_level * dt + strong * (envelope.fade_length - dt)) / envelope.fade_length; + } + + return { + weak, strong + }; + } + + std::chrono::milliseconds delay; + std::chrono::milliseconds length; + + std::chrono::steady_clock::time_point end_point; + + ff_envelope envelope; + struct { + int weak, strong; + } start; + + struct { + int weak, strong; + } end; + }; + + std::pair rumble(std::chrono::steady_clock::time_point tp) { + std::pair weak_strong {}; + for(auto &[_, data] : id_to_data) { + weak_strong += data.rumble(tp); + } + + std::clamp(weak_strong.first, 0, 0xFFFFFF); + std::clamp(weak_strong.second, 0, 0xFFFFFF); + + return weak_strong * gain / 0xFFFF; + } + + void upload(const ff_effect &effect) { + print(effect); + + auto it = id_to_data.find(effect.id); + + if(it == std::end(id_to_data)) { + id_to_data.emplace(effect.id, effect); + return; + } + + data_t data { effect }; + + data.end_point = it->second.end_point; + it->second = data; + } + + void erase(int id) { + id_to_data.erase(id); + BOOST_LOG(debug) << "Removed rumble effect id ["sv << id << ']'; + } + + // Used as ID for rumble notifications + uinput_t::pointer dev; + rumble_queue_t rumble_queue; + + int gain; + + std::unordered_map id_to_data; +}; + +struct rumble_ctx_t { + std::thread rumble_thread; + + safe::queue_t rumble_queue_queue; +}; + +void broadcastRumble(safe::queue_t &ctx); +int startRumble(rumble_ctx_t &ctx) { + ctx.rumble_thread = std::thread { broadcastRumble, std::ref(ctx.rumble_queue_queue) }; + + return 0; +} + +void stopRumble(rumble_ctx_t &ctx) { + ctx.rumble_queue_queue.stop(); + + BOOST_LOG(debug) << "Waiting for Gamepad notifications to stop..."sv; + ctx.rumble_thread.join(); + BOOST_LOG(debug) << "Gamepad notifications stopped"sv; +} + +static auto notifications = safe::make_shared(startRumble, stopRumble); + struct input_raw_t { public: void clear_touchscreen() { @@ -57,6 +387,11 @@ public: } void clear_gamepad(int nr) { + auto &[dev, _] = gamepads[nr]; + + // Remove this gamepad from notifications + rumble_ctx->rumble_queue_queue.raise(dev.get(), nullptr, pollfd {}); + std::stringstream ss; ss << "sunshine_gamepad_"sv << nr; @@ -95,7 +430,7 @@ public: return 0; } - int alloc_gamepad(int nr) { + int alloc_gamepad(int nr, rumble_queue_t &&rumble_queue) { TUPLE_2D_REF(input, gamepad_state, gamepads[nr]); int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input); @@ -115,7 +450,18 @@ public: std::filesystem::remove(gamepad_path); } - std::filesystem::create_symlink(libevdev_uinput_get_devnode(input.get()), gamepad_path); + auto dev_node = libevdev_uinput_get_devnode(input.get()); + + rumble_ctx->rumble_queue_queue.raise( + input.get(), + std::move(rumble_queue), + pollfd_t { + dup(libevdev_uinput_get_fd(input.get())), + (std::int16_t)POLLIN, + (std::int16_t)0, + }); + + std::filesystem::create_symlink(dev_node, gamepad_path); return 0; } @@ -131,6 +477,8 @@ public: clear(); } + safe::shared_t::ptr_t rumble_ctx; + std::vector> gamepads; uinput_t mouse_input; uinput_t touch_input; @@ -142,6 +490,170 @@ public: keyboard_t keyboard; }; +inline void rumbleIterate(std::vector &effects, std::vector &polls, std::chrono::milliseconds to) { + auto res = poll(&polls.data()->el, polls.size(), to.count()); + + // If timed out + if(!res) { + return; + } + + if(res < 0) { + char err_str[1024]; + BOOST_LOG(error) << "Couldn't poll Gamepad file descriptors: "sv << strerror_r(errno, err_str, 1024); + + return; + } + + for(int x = 0; x < polls.size(); ++x) { + auto poll = std::begin(polls) + x; + auto effect_it = std::begin(effects) + x; + + auto fd = (*poll)->fd; + + // TUPLE_2D_REF(dev, q, *dev_q_it); + + // on error + if((*poll)->revents & (POLLHUP | POLLRDHUP | POLLERR)) { + BOOST_LOG(warning) << "Gamepad ["sv << x << "] file discriptor closed unexpectedly"sv; + + polls.erase(poll); + effects.erase(effect_it); + + continue; + } + + if(!((*poll)->revents & POLLIN)) { + continue; + } + + input_event events[64]; + + // Read all available events + auto bytes = read(fd, &events, sizeof(events)); + + if(bytes < 0) { + char err_str[1024]; + + BOOST_LOG(error) << "Couldn't read evdev input ["sv << errno << "]: "sv << strerror_r(errno, err_str, 1024); + + polls.erase(poll); + effects.erase(effect_it); + + continue; + } + + if(bytes < sizeof(input_event)) { + BOOST_LOG(warning) << "Reading evdev input: Expected at least "sv << sizeof(input_event) << " bytes, got "sv << bytes << " instead"sv; + continue; + } + + auto event_count = bytes / sizeof(input_event); + + for(auto event = events; event != (events + event_count); ++event) { + switch(event->type) { + case EV_FF: + BOOST_LOG(debug) << "EV_FF: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); + break; + case EV_UINPUT: + switch(event->code) { + case UI_FF_UPLOAD: { + uinput_ff_upload upload {}; + + // *VERY* important, without this you break + // the kernel and have to reboot due to dead + // hanging process + upload.request_id = event->value; + + ioctl(fd, UI_BEGIN_FF_UPLOAD, &upload); + auto fg = util::fail_guard([&]() { + upload.retval = 0; + ioctl(fd, UI_END_FF_UPLOAD, &upload); + }); + + effect_it->upload(upload.effect); + } break; + case UI_FF_ERASE: { + uinput_ff_erase erase {}; + + // *VERY* important, without this you break + // the kernel and have to reboot due to dead + // hanging process + erase.request_id = event->value; + + ioctl(fd, UI_BEGIN_FF_ERASE, &erase); + auto fg = util::fail_guard([&]() { + erase.retval = 0; + ioctl(fd, UI_END_FF_ERASE, &erase); + }); + + effect_it->erase(erase.effect_id); + } break; + } + break; + default: + BOOST_LOG(debug) + << util::hex(event->type).to_string_view() << ": "sv + << util::hex(event->code).to_string_view() << ": "sv + << event->value << " aka "sv << util::hex(event->value).to_string_view(); + } + } + } +} + +void broadcastRumble(safe::queue_t &rumble_queue_queue) { + std::vector effects; + std::vector polls; + + while(rumble_queue_queue.running()) { + while(rumble_queue_queue.peek()) { + auto dev_rumble_queue = rumble_queue_queue.pop(); + + if(!dev_rumble_queue) { + // rumble_queue_queue is no longer running + return; + } + + TUPLE_3D_REF(dev, rumble_queue, pollfd, *dev_rumble_queue); + { + auto effect_it = std::find_if(std::begin(effects), std::end(effects), [dev](auto &curr_effect) { + return dev == curr_effect.dev; + }); + + if(effect_it != std::end(effects)) { + + auto poll_it = std::begin(polls) + (effect_it - std::begin(effects)); + + polls.erase(poll_it); + effects.erase(effect_it); + + BOOST_LOG(debug) << "Removed Gamepad device from notifications"sv; + + continue; + } + + // There may be an attepmt to remove, that which not exists + if(!rumble_queue) { + continue; + } + } + + polls.emplace_back(std::move(pollfd)); + effects.emplace_back(dev, std::move(rumble_queue)); + + BOOST_LOG(debug) << "Added Gamepad device to notifications"sv; + } + + if(polls.empty()) { + std::this_thread::sleep_for(50ms); + } + else { + rumbleIterate(effects, polls, 100ms); + } + } +} + + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { auto touchscreen = ((input_raw_t *)input.get())->touch_input.get(); @@ -337,8 +849,8 @@ void keyboard(input_t &input, uint16_t modcode, bool release) { XFlush(keyboard.get()); } -int alloc_gamepad(input_t &input, int nr) { - return ((input_raw_t *)input.get())->alloc_gamepad(nr); +int alloc_gamepad(input_t &input, int nr, rumble_queue_t &&rumble_queue) { + return ((input_raw_t *)input.get())->alloc_gamepad(nr, std::move(rumble_queue)); } void free_gamepad(input_t &input, int nr) { @@ -548,6 +1060,13 @@ evdev_t x360() { libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &stick); libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RY, &stick); + libevdev_enable_event_type(dev.get(), EV_FF); + libevdev_enable_event_code(dev.get(), EV_FF, FF_RUMBLE, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_CONSTANT, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_PERIODIC, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_RAMP, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_GAIN, nullptr); + return dev; } @@ -555,6 +1074,8 @@ input_t input() { input_t result { new input_raw_t() }; auto &gp = *(input_raw_t *)result.get(); + gp.rumble_ctx = notifications.ref(); + gp.keyboard.reset(XOpenDisplay(nullptr)); // If we do not have a keyboard, gamepad or mouse, no input is possible and we should abort @@ -584,4 +1105,10 @@ void freeInput(void *p) { auto *input = (input_raw_t *)p; delete input; } + +std::vector &supported_gamepads() { + static std::vector gamepads { "x360"sv }; + + return gamepads; +} } // namespace platf diff --git a/sunshine/thread_safe.h b/sunshine/thread_safe.h index 01a122fe..d1541ee9 100644 --- a/sunshine/thread_safe.h +++ b/sunshine/thread_safe.h @@ -221,7 +221,7 @@ class queue_t { public: using status_t = util::optional_t; - queue_t(std::uint32_t max_elements) : _max_elements { max_elements } {} + queue_t(std::uint32_t max_elements = 32) : _max_elements { max_elements } {} template void raise(Args &&...args) {