Implement pen and touch support for Windows

This commit is contained in:
Cameron Gutman 2023-06-25 20:47:30 -05:00
parent 43463a9d1e
commit bd68aebe4c
6 changed files with 914 additions and 34 deletions

View File

@ -11,6 +11,7 @@ extern "C" {
#include <bitset>
#include <chrono>
#include <cmath>
#include <list>
#include <thread>
#include <unordered_map>
@ -90,6 +91,18 @@ namespace input {
return boost::endian::endian_load<float, sizeof(float), boost::endian::order::little>(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_id_t, bool> key_press {};
static std::array<std::uint8_t, 5> 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<gamepad_t> gamepads;
std::unique_ptr<platf::client_input_t> client_context;
safe::mail_raw_t::event_t<input::touch_port_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<float, float>
client_to_touchport(std::shared_ptr<input_t> &input, const std::pair<float, float> &val, const std::pair<float, float> &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<float, float> &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<float, float>
scale_client_contact_area(const std::pair<float, float> &val, uint16_t rotation, const std::pair<float, float> &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_t> &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_t> &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_t> &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;

View File

@ -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<client_input_t>
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.

View File

@ -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<client_input_t>
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

View File

@ -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<client_input_t>
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

View File

@ -2,6 +2,7 @@
* @file src/platform/windows/input.cpp
* @brief todo
*/
#define WINVER 0x0A00
#include <windows.h>
#include <cmath>
@ -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<client_input_t>
allocate_client_input_context(input_t &input) {
return std::make_unique<client_input_raw_t>(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<LONG>(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2));
touchInfo.rcContact.right = std::min<LONG>(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2));
touchInfo.rcContact.top = std::max<LONG>(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2));
touchInfo.rcContact.bottom = std::min<LONG>(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

@ -1 +1 @@
Subproject commit f2cea4d6b0e5a784150e90cd505f76015c2ace81
Subproject commit 0f17b4d0c5f40842bb81fec0f2f6b5afbf182d04