diff --git a/src/input.cpp b/src/input.cpp index 6f66f02c370..d38dd5ba4dc 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -11,6 +11,7 @@ extern "C" { #include #include +#include #include #include #include @@ -90,6 +91,18 @@ namespace input { return boost::endian::endian_load(f); } + /** + * @brief Converts a little-endian netfloat to a native endianness float and clamps it. + * @param f Netfloat value. + * @param min The minimium value for clamping. + * @param max The maximum value for clamping. + * @return Clamped float value. + */ + float + from_clamped_netfloat(netfloat f, float min, float max) { + return std::clamp(from_netfloat(f), min, max); + } + static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; static std::unordered_map key_press {}; static std::array mouse_press {}; @@ -143,6 +156,7 @@ namespace input { platf::feedback_queue_t feedback_queue): shortcutFlags {}, gamepads(MAX_GAMEPADS), + client_context { platf::allocate_client_input_context(platf_input) }, touch_port_event { std::move(touch_port_event) }, feedback_queue { std::move(feedback_queue) }, mouse_left_button_timeout {}, @@ -152,6 +166,7 @@ namespace input { int shortcutFlags; std::vector gamepads; + std::unique_ptr client_context; safe::mail_raw_t::event_t touch_port_event; platf::feedback_queue_t feedback_queue; @@ -274,6 +289,46 @@ namespace input { << "--end controller packet--"sv; } + /** + * @brief Prints a touch packet. + * @param packet The touch packet. + */ + void + print(PSS_TOUCH_PACKET packet) { + BOOST_LOG(debug) + << "--begin touch packet--"sv << std::endl + << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl + << "pointerId ["sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl + << "x ["sv << from_netfloat(packet->x) << ']' << std::endl + << "y ["sv << from_netfloat(packet->y) << ']' << std::endl + << "pressureOrDistance ["sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl + << "contactAreaMajor ["sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl + << "contactAreaMinor ["sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl + << "rotation ["sv << (uint32_t) packet->rotation << ']' << std::endl + << "--end touch packet--"sv; + } + + /** + * @brief Prints a pen packet. + * @param packet The pen packet. + */ + void + print(PSS_PEN_PACKET packet) { + BOOST_LOG(debug) + << "--begin pen packet--"sv << std::endl + << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl + << "toolType ["sv << util::hex(packet->toolType).to_string_view() << ']' << std::endl + << "penButtons ["sv << util::hex(packet->penButtons).to_string_view() << ']' << std::endl + << "x ["sv << from_netfloat(packet->x) << ']' << std::endl + << "y ["sv << from_netfloat(packet->y) << ']' << std::endl + << "pressureOrDistance ["sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl + << "contactAreaMajor ["sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl + << "contactAreaMinor ["sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl + << "rotation ["sv << (uint32_t) packet->rotation << ']' << std::endl + << "tilt ["sv << (uint32_t) packet->tilt << ']' << std::endl + << "--end pen packet--"sv; + } + /** * @brief Prints a controller arrival packet. * @param packet The controller arrival packet. @@ -367,6 +422,12 @@ namespace input { case MULTI_CONTROLLER_MAGIC_GEN5: print((PNV_MULTI_CONTROLLER_PACKET) payload); break; + case SS_TOUCH_MAGIC: + print((PSS_TOUCH_PACKET) payload); + break; + case SS_PEN_MAGIC: + print((PSS_PEN_PACKET) payload); + break; case SS_CONTROLLER_ARRIVAL_MAGIC: print((PSS_CONTROLLER_ARRIVAL_PACKET) payload); break; @@ -392,6 +453,78 @@ namespace input { platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); } + /** + * @brief Converts client coordinates on the specified surface into screen coordinates. + * @param input The input context. + * @param val The cartesian coordinate pair to convert. + * @param size The size of the client's surface containing the value. + * @return The host-relative coordinate pair. + */ + std::pair + client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { + auto &touch_port_event = input->touch_port_event; + auto &touch_port = input->touch_port; + if (touch_port_event->peek()) { + touch_port = *touch_port_event->pop(); + } + + auto scalarX = touch_port.width / size.first; + auto scalarY = touch_port.height / size.second; + + float x = std::clamp(val.first, 0.0f, size.first) * scalarX; + float y = std::clamp(val.second, 0.0f, size.second) * scalarY; + + auto offsetX = touch_port.client_offsetX; + auto offsetY = touch_port.client_offsetY; + + x = std::clamp(x, offsetX, size.first - offsetX); + y = std::clamp(y, offsetY, size.second - offsetY); + + return { (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv }; + } + + /** + * @brief Multiplies a polar coordinate pair by a cartesian scaling factor. + * @param r The radial coordinate. + * @param angle The angular coordinate (radians). + * @param scalar The scalar cartesian coordinate pair. + * @return The scaled radial coordinate. + */ + float + multiply_polar_by_cartesian_scalar(float r, float angle, const std::pair &scalar) { + // Convert polar to cartesian coordinates + float x = r * std::cos(angle); + float y = r * std::sin(angle); + + // Scale the values + x *= scalar.first; + y *= scalar.second; + + // Convert the result back to a polar radial coordinate + return std::sqrt(std::pow(x, 2) + std::pow(y, 2)); + } + + /** + * @brief Scales the ellipse axes according to the provided size. + * @param val The major and minor axis pair. + * @param rotation The rotation value from the touch/pen event. + * @param scalar The scalar cartesian coordinate pair. + * @return The major and minor axis pair. + */ + std::pair + scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar) { + // If the rotation is unknown, we'll just scale both axes equally by using + // a 45 degree angle for our scaling calculations + float angle = rotation == LI_ROT_UNKNOWN ? (M_PI / 4) : (rotation * (M_PI / 180)); + + // If we have a major but not a minor axis, treat the touch as circular + float major = val.first; + float minor = val.second != 0.0f ? val.second : val.first; + + // The minor axis is perpendicular to major axis so the angle must be rotated by 90 degrees + return { multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar) }; + } + void passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { if (!config::input.mouse) { @@ -402,12 +535,6 @@ namespace input { input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY; } - auto &touch_port_event = input->touch_port_event; - auto &touch_port = input->touch_port; - if (touch_port_event->peek()) { - touch_port = *touch_port_event->pop(); - } - float x = util::endian::big(packet->x); float y = util::endian::big(packet->y); @@ -422,24 +549,15 @@ namespace input { auto width = (float) util::endian::big(packet->width); auto height = (float) util::endian::big(packet->height); - auto scalarX = touch_port.width / width; - auto scalarY = touch_port.height / height; - - x *= scalarX; - y *= scalarY; - - auto offsetX = touch_port.client_offsetX; - auto offsetY = touch_port.client_offsetY; - - std::clamp(x, offsetX, width - offsetX); - std::clamp(y, offsetY, height - offsetY); + auto tpcoords = client_to_touchport(input, { x, y }, { width, height }); + auto &touch_port = input->touch_port; platf::touch_port_t abs_port { touch_port.offset_x, touch_port.offset_y, touch_port.env_width, touch_port.env_height }; - platf::abs_mouse(platf_input, abs_port, (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv); + platf::abs_mouse(platf_input, abs_port, tpcoords.first, tpcoords.second); } void @@ -741,6 +859,116 @@ namespace input { input->gamepads[packet->controllerNumber].id = id; } + /** + * @brief Called to pass a touch message to the platform backend. + * @param input The input context pointer. + * @param packet The touch packet. + */ + void + passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) { + if (!config::input.mouse) { + return; + } + + // Convert the client normalized coordinates to touchport coordinates + auto coords = client_to_touchport(input, + { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, + { 65535.f, 65535.f }); + + auto &touch_port = input->touch_port; + platf::touch_port_t abs_port { + touch_port.offset_x, touch_port.offset_y, + touch_port.env_width, touch_port.env_height + }; + + // Renormalize the coordinates + coords.first /= abs_port.width; + coords.second /= abs_port.height; + + // Normalize rotation value to 0-359 degree range + auto rotation = util::endian::little(packet->rotation); + if (rotation != LI_ROT_UNKNOWN) { + rotation %= 360; + } + + // Normalize the contact area based on the touchport + auto contact_area = scale_client_contact_area( + { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f }, + rotation, + { abs_port.width / 65535.f, abs_port.height / 65535.f }); + + platf::touch_input_t touch { + packet->eventType, + rotation, + util::endian::little(packet->pointerId), + coords.first, + coords.second, + from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), + contact_area.first, + contact_area.second, + }; + + platf::touch(input->client_context.get(), abs_port, touch); + } + + /** + * @brief Called to pass a pen message to the platform backend. + * @param input The input context pointer. + * @param packet The pen packet. + */ + void + passthrough(std::shared_ptr &input, PSS_PEN_PACKET packet) { + if (!config::input.mouse) { + return; + } + + // Convert the client normalized coordinates to touchport coordinates + auto coords = client_to_touchport(input, + { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, + { 65535.f, 65535.f }); + + auto &touch_port = input->touch_port; + platf::touch_port_t abs_port { + touch_port.offset_x, touch_port.offset_y, + touch_port.env_width, touch_port.env_height + }; + + // Renormalize the coordinates + coords.first /= abs_port.width; + coords.second /= abs_port.height; + + // Normalize rotation value to 0-359 degree range + auto rotation = util::endian::little(packet->rotation); + if (rotation != LI_ROT_UNKNOWN) { + rotation %= 360; + } + + // Normalize the contact area based on the touchport + auto contact_area = scale_client_contact_area( + { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f }, + rotation, + { abs_port.width / 65535.f, abs_port.height / 65535.f }); + + platf::pen_input_t pen { + packet->eventType, + packet->toolType, + packet->penButtons, + packet->tilt, + rotation, + coords.first, + coords.second, + from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), + contact_area.first, + contact_area.second, + }; + + platf::pen(input->client_context.get(), abs_port, pen); + } + /** * @brief Called to pass a controller touch message to the platform backend. * @param input The input context pointer. @@ -1327,6 +1555,12 @@ namespace input { case MULTI_CONTROLLER_MAGIC_GEN5: passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload); break; + case SS_TOUCH_MAGIC: + passthrough(input, (PSS_TOUCH_PACKET) payload); + break; + case SS_PEN_MAGIC: + passthrough(input, (PSS_PEN_PACKET) payload); + break; case SS_CONTROLLER_ARRIVAL_MAGIC: passthrough(input, (PSS_CONTROLLER_ARRIVAL_PACKET) payload); break; diff --git a/src/platform/common.h b/src/platform/common.h index 1173afd5ad5..de285331bcf 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -279,6 +279,30 @@ namespace platf { std::uint8_t percentage; }; + struct touch_input_t { + std::uint8_t eventType; + std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN + std::uint32_t pointerId; + float x; + float y; + float pressureOrDistance; // Distance for hover and pressure for contact + float contactAreaMajor; + float contactAreaMinor; + }; + + struct pen_input_t { + std::uint8_t eventType; + std::uint8_t toolType; + std::uint8_t penButtons; + std::uint8_t tilt; // Degrees (0..90) or LI_TILT_UNKNOWN + std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN + float x; + float y; + float pressureOrDistance; // Distance for hover and pressure for contact + float contactAreaMajor; + float contactAreaMinor; + }; + class deinit_t { public: virtual ~deinit_t() = default; @@ -564,9 +588,37 @@ namespace platf { void unicode(input_t &input, char *utf8, int size); + typedef deinit_t client_input_t; + + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input); + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); + /** * @brief Sends a gamepad touch event to the OS. - * @param input The input context. + * @param input The global input context. * @param touch The touch event. */ void @@ -574,7 +626,7 @@ namespace platf { /** * @brief Sends a gamepad motion event to the OS. - * @param input The input context. + * @param input The global input context. * @param motion The motion event. */ void @@ -582,7 +634,7 @@ namespace platf { /** * @brief Sends a gamepad battery event to the OS. - * @param input The input context. + * @param input The global input context. * @param battery The battery event. */ void @@ -590,7 +642,7 @@ namespace platf { /** * @brief Creates a new virtual gamepad. - * @param input The input context. + * @param input The global input context. * @param id The gamepad ID. * @param metadata Controller metadata from client (empty if none provided). * @param feedback_queue The queue for posting messages back to the client. diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index a14a63c99cc..8d951c6d47d 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -1489,7 +1489,7 @@ namespace platf { /** * @brief Creates a new virtual gamepad. - * @param input The input context. + * @param input The global input context. * @param id The gamepad ID. * @param metadata Controller metadata from client (empty if none provided). * @param feedback_queue The queue for posting messages back to the client. @@ -1567,9 +1567,42 @@ namespace platf { libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0); } + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input) { + // Unused + return nullptr; + } + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + // Unimplemented feature - platform_caps::pen_touch + } + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + // Unimplemented feature - platform_caps::pen_touch + } + /** * @brief Sends a gamepad touch event to the OS. - * @param input The input context. + * @param input The global input context. * @param touch The touch event. */ void @@ -1579,7 +1612,7 @@ namespace platf { /** * @brief Sends a gamepad motion event to the OS. - * @param input The input context. + * @param input The global input context. * @param motion The motion event. */ void @@ -1589,7 +1622,7 @@ namespace platf { /** * @brief Sends a gamepad battery event to the OS. - * @param input The input context. + * @param input The global input context. * @param battery The battery event. */ void diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index ddb45761c33..1401c169cae 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -448,9 +448,42 @@ const KeyCodeMap kKeyCodesMap[] = { // Unimplemented } + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input) { + // Unused + return nullptr; + } + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + // Unimplemented feature - platform_caps::pen_touch + } + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + // Unimplemented feature - platform_caps::pen_touch + } + /** * @brief Sends a gamepad touch event to the OS. - * @param input The input context. + * @param input The global input context. * @param touch The touch event. */ void @@ -460,7 +493,7 @@ const KeyCodeMap kKeyCodesMap[] = { /** * @brief Sends a gamepad motion event to the OS. - * @param input The input context. + * @param input The global input context. * @param motion The motion event. */ void @@ -470,7 +503,7 @@ const KeyCodeMap kKeyCodesMap[] = { /** * @brief Sends a gamepad battery event to the OS. - * @param input The input context. + * @param input The global input context. * @param battery The battery event. */ void diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index f64855120a3..8ab1484c5fc 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -2,6 +2,7 @@ * @file src/platform/windows/input.cpp * @brief todo */ +#define WINVER 0x0A00 #include #include @@ -13,6 +14,16 @@ #include "src/main.h" #include "src/platform/common.h" +#ifdef __MINGW32__ +DECLARE_HANDLE(HSYNTHETICPOINTERDEVICE); +WINUSERAPI HSYNTHETICPOINTERDEVICE WINAPI +CreateSyntheticPointerDevice(POINTER_INPUT_TYPE pointerType, ULONG maxCount, POINTER_FEEDBACK_MODE mode); +WINUSERAPI BOOL WINAPI +InjectSyntheticPointerInput(HSYNTHETICPOINTERDEVICE device, CONST POINTER_TYPE_INFO *pointerInfo, UINT32 count); +WINUSERAPI VOID WINAPI +DestroySyntheticPointerDevice(HSYNTHETICPOINTERDEVICE device); +#endif + namespace platf { using namespace std::literals; @@ -390,6 +401,10 @@ namespace platf { vigem_t *vigem; HKL keyboard_layout; HKL active_layout; + + decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; + decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; + decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; }; input_t @@ -418,6 +433,11 @@ namespace platf { raw.keyboard_layout = NULL; } + // Get pointers to virtual touch/pen input functions (Win10 1809+) + raw.fnCreateSyntheticPointerDevice = (decltype(CreateSyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice"); + raw.fnInjectSyntheticPointerInput = (decltype(InjectSyntheticPointerInput) *) GetProcAddress(GetModuleHandleA("user32.dll"), "InjectSyntheticPointerInput"); + raw.fnDestroySyntheticPointerDevice = (decltype(DestroySyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "DestroySyntheticPointerDevice"); + return result; } @@ -580,6 +600,506 @@ namespace platf { send_input(i); } + struct client_input_raw_t: public client_input_t { + client_input_raw_t(input_t &input) { + global = (input_raw_t *) input.get(); + } + + ~client_input_raw_t() override { + if (penRepeatTask) { + task_pool.cancel(penRepeatTask); + } + if (touchRepeatTask) { + task_pool.cancel(touchRepeatTask); + } + + if (pen) { + global->fnDestroySyntheticPointerDevice(pen); + } + if (touch) { + global->fnDestroySyntheticPointerDevice(touch); + } + } + + input_raw_t *global; + + // Device state and handles for pen and touch input must be stored in the per-client + // input context, because each connected client may be sending their own independent + // pen/touch events. To maintain separation, we expose separate pen and touch devices + // for each client. + + HSYNTHETICPOINTERDEVICE pen {}; + POINTER_TYPE_INFO penInfo {}; + thread_pool_util::ThreadPool::task_id_t penRepeatTask {}; + + HSYNTHETICPOINTERDEVICE touch {}; + POINTER_TYPE_INFO touchInfo[10] {}; + UINT32 activeTouchSlots {}; + thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; + }; + + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input) { + return std::make_unique(input); + } + + /** + * @brief Compacts the touch slots into a contiguous block and updates the active count. + * @details Since this swaps entries around, all slot pointers/references are invalid after compaction. + * @param raw The client-specific input context. + */ + void + perform_touch_compaction(client_input_raw_t *raw) { + // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput(). + UINT32 i; + for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { + // This is an empty slot. Look for a later entry to move into this slot. + for (UINT32 j = i + 1; j < ARRAYSIZE(raw->touchInfo); j++) { + if (raw->touchInfo[j].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + std::swap(raw->touchInfo[i], raw->touchInfo[j]); + break; + } + } + + // If we didn't find anything, we've reached the end of active slots. + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { + break; + } + } + } + + // Update the number of active touch slots + raw->activeTouchSlots = i; + } + + /** + * @brief Gets a pointer slot by client-relative pointer ID, claiming a new one if necessary. + * @param raw The raw client-specific input context. + * @param pointerId The client's pointer ID. + * @param eventType The LI_TOUCH_EVENT value from the client. + * @return A pointer to the slot entry. + */ + POINTER_TYPE_INFO * + pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) { + // Compact active touches into a single contiguous block + perform_touch_compaction(raw); + + // Try to find a matching pointer ID + for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerId == pointerId && + raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + if (eventType == LI_TOUCH_EVENT_DOWN && (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT)) { + BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv; + } + + return &raw->touchInfo[i]; + } + } + + if (eventType != LI_TOUCH_EVENT_HOVER && eventType != LI_TOUCH_EVENT_DOWN) { + BOOST_LOG(warning) << "Unexpected new pointer "sv << pointerId << " for event "sv << (uint32_t) eventType << ". Did the client drop a down/hover event?"sv; + } + + // If there was none, grab an unused entry and increment the active slot count + for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { + raw->touchInfo[i].touchInfo.pointerInfo.pointerId = pointerId; + raw->activeTouchSlots = i + 1; + return &raw->touchInfo[i]; + } + } + + return nullptr; + } + + /** + * @brief Populate common `POINTER_INFO` members shared between pen and touch events. + * @param pointerInfo The pointer info to populate. + * @param touchPort The current viewport for translating to screen coordinates. + * @param eventType The type of touch/pen event. + * @param x The normalized 0.0-1.0 X coordinate. + * @param y The normalized 0.0-1.0 Y coordinate. + */ + void + populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) { + switch (eventType) { + case LI_TOUCH_EVENT_HOVER: + pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT; + pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE; + pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; + pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; + break; + case LI_TOUCH_EVENT_DOWN: + pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN; + pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; + pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; + break; + case LI_TOUCH_EVENT_UP: + // We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range + pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + pointerInfo.pointerFlags |= POINTER_FLAG_UP; + break; + case LI_TOUCH_EVENT_MOVE: + pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE; + pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; + pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; + break; + case LI_TOUCH_EVENT_CANCEL: + case LI_TOUCH_EVENT_CANCEL_ALL: + // If we were in contact with the touch surface at the time of the cancellation, + // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE. + if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { + pointerInfo.pointerFlags |= POINTER_FLAG_UP; + } + else { + pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + } + pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED; + break; + case LI_TOUCH_EVENT_HOVER_LEAVE: + pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + break; + case LI_TOUCH_EVENT_BUTTON_ONLY: + // On Windows, we can only pass buttons if we have an active pointer + if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + } + break; + default: + BOOST_LOG(warning) << "Unknown touch event: "sv << (uint32_t) eventType; + break; + } + } + + // Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically + // cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh + // the injected input periodically. + constexpr auto ISPI_REPEAT_INTERVAL = 50ms; + + /** + * @brief Repeats the current touch state to avoid the interactions timing out. + * @param raw The raw client-specific input context. + */ + void + repeat_touch(client_input_raw_t *raw) { + if (!raw->global->fnInjectSyntheticPointerInput(raw->touch, raw->touchInfo, raw->activeTouchSlots)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to refresh virtual touch input: "sv << err; + } + + raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id; + } + + /** + * @brief Repeats the current pen state to avoid the interactions timing out. + * @param raw The raw client-specific input context. + */ + void + repeat_pen(client_input_raw_t *raw) { + if (!raw->global->fnInjectSyntheticPointerInput(raw->pen, &raw->penInfo, 1)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to refresh virtual pen input: "sv << err; + } + + raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id; + } + + /** + * @brief Cancels all active touches. + * @param raw The raw client-specific input context. + */ + void + cancel_all_active_touches(client_input_raw_t *raw) { + // Cancel touch repeat callbacks + if (raw->touchRepeatTask) { + task_pool.cancel(raw->touchRepeatTask); + raw->touchRepeatTask = nullptr; + } + + // Compact touches to update activeTouchSlots + perform_touch_compaction(raw); + + // If we have active slots, cancel them all + if (raw->activeTouchSlots > 0) { + for (UINT32 i = 0; i < raw->activeTouchSlots; i++) { + populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f); + raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE; + } + if (!raw->global->fnInjectSyntheticPointerInput(raw->touch, raw->touchInfo, raw->activeTouchSlots)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to cancel all virtual touch input: "sv << err; + } + } + + // Zero all touch state + std::memset(raw->touchInfo, 0, sizeof(raw->touchInfo)); + raw->activeTouchSlots = 0; + } + + // These are edge-triggered pointer state flags that should always be cleared next frame + constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + auto raw = (client_input_raw_t *) input; + + // Bail if we're not running on an OS that supports virtual touch input + if (!raw->global->fnCreateSyntheticPointerDevice || + !raw->global->fnInjectSyntheticPointerInput || + !raw->global->fnDestroySyntheticPointerDevice) { + BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; + return; + } + + // If there's not already a virtual touch device, create one now + if (!raw->touch) { + if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { + BOOST_LOG(info) << "Creating virtual touch input device"sv; + raw->touch = raw->global->fnCreateSyntheticPointerDevice(PT_TOUCH, ARRAYSIZE(raw->touchInfo), POINTER_FEEDBACK_DEFAULT); + if (!raw->touch) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to create virtual touch device: "sv << err; + return; + } + } + else { + // No need to cancel anything if we had no touch input device + return; + } + } + + // Cancel touch repeat callbacks + if (raw->touchRepeatTask) { + task_pool.cancel(raw->touchRepeatTask); + raw->touchRepeatTask = nullptr; + } + + // If this is a special request to cancel all touches, do that and return + if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + cancel_all_active_touches(raw); + return; + } + + // Find or allocate an entry for this touch pointer ID + auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType); + if (!pointer) { + BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv; + cancel_all_active_touches(raw); + pointer = pointer_by_id(raw, touch.pointerId, touch.eventType); + } + + pointer->type = PT_TOUCH; + + auto &touchInfo = pointer->touchInfo; + touchInfo.pointerInfo.pointerType = PT_TOUCH; + + // Populate shared pointer info fields + populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y); + + touchInfo.touchMask = TOUCH_MASK_NONE; + + // Pressure and contact area only apply to in-contact pointers. + // + // The clients also pass distance and tool size for hovers, but Windows doesn't + // provide APIs to receive that data. + if (touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { + if (touch.pressureOrDistance != 0.0f) { + touchInfo.touchMask |= TOUCH_MASK_PRESSURE; + + // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses + touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024); + } + else { + // The default touch pressure is 512 + touchInfo.pressure = 512; + } + + if (touch.contactAreaMajor != 0.0f && touch.contactAreaMinor != 0.0f) { + // For the purposes of contact area calculation, we will assume the touches + // are at a 45 degree angle if rotation is unknown. This will scale the major + // axis value by width and height equally. + float rotationAngleDegs = touch.rotation == LI_ROT_UNKNOWN ? 45 : touch.rotation; + + float majorAxisAngle = rotationAngleDegs * (M_PI / 180); + float minorAxisAngle = majorAxisAngle + (M_PI / 2); + + // Estimate the contact rectangle + float contactWidth = (std::cos(majorAxisAngle) * touch.contactAreaMajor) + (std::cos(minorAxisAngle) * touch.contactAreaMinor); + float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor); + + // Convert into screen coordinates centered at the touch location and constrained by screen dimensions + touchInfo.rcContact.left = std::max(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2)); + touchInfo.rcContact.right = std::min(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2)); + touchInfo.rcContact.top = std::max(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2)); + touchInfo.rcContact.bottom = std::min(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2)); + + touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA; + } + } + else { + touchInfo.pressure = 0; + touchInfo.rcContact = {}; + } + + if (touch.rotation != LI_ROT_UNKNOWN) { + touchInfo.touchMask |= TOUCH_MASK_ORIENTATION; + touchInfo.orientation = touch.rotation; + } + else { + touchInfo.orientation = 0; + } + + if (!raw->global->fnInjectSyntheticPointerInput(raw->touch, raw->touchInfo, raw->activeTouchSlots)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to inject virtual touch input: "sv << err; + return; + } + + // Clear pointer flags that should only remain set for one frame + touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS; + + // If we still have an active touch, refresh the touch state periodically + if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id; + } + } + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + auto raw = (client_input_raw_t *) input; + + // Bail if we're not running on an OS that supports virtual pen input + if (!raw->global->fnCreateSyntheticPointerDevice || + !raw->global->fnInjectSyntheticPointerInput || + !raw->global->fnDestroySyntheticPointerDevice) { + BOOST_LOG(warning) << "Pen input requires Windows 10 1809 or later"sv; + return; + } + + // If there's not already a virtual pen device, create one now + if (!raw->pen) { + if (pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { + BOOST_LOG(info) << "Creating virtual pen input device"sv; + raw->pen = raw->global->fnCreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT); + if (!raw->pen) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to create virtual pen device: "sv << err; + return; + } + } + else { + // No need to cancel anything if we had no pen input device + return; + } + } + + // Cancel pen repeat callbacks + if (raw->penRepeatTask) { + task_pool.cancel(raw->penRepeatTask); + raw->penRepeatTask = nullptr; + } + + raw->penInfo.type = PT_PEN; + + auto &penInfo = raw->penInfo.penInfo; + penInfo.pointerInfo.pointerType = PT_PEN; + penInfo.pointerInfo.pointerId = 0; + + // Populate shared pointer info fields + populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y); + + // Windows only supports a single pen button, so send all buttons as the barrel button + if (pen.penButtons) { + penInfo.penFlags |= PEN_FLAG_BARREL; + } + else { + penInfo.penFlags &= ~PEN_FLAG_BARREL; + } + + switch (pen.toolType) { + default: + case LI_TOOL_TYPE_PEN: + penInfo.penFlags &= ~PEN_FLAG_ERASER; + break; + case LI_TOOL_TYPE_ERASER: + penInfo.penFlags |= PEN_FLAG_ERASER; + break; + case LI_TOOL_TYPE_UNKNOWN: + // Leave tool flags alone + break; + } + + penInfo.penMask = PEN_MASK_NONE; + + // Windows doesn't support hover distance, so only pass pressure/distance when the pointer is in contact + if ((penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) && pen.pressureOrDistance != 0.0f) { + penInfo.penMask |= PEN_MASK_PRESSURE; + + // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses + penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024); + } + else { + // The default pen pressure is 0 + penInfo.pressure = 0; + } + + if (pen.rotation != LI_ROT_UNKNOWN) { + penInfo.penMask |= PEN_MASK_ROTATION; + penInfo.rotation = pen.rotation; + } + else { + penInfo.rotation = 0; + } + + // We require rotation and tilt to perform the polar to cartesian conversion + if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) { + auto rotationRads = pen.rotation * (M_PI / 180.f); + + // Convert into cartesian coordinates + penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y; + penInfo.tiltX = (INT32) (std::cos(rotationRads) * pen.tilt); + penInfo.tiltY = (INT32) (std::sin(rotationRads) * pen.tilt); + } + else { + penInfo.tiltX = 0; + penInfo.tiltY = 0; + } + + if (!raw->global->fnInjectSyntheticPointerInput(raw->pen, &raw->penInfo, 1)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to inject virtual pen input: "sv << err; + return; + } + + // Clear pointer flags that should only remain set for one frame + penInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS; + + // If we still have an active pen interaction, refresh the pen state periodically + if (penInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id; + } + } + void unicode(input_t &input, char *utf8, int size) { // We can do no worse than one UTF-16 character per byte of UTF-8 @@ -611,7 +1131,7 @@ namespace platf { /** * @brief Creates a new virtual gamepad. - * @param input The input context. + * @param input The global input context. * @param id The gamepad ID. * @param metadata Controller metadata from client (empty if none provided). * @param feedback_queue The queue for posting messages back to the client. @@ -870,7 +1390,7 @@ namespace platf { /** * @brief Sends a gamepad touch event to the OS. - * @param input The input context. + * @param input The global input context. * @param touch The touch event. */ void @@ -970,7 +1490,7 @@ namespace platf { /** * @brief Sends a gamepad motion event to the OS. - * @param input The input context. + * @param input The global input context. * @param motion The motion event. */ void @@ -1002,7 +1522,7 @@ namespace platf { /** * @brief Sends a gamepad battery event to the OS. - * @param input The input context. + * @param input The global input context. * @param battery The battery event. */ void @@ -1112,6 +1632,14 @@ namespace platf { caps |= platform_caps::controller_touch; } + // We support pen and touch input on Win10 1809+ + if (GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice") != nullptr) { + caps |= platform_caps::pen_touch; + } + else { + BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; + } + return caps; } } // namespace platf diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c index f2cea4d6b0e..0f17b4d0c5f 160000 --- a/third-party/moonlight-common-c +++ b/third-party/moonlight-common-c @@ -1 +1 @@ -Subproject commit f2cea4d6b0e5a784150e90cd505f76015c2ace81 +Subproject commit 0f17b4d0c5f40842bb81fec0f2f6b5afbf182d04