diff --git a/CMakeLists.txt b/CMakeLists.txt index 950a748e..c81733ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,7 @@ set(SUNSHINE_TARGET_FILES sunshine/config.h sunshine/config.cpp sunshine/main.cpp + sunshine/main.h sunshine/crypto.cpp sunshine/crypto.h sunshine/nvhttp.cpp @@ -78,6 +79,9 @@ set(SUNSHINE_TARGET_FILES sunshine/platform/common.h sunshine/process.cpp sunshine/process.h + sunshine/move_by_copy.h + sunshine/task_pool.h + sunshine/thread_pool.h ${PLATFORM_TARGET_FILES}) include_directories( diff --git a/assets/sunshine.conf b/assets/sunshine.conf index 680c598a..5281d0fd 100644 --- a/assets/sunshine.conf +++ b/assets/sunshine.conf @@ -25,6 +25,14 @@ ping_timeout = 2000 # The higher fec_percentage, the lower space for the actual data to send per frame there is fec_percentage = 1 + +# The back/select button on the controller +# On the Shield, the home and powerbutton are not passed to Moonlight +# If, after the timeout, the back button is still pressed down, Home/Guide button press is emulated. +# If back_button_timeout < 0, then the Home/Guide button will not be emulated +# back_button_timeout = 2000 + + ############################################### # FFmpeg software encoding parameters # Honestly, I have no idea what the optimal values would be. diff --git a/sunshine/config.cpp b/sunshine/config.cpp index 46fef4c5..c3b1e938 100644 --- a/sunshine/config.cpp +++ b/sunshine/config.cpp @@ -43,6 +43,10 @@ nvhttp_t nvhttp { "devices.json" // file_devices }; +input_t input { + 2s +}; + bool whitespace(char ch) { return ch == ' ' || ch == '\t'; } @@ -143,6 +147,13 @@ void parse_file(const char *file) { } string_f(vars, "file_apps", stream.file_apps); int_f(vars, "fec_percentage", stream.fec_percentage); + + to = std::numeric_limits::min(); + int_f(vars, "back_button_timeout", to); + + if(to > std::numeric_limits::min()) { + input.back_button_timeout = std::chrono::milliseconds {to }; + } } diff --git a/sunshine/config.h b/sunshine/config.h index 6ee960c1..97e20805 100644 --- a/sunshine/config.h +++ b/sunshine/config.h @@ -37,9 +37,14 @@ struct nvhttp_t { std::string external_ip; }; +struct input_t { + std::chrono::milliseconds back_button_timeout; +}; + extern video_t video; extern stream_t stream; extern nvhttp_t nvhttp; +extern input_t input; void parse_file(const char *file); } diff --git a/sunshine/input.cpp b/sunshine/input.cpp index 9f200627..6b63e904 100644 --- a/sunshine/input.cpp +++ b/sunshine/input.cpp @@ -9,6 +9,8 @@ extern "C" { #include #include +#include "main.h" +#include "config.h" #include "input.h" #include "utility.h" @@ -132,7 +134,7 @@ void passthrough(platf::input_t &input, PNV_SCROLL_PACKET packet) { platf::scroll(input, util::endian::big(packet->scrollAmt1)); } -void passthrough(input_t &input, PNV_MULTI_CONTROLLER_PACKET packet) { +void passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { std::uint16_t bf; std::memcpy(&bf, &packet->buttonFlags, sizeof(std::uint16_t)); @@ -146,7 +148,7 @@ void passthrough(input_t &input, PNV_MULTI_CONTROLLER_PACKET packet) { packet->rightStickY }; - bf = gamepad_state.buttonFlags ^ input.gamepad_state.buttonFlags; + bf = gamepad_state.buttonFlags ^ input->gamepad_state.buttonFlags; auto bf_new = gamepad_state.buttonFlags; if(bf) { @@ -154,73 +156,94 @@ void passthrough(input_t &input, PNV_MULTI_CONTROLLER_PACKET packet) { if((DPAD_UP | DPAD_DOWN) & bf) { int val = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0); - platf::gp::dpad_y(input.input, val); + platf::gp::dpad_y(input->input, val); } if((DPAD_LEFT | DPAD_RIGHT) & bf) { int val = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0); - platf::gp::dpad_x(input.input, val); + platf::gp::dpad_x(input->input, val); } - if(START & bf) platf::gp::start(input.input, bf_new & START ? 1 : 0); - if(BACK & bf) platf::gp::back(input.input, bf_new & BACK ? 1 : 0); - if(LEFT_STICK & bf) platf::gp::left_stick(input.input, bf_new & LEFT_STICK ? 1 : 0); - if(RIGHT_STICK & bf) platf::gp::right_stick(input.input, bf_new & RIGHT_STICK ? 1 : 0); - if(LEFT_BUTTON & bf) platf::gp::left_button(input.input, bf_new & LEFT_BUTTON ? 1 : 0); - if(RIGHT_BUTTON & bf) platf::gp::right_button(input.input, bf_new & RIGHT_BUTTON ? 1 : 0); - if(HOME & bf) platf::gp::home(input.input, bf_new & HOME ? 1 : 0); - if(A & bf) platf::gp::a(input.input, bf_new & A ? 1 : 0); - if(B & bf) platf::gp::b(input.input, bf_new & B ? 1 : 0); - if(X & bf) platf::gp::x(input.input, bf_new & X ? 1 : 0); - if(Y & bf) platf::gp::y(input.input, bf_new & Y ? 1 : 0); + if(START & bf) platf::gp::start(input->input, bf_new & START ? 1 : 0); + if(LEFT_STICK & bf) platf::gp::left_stick(input->input, bf_new & LEFT_STICK ? 1 : 0); + if(RIGHT_STICK & bf) platf::gp::right_stick(input->input, bf_new & RIGHT_STICK ? 1 : 0); + if(LEFT_BUTTON & bf) platf::gp::left_button(input->input, bf_new & LEFT_BUTTON ? 1 : 0); + if(RIGHT_BUTTON & bf) platf::gp::right_button(input->input, bf_new & RIGHT_BUTTON ? 1 : 0); + if(HOME & bf) platf::gp::home(input->input, bf_new & HOME ? 1 : 0); + if(A & bf) platf::gp::a(input->input, bf_new & A ? 1 : 0); + if(B & bf) platf::gp::b(input->input, bf_new & B ? 1 : 0); + if(X & bf) platf::gp::x(input->input, bf_new & X ? 1 : 0); + if(Y & bf) platf::gp::y(input->input, bf_new & Y ? 1 : 0); + + if(BACK & bf) { + if(BACK & bf_new) { + platf::gp::back(input->input,1); + input->back_timeout_id = task_pool.pushDelayed([input]() { + platf::gp::back(input->input, 0); + + platf::gp::home(input->input,1); + platf::gp::home(input->input,0); + + input->back_timeout_id = nullptr; + }, config::input.back_button_timeout).task_id; + } + else if(input->back_timeout_id) { + platf::gp::back(input->input, 0); + + task_pool.cancel(input->back_timeout_id); + input->back_timeout_id = nullptr; + } + } } - if(input.gamepad_state.lt != gamepad_state.lt) { - platf::gp::left_trigger(input.input, gamepad_state.lt); + if(input->gamepad_state.lt != gamepad_state.lt) { + platf::gp::left_trigger(input->input, gamepad_state.lt); } - if(input.gamepad_state.rt != gamepad_state.rt) { - platf::gp::right_trigger(input.input, gamepad_state.rt); + if(input->gamepad_state.rt != gamepad_state.rt) { + platf::gp::right_trigger(input->input, gamepad_state.rt); } - if(input.gamepad_state.lsX != gamepad_state.lsX) { - platf::gp::left_stick_x(input.input, gamepad_state.lsX); + if(input->gamepad_state.lsX != gamepad_state.lsX) { + platf::gp::left_stick_x(input->input, gamepad_state.lsX); } - if(input.gamepad_state.lsY != gamepad_state.lsY) { - platf::gp::left_stick_y(input.input, gamepad_state.lsY); + if(input->gamepad_state.lsY != gamepad_state.lsY) { + platf::gp::left_stick_y(input->input, gamepad_state.lsY); } - if(input.gamepad_state.rsX != gamepad_state.rsX) { - platf::gp::right_stick_x(input.input, gamepad_state.rsX); + if(input->gamepad_state.rsX != gamepad_state.rsX) { + platf::gp::right_stick_x(input->input, gamepad_state.rsX); } - if(input.gamepad_state.rsY != gamepad_state.rsY) { - platf::gp::right_stick_y(input.input, gamepad_state.rsY); + if(input->gamepad_state.rsY != gamepad_state.rsY) { + platf::gp::right_stick_y(input->input, gamepad_state.rsY); } - input.gamepad_state = gamepad_state; - platf::gp::sync(input.input); + input->gamepad_state = gamepad_state; + platf::gp::sync(input->input); } -void passthrough(input_t &input, void *payload) { +void passthrough_helper(std::shared_ptr input, std::vector &&input_data) { + void *payload = input_data.data(); + int input_type = util::endian::big(*(int*)payload); switch(input_type) { case PACKET_TYPE_MOUSE_MOVE: - passthrough(input.input, (PNV_MOUSE_MOVE_PACKET)payload); + passthrough(input->input, (PNV_MOUSE_MOVE_PACKET)payload); break; case PACKET_TYPE_MOUSE_BUTTON: - passthrough(input.input, (PNV_MOUSE_BUTTON_PACKET)payload); + passthrough(input->input, (PNV_MOUSE_BUTTON_PACKET)payload); break; case PACKET_TYPE_SCROLL_OR_KEYBOARD: { char *tmp_input = (char*)payload + 4; if(tmp_input[0] == 0x0A) { - passthrough(input.input, (PNV_SCROLL_PACKET)payload); + passthrough(input->input, (PNV_SCROLL_PACKET)payload); } else { - passthrough(input.input, (PNV_KEYBOARD_PACKET)payload); + passthrough(input->input, (PNV_KEYBOARD_PACKET)payload); } break; @@ -231,5 +254,9 @@ void passthrough(input_t &input, void *payload) { } } -input_t::input_t() : gamepad_state { 0 }, input { platf::input() } {} +void passthrough(std::shared_ptr &input, std::vector &&input_data) { + task_pool.push(passthrough_helper, input, util::cmove(input_data)); +} + +input_t::input_t() : gamepad_state { 0 }, back_timeout_id { nullptr }, input { platf::input() } {} } diff --git a/sunshine/input.h b/sunshine/input.h index bb27558c..339700bc 100644 --- a/sunshine/input.h +++ b/sunshine/input.h @@ -6,6 +6,7 @@ #define SUNSHINE_INPUT_H #include "platform/common.h" +#include "thread_pool.h" namespace input { struct gamepad_state_t { @@ -22,11 +23,13 @@ struct input_t { input_t(); gamepad_state_t gamepad_state; + util::ThreadPool::task_id_t back_timeout_id; + platf::input_t input; }; void print(void *input); -void passthrough(input_t &, void *input); +void passthrough(std::shared_ptr &input, std::vector &&input_data); } #endif //SUNSHINE_INPUT_H diff --git a/sunshine/main.cpp b/sunshine/main.cpp index b04523b4..d6bff9f9 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -10,12 +10,15 @@ #include "stream.h" #include "config.h" #include "process.h" +#include "thread_pool.h" extern "C" { #include } using namespace std::literals; + +util::ThreadPool task_pool; int main(int argc, char *argv[]) { if(argc > 1) { if(!std::filesystem::exists(argv[1])) { @@ -34,11 +37,12 @@ int main(int argc, char *argv[]) { reed_solomon_init(); - std::thread httpThread { nvhttp::start }; - std::thread rtpThread { stream::rtpThread }; + task_pool.start(1); + std::thread httpThread { nvhttp::start }; + + stream::rtpThread(); httpThread.join(); - rtpThread.join(); return 0; } diff --git a/sunshine/main.h b/sunshine/main.h new file mode 100644 index 00000000..1a7bd309 --- /dev/null +++ b/sunshine/main.h @@ -0,0 +1,12 @@ +// +// Created by loki on 12/22/19. +// + +#ifndef SUNSHINE_MAIN_H +#define SUNSHINE_MAIN_H + +#include "thread_pool.h" + +extern util::ThreadPool task_pool; + +#endif //SUNSHINE_MAIN_H diff --git a/sunshine/move_by_copy.h b/sunshine/move_by_copy.h new file mode 100755 index 00000000..61e205f5 --- /dev/null +++ b/sunshine/move_by_copy.h @@ -0,0 +1,50 @@ +#ifndef DOSSIER_MOVE_BY_COPY_H +#define DOSSIER_MOVE_BY_COPY_H + +#include +namespace util { +/* + * When a copy is made, it moves the object + * This allows you to move an object when a move can't be done. + */ +template +class MoveByCopy { +public: + typedef T move_type; +private: + move_type _to_move; +public: + + explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) { } + + MoveByCopy(MoveByCopy &&other) = default; + + MoveByCopy(const MoveByCopy &other) { + *this = other; + } + + MoveByCopy& operator=(MoveByCopy &&other) = default; + + MoveByCopy& operator=(const MoveByCopy &other) { + this->_to_move = std::move(const_cast(other)._to_move); + + return *this; + } + + operator move_type() { + return std::move(_to_move); + } +}; + +template +MoveByCopy cmove(T &movable) { + return MoveByCopy(std::move(movable)); +} + +// Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller +template +MoveByCopy const_cmove(const T &movable) { + return MoveByCopy(std::move(const_cast(movable))); +} +} +#endif diff --git a/sunshine/stream.cpp b/sunshine/stream.cpp index c36b84cc..011f9680 100644 --- a/sunshine/stream.cpp +++ b/sunshine/stream.cpp @@ -468,7 +468,7 @@ void control_server_t::send(const std::string_view & payload) { void controlThread(video::idr_event_t idr_events) { control_server_t server { CONTROL_PORT }; - input::input_t input; + auto input = std::make_shared(); server.map(packetTypes[IDX_START_A], [](const std::string_view &payload) { session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; @@ -539,7 +539,7 @@ void controlThread(video::idr_event_t idr_events) { } input::print(plaintext.data()); - input::passthrough(input, plaintext.data()); + input::passthrough(input, std::move(plaintext)); }); while(has_session) { diff --git a/sunshine/task_pool.h b/sunshine/task_pool.h new file mode 100755 index 00000000..02e0f250 --- /dev/null +++ b/sunshine/task_pool.h @@ -0,0 +1,238 @@ +#ifndef KITTY_TASK_POOL_H +#define KITTY_TASK_POOL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utility.h" +#include "move_by_copy.h" +namespace util { + +class _ImplBase { +public: + //_unique_base_type _this_ptr; + + inline virtual ~_ImplBase() = default; + + virtual void run() = 0; +}; + +template +class _Impl : public _ImplBase { + Function _func; + +public: + + _Impl(Function&& f) : _func(std::forward(f)) { } + + void run() override { + _func(); + } +}; + +class TaskPool { +public: + typedef std::unique_ptr<_ImplBase> __task; + typedef _ImplBase* task_id_t; + + + typedef std::chrono::steady_clock::time_point __time_point; + + template + class timer_task_t { + public: + task_id_t task_id; + std::future future; + + timer_task_t(task_id_t task_id, std::future &future) : task_id { task_id }, future { std::move(future) } {} + }; +protected: + std::deque<__task> _tasks; + std::vector> _timer_tasks; + std::mutex _task_mutex; + +public: + TaskPool() = default; + TaskPool(TaskPool &&other) noexcept : _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {} + + TaskPool &operator=(TaskPool &&other) noexcept { + std::swap(_tasks, other._tasks); + std::swap(_timer_tasks, other._timer_tasks); + + return *this; + } + + template + auto push(Function && newTask, Args &&... args) { + static_assert(std::is_invocable_v, "arguments don't match the function"); + + using __return = std::invoke_result_t; + using task_t = std::packaged_task<__return()>; + + auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { + return std::apply(task, std::move(tuple_args)); + }; + + task_t task(std::move(bind)); + + auto future = task.get_future(); + + std::lock_guard lg(_task_mutex); + _tasks.emplace_back(toRunnable(std::move(task))); + + return future; + } + + void pushDelayed(std::pair<__time_point, __task> &&task) { + std::lock_guard lg(_task_mutex); + + auto it = _timer_tasks.cbegin(); + for(; it < _timer_tasks.cend(); ++it) { + if(std::get<0>(*it) < task.first) { + break; + } + } + + _timer_tasks.emplace(it, task.first, std::move(task.second)); + } + + /** + * @return an id to potentially delay the task + */ + template + auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&... args) { + static_assert(std::is_invocable_v, "arguments don't match the function"); + + using __return = std::invoke_result_t; + using task_t = std::packaged_task<__return()>; + + __time_point time_point = std::chrono::steady_clock::now() + duration; + + auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { + return std::apply(task, std::move(tuple_args)); + }; + + task_t task(std::move(bind)); + + auto future = task.get_future(); + auto runnable = toRunnable(std::move(task)); + + task_id_t task_id = &*runnable; + + pushDelayed(std::pair { time_point, std::move(runnable) }); + + return timer_task_t<__return> { task_id, future }; + } + + /** + * @param duration The delay before executing the task + */ + template + void delay(task_id_t task_id, std::chrono::duration duration) { + std::lock_guard lg(_task_mutex); + + auto it = _timer_tasks.begin(); + for(; it < _timer_tasks.cend(); ++it) { + const __task &task = std::get<1>(*it); + + if(&*task == task_id) { + std::get<0>(*it) = std::chrono::steady_clock::now() + duration; + + break; + } + } + + if(it == _timer_tasks.cend()) { + return; + } + + // smaller time goes to the back + auto prev = it -1; + while(it > _timer_tasks.cbegin()) { + if(std::get<0>(*it) > std::get<0>(*prev)) { + std::swap(*it, *prev); + } + + --prev; --it; + } + } + + bool cancel(task_id_t task_id) { + std::lock_guard lg(_task_mutex); + + auto it = _timer_tasks.begin(); + for(; it < _timer_tasks.cend(); ++it) { + const __task &task = std::get<1>(*it); + + if(&*task == task_id) { + _timer_tasks.erase(it); + + return true; + } + } + + return false; + } + + std::optional> pop(task_id_t task_id) { + std::lock_guard lg(_task_mutex); + + auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; }); + + if(pos == std::end(_timer_tasks)) { + return std::nullopt; + } + + return std::move(*pos); + } + + std::optional<__task> pop() { + std::lock_guard lg(_task_mutex); + + if(!_tasks.empty()) { + __task task = std::move(_tasks.front()); + _tasks.pop_front(); + return std::move(task); + } + + if(!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) { + __task task = std::move(std::get<1>(_timer_tasks.back())); + _timer_tasks.pop_back(); + + return std::move(task); + } + + return std::nullopt; + } + + bool ready() { + std::lock_guard lg(_task_mutex); + + return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()); + } + + std::optional<__time_point> next() { + std::lock_guard lg(_task_mutex); + + if(_timer_tasks.empty()) { + return std::nullopt; + } + + return std::get<0>(_timer_tasks.back()); + } +private: + + template + std::unique_ptr<_ImplBase> toRunnable(Function &&f) { + return std::make_unique<_Impl>(std::forward(f)); + } +}; +} +#endif diff --git a/sunshine/thread_pool.h b/sunshine/thread_pool.h new file mode 100755 index 00000000..be3f4eb9 --- /dev/null +++ b/sunshine/thread_pool.h @@ -0,0 +1,121 @@ +#ifndef KITTY_THREAD_POOL_H +#define KITTY_THREAD_POOL_H + +#include +#include "task_pool.h" + +namespace util { +/* + * Allow threads to execute unhindered + * while keeping full controll over the threads. + */ +class ThreadPool : public TaskPool { +public: + typedef TaskPool::__task __task; + +private: + std::vector _thread; + + std::condition_variable _cv; + std::mutex _lock; + + bool _continue; +public: + ThreadPool() : _continue { false } {} + + explicit ThreadPool(int threads) : _thread(threads), _continue { true } { + for (auto &t : _thread) { + t = std::thread(&ThreadPool::_main, this); + } + } + + ~ThreadPool() noexcept { + if (!_continue) return; + + stop(); + join(); + } + + template + auto push(Function && newTask, Args &&... args) { + std::lock_guard lg(_lock); + auto future = TaskPool::push(std::forward(newTask), std::forward(args)...); + + _cv.notify_one(); + return future; + } + + void pushDelayed(std::pair<__time_point, __task> &&task) { + std::lock_guard lg(_lock); + + TaskPool::pushDelayed(std::move(task)); + } + + template + auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&... args) { + std::lock_guard lg(_lock); + auto future = TaskPool::pushDelayed(std::forward(newTask), duration, std::forward(args)...); + + // Update all timers for wait_until + _cv.notify_all(); + return future; + } + + void start(int threads) { + _continue = true; + + _thread.resize(threads); + + for(auto &t : _thread) { + t = std::thread(&ThreadPool::_main, this); + } + } + + void stop() { + std::lock_guard lg(_lock); + + _continue = false; + _cv.notify_all(); + } + + void join() { + for (auto & t : _thread) { + t.join(); + } + } + +public: + + void _main() { + while (_continue) { + if(auto task = this->pop()) { + (*task)->run(); + } + else { + std::unique_lock uniq_lock(_lock); + + if(ready()) { + continue; + } + + if(!_continue) { + break; + } + + if(auto tp = next()) { + _cv.wait_until(uniq_lock, *tp); + } + else { + _cv.wait(uniq_lock); + } + } + } + + // Execute remaining tasks + while(auto task = this->pop()) { + (*task)->run(); + } + } +}; +} +#endif