mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
26b3b64794
## Link to GitHub Issue or related Pull Request, if one exists Fixes #648 ## Description of change Deal with devices in hybrid packet mode that send garbage data in second packet when it's out of bounds (outside the number of contact points). MSDN says they're supposed to be (0, 0), but some devices don't follow the spec. ## Testing I don't have a device that reports garbage values but this should fix it in theory. I tested on my Dell touch monitor which is in hybrid mode & sends 5 touches at a time, but can split across two packets to send up to 10 touches. It sends (0, 0) for the remaining slots when there are 6-9 touch points, but confirmed that there was no regression.
565 lines
19 KiB
C++
565 lines
19 KiB
C++
#include "touch.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <windows.h>
|
|
#include <versionhelpers.h>
|
|
|
|
#include "util/logging.h"
|
|
#include "util/time.h"
|
|
#include "touch/touch.h"
|
|
|
|
// std::min
|
|
#ifdef min
|
|
#undef min
|
|
#endif
|
|
|
|
// std::max
|
|
#ifdef max
|
|
#undef max
|
|
#endif
|
|
|
|
namespace rawinput::touch {
|
|
|
|
// settings
|
|
bool DISABLED = false;
|
|
bool INVERTED = false;
|
|
|
|
// state
|
|
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
|
|
long DISPLAY_SIZE_X = 1920L;
|
|
long DISPLAY_SIZE_Y = 1080L;
|
|
bool DISPLAY_INITIALIZED = false;
|
|
|
|
bool is_touchscreen(Device *device) {
|
|
|
|
// check if disabled
|
|
if (DISABLED) {
|
|
return false;
|
|
}
|
|
|
|
// check device type
|
|
if (device->type != HID) {
|
|
return false;
|
|
}
|
|
|
|
auto *hid = device->hidInfo;
|
|
auto &attributes = hid->attributes;
|
|
|
|
// filter by VID/PID
|
|
if (attributes.VendorID > 0) {
|
|
|
|
// P2418HT
|
|
// touch points apparently aren't released and will stay
|
|
if (attributes.VendorID == 0x1FD2 && attributes.ProductID == 0x6103) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ignore the "Touch Pad" (0x05) usage under the "Digitizers" (0x0d) usage page
|
|
if (hid->caps.UsagePage == 0x0d && hid->caps.Usage == 0x05) {
|
|
return false;
|
|
}
|
|
|
|
// check description
|
|
if (device->desc == "HID-compliant touch screen") {
|
|
// TODO: this only works on english OS (?)
|
|
return true;
|
|
}
|
|
|
|
// we can also check if there's touch point values inside
|
|
for (const auto &value_name : hid->value_caps_names) {
|
|
if (value_name == "Contact identifier") {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// nope this one probably not
|
|
return false;
|
|
}
|
|
|
|
void enable(Device *device) {
|
|
if (device->type == HID) {
|
|
device->hidInfo->touch.valid = true;
|
|
log_info("rawinput", "enabled touchscreen device: {} ({})", device->desc, device->name);
|
|
} else {
|
|
log_fatal("rawinput", "tried to enable touch functionality on non HID device");
|
|
}
|
|
}
|
|
|
|
void disable(Device *device) {
|
|
if (device->type == HID) {
|
|
device->hidInfo->touch.valid = false;
|
|
log_info("rawinput", "disabled touchscreen device: {} ({})", device->desc, device->name);
|
|
} else
|
|
log_fatal("rawinput", "tried to disable touch functionality on non HID device");
|
|
}
|
|
|
|
void update_input(Device *device) {
|
|
|
|
// check type
|
|
if (device->type != HID) {
|
|
log_fatal("rawinput", "touch update called on non HID device");
|
|
return;
|
|
}
|
|
|
|
// get touch info
|
|
auto hid = device->hidInfo;
|
|
auto &touch = hid->touch;
|
|
if (!touch.valid) {
|
|
|
|
// not a registered touchscreen
|
|
return;
|
|
}
|
|
|
|
// parse elements
|
|
if (!touch.parsed_elements) {
|
|
log_info("rawinput", "parsing touch elements");
|
|
|
|
// clear lists first
|
|
touch.elements_contact_count.clear();
|
|
touch.elements_contact_identifier.clear();
|
|
touch.elements_x.clear();
|
|
touch.elements_y.clear();
|
|
touch.elements_width.clear();
|
|
touch.elements_height.clear();
|
|
touch.elements_pressed.clear();
|
|
touch.elements_pressure.clear();
|
|
|
|
// get value indices
|
|
for (int i = 0; i < (int) hid->value_caps_names.size(); i++) {
|
|
auto &name = hid->value_caps_names[i];
|
|
if (name == "Contact count") {
|
|
touch.elements_contact_count.push_back(i);
|
|
} else if (name == "X") {
|
|
touch.elements_x.push_back(i);
|
|
} else if (name == "Y") {
|
|
touch.elements_y.push_back(i);
|
|
} else if (name == "Width") {
|
|
touch.elements_width.push_back(i);
|
|
} else if (name == "Height") {
|
|
touch.elements_height.push_back(i);
|
|
} else if (name == "Contact identifier") {
|
|
touch.elements_contact_identifier.push_back(i);
|
|
} else if (name == "Pressure") {
|
|
touch.elements_pressure.push_back(i);
|
|
}
|
|
}
|
|
|
|
// get button indices
|
|
for (int i = 0; i < (int) hid->button_caps_names.size(); i++) {
|
|
auto &name = hid->button_caps_names[i];
|
|
if (name == "Tip Switch") {
|
|
touch.elements_pressed.push_back(i);
|
|
}
|
|
}
|
|
|
|
// check sizes
|
|
auto touch_size = touch.elements_contact_identifier.size();
|
|
if (touch_size != touch.elements_x.size() ||
|
|
touch_size != touch.elements_y.size() ||
|
|
touch_size != touch.elements_pressed.size())
|
|
{
|
|
log_info("rawinput", "touch element size mismatch: {}:contacts, {}:x, {}:y, {}:pressed",
|
|
touch.elements_contact_identifier.size(),
|
|
touch.elements_x.size(),
|
|
touch.elements_y.size(),
|
|
touch.elements_pressed.size());
|
|
disable(device);
|
|
return;
|
|
}
|
|
|
|
// mark as parsed
|
|
touch.parsed_elements = true;
|
|
log_info("rawinput", "touch elements parsed: {} status fields", touch.elements_x.size());
|
|
}
|
|
|
|
// check if display is initialized
|
|
if (!DISPLAY_INITIALIZED) {
|
|
display_update();
|
|
}
|
|
|
|
// update timeouts here as well so events are in the right order
|
|
update_timeouts(device);
|
|
|
|
// determine the number of touches contained in this report, defaulting to the size of `elements_x`
|
|
size_t touch_report_count = touch.elements_x.size();
|
|
if (!touch.elements_contact_count.empty()) {
|
|
|
|
// support devices that have multiple "Contact count" fields, for some reason
|
|
size_t contact_count = 0;
|
|
for (auto &index : touch.elements_contact_count) {
|
|
contact_count = std::max(contact_count, (size_t) hid->value_states_raw[index]);
|
|
}
|
|
|
|
// https://learn.microsoft.com/en-us/windows-hardware/design/component-guidelines/touchscreen-packet-reporting-modes
|
|
// hybrid mode devices will report a contact count of 0 for subsequent reports that are
|
|
// part of the same initial frame
|
|
// most laptops and touchscreens are like this - they have the contact count set to 5 but can actually report up to 10 fingers
|
|
if (contact_count > 0) {
|
|
if (contact_count > touch_report_count) {
|
|
touch.remaining_contact_count = contact_count - touch_report_count;
|
|
} else {
|
|
touch_report_count = contact_count;
|
|
touch.remaining_contact_count = 0;
|
|
}
|
|
} else if (touch.remaining_contact_count > 0) {
|
|
if (touch.remaining_contact_count > touch_report_count) {
|
|
touch.remaining_contact_count -= touch_report_count;
|
|
} else {
|
|
touch_report_count = touch.remaining_contact_count;
|
|
touch.remaining_contact_count = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// iterate all input data and get touch points
|
|
std::vector<HIDTouchPoint> touch_points;
|
|
touch_points.reserve(touch.elements_x.size());
|
|
for (size_t i = 0; i < touch.elements_x.size(); i++) {
|
|
|
|
// if there are no more touch events to handle, exit out early
|
|
if (touch_report_count == 0) {
|
|
break;
|
|
}
|
|
touch_report_count--;
|
|
|
|
// build touch point
|
|
HIDTouchPoint hid_tp{};
|
|
auto pos_x = hid->value_states[touch.elements_x[i]];
|
|
auto pos_y = hid->value_states[touch.elements_y[i]];
|
|
switch (DISPLAY_ORIENTATION) {
|
|
case DMDO_DEFAULT:
|
|
hid_tp.x = pos_x;
|
|
hid_tp.y = pos_y;
|
|
break;
|
|
case DMDO_90:
|
|
hid_tp.x = 1.f - pos_y;
|
|
hid_tp.y = pos_x;
|
|
break;
|
|
case DMDO_180:
|
|
hid_tp.x = 1.f - pos_x;
|
|
hid_tp.y = 1.f - pos_y;
|
|
break;
|
|
case DMDO_270:
|
|
hid_tp.x = pos_y;
|
|
hid_tp.y = 1.f - pos_x;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// optionally invert
|
|
if (INVERTED) {
|
|
hid_tp.x = 1.f - hid_tp.x;
|
|
hid_tp.y = 1.f - hid_tp.y;
|
|
}
|
|
|
|
// generate ID (hopefully unique)
|
|
hid_tp.id = (DWORD) hid->value_states_raw[touch.elements_contact_identifier[i]];
|
|
hid_tp.id += (DWORD) (0xFFFFFF + device->id * 512);
|
|
|
|
//std::string src = "(none)";
|
|
|
|
// check if tip switch is down
|
|
size_t index_pressed = touch.elements_pressed[i];
|
|
for (auto &button_states : hid->button_states) {
|
|
if (index_pressed < button_states.size()) {
|
|
hid_tp.down = button_states[index_pressed];
|
|
|
|
/*
|
|
if (hid_tp.down)
|
|
src = "pressed (index: " + to_string(touch.elements_pressed[i]) + ", state: " + to_string(button_states[index_pressed]) +")";
|
|
*/
|
|
|
|
break;
|
|
} else
|
|
index_pressed -= button_states.size();
|
|
}
|
|
|
|
// check width/height of touch point to see if pressed
|
|
float width = 0.f, height = 0.f;
|
|
if (!hid_tp.down && i < touch.elements_width.size() && i < touch.elements_height.size()) {
|
|
width = hid->value_states[touch.elements_width[i]];
|
|
height = hid->value_states[touch.elements_height[i]];
|
|
hid_tp.down = width > 0.f && height > 0.f;
|
|
|
|
/*
|
|
if (hid_tp.down)
|
|
src = "width_height (width index: " + to_string(touch.elements_width[i]) + ", height index: " + to_string(touch.elements_height[i]) + ")";
|
|
*/
|
|
}
|
|
|
|
// so last thing we can check is the pressure
|
|
if (!hid_tp.down && i < touch.elements_pressure.size()) {
|
|
auto pressure = hid->value_states[touch.elements_pressure[i]];
|
|
hid_tp.down = pressure > 0.f;
|
|
|
|
/*
|
|
if (hid_tp.down)
|
|
src = "pressure (index: " + to_string(touch.elements_pressure[i]) + ")";
|
|
*/
|
|
}
|
|
|
|
/*
|
|
log_info("rawinput",
|
|
"touch i: " + to_string(i) +
|
|
" (id: " + to_string(hid_tp.id) +
|
|
"), ci: " + to_string(hid->value_states_raw[touch.elements_contact_identifier[i]]) +
|
|
", x: " + to_string(pos_x) +
|
|
", y: " + to_string(pos_y) +
|
|
", width: " + to_string(width) +
|
|
", height: " + to_string(height) +
|
|
", down: " + to_string(hid_tp.down) +
|
|
", src: " + src);
|
|
*/
|
|
|
|
// add to touch points
|
|
touch_points.emplace_back(hid_tp);
|
|
}
|
|
|
|
// process touch points
|
|
std::vector<DWORD> touch_removes;
|
|
std::vector<TouchPoint> touch_writes;
|
|
std::vector<DWORD> touch_modifications;
|
|
for (auto &hid_tp : touch_points) {
|
|
|
|
// check if existing
|
|
auto existing = std::find_if(
|
|
touch.touch_points.begin(),
|
|
touch.touch_points.end(),
|
|
[hid_tp] (const HIDTouchPoint &x) {
|
|
return x.id == hid_tp.id;
|
|
});
|
|
|
|
/*
|
|
ssize_t ttl = -1;
|
|
int64_t last_report = -1;
|
|
if (existing != touch.touch_points.end()) {
|
|
ttl = existing->ttl;
|
|
last_report = existing->last_report;
|
|
}
|
|
|
|
log_info("rawinput",
|
|
"id: " + to_string(hid_tp.id) +
|
|
", existing: " + to_string(existing != touch.touch_points.end()) +
|
|
", ttl: " + to_string(ttl) +
|
|
", last_report: " + to_string(last_report) +
|
|
", down: " + to_string(hid_tp.down));
|
|
*/
|
|
|
|
// check if pressed
|
|
if (!hid_tp.down) {
|
|
|
|
// only remove if it exists
|
|
if (existing != touch.touch_points.end()) {
|
|
|
|
// remove all touch points with this ID
|
|
touch_removes.push_back(hid_tp.id);
|
|
touch.touch_points.erase(std::remove_if(
|
|
touch.touch_points.begin(),
|
|
touch.touch_points.end(),
|
|
[hid_tp] (const HIDTouchPoint &x) {
|
|
return x.id == hid_tp.id;
|
|
}), touch.touch_points.end());
|
|
}
|
|
} else {
|
|
|
|
// write touch point
|
|
TouchPoint tp {
|
|
.id = hid_tp.id,
|
|
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
|
|
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
|
|
.mouse = false,
|
|
};
|
|
touch_writes.push_back(tp);
|
|
touch_modifications.push_back(hid_tp.id);
|
|
|
|
// check if existing
|
|
if (existing == touch.touch_points.end()) {
|
|
|
|
// add new touch point
|
|
touch.touch_points.push_back(hid_tp);
|
|
|
|
} else {
|
|
|
|
// update existing touch point
|
|
*existing = hid_tp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// set TTL and last report time
|
|
if (!touch.elements_x.empty() && !touch_modifications.empty()) {
|
|
auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1;
|
|
auto system_time_ms = get_system_milliseconds();
|
|
for (auto &hid_tp_id : touch_modifications) {
|
|
for (auto &hid_tp : touch.touch_points) {
|
|
if (hid_tp.id == hid_tp_id) {
|
|
hid_tp.ttl = ttl;
|
|
hid_tp.last_report = system_time_ms;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove dead touch points
|
|
auto ttl_it = touch.touch_points.begin();
|
|
while (ttl_it != touch.touch_points.end()) {
|
|
auto &tp = *ttl_it;
|
|
if (tp.ttl == 0) {
|
|
touch_removes.push_back(tp.id);
|
|
ttl_it = touch.touch_points.erase(ttl_it);
|
|
} else {
|
|
tp.ttl--;
|
|
ttl_it++;
|
|
}
|
|
}
|
|
|
|
#ifndef SPICETOOLS_SPICECFG_STANDALONE
|
|
|
|
// update touch module
|
|
touch_remove_points(&touch_removes);
|
|
touch_write_points(&touch_writes);
|
|
#endif
|
|
}
|
|
|
|
void update_timeouts(Device *device) {
|
|
|
|
// check type
|
|
if (device->type != HID) {
|
|
log_fatal("rawinput", "touch timeout update called on non HID device");
|
|
return;
|
|
}
|
|
|
|
// get touch info
|
|
auto hid = device->hidInfo;
|
|
auto &touch = hid->touch;
|
|
if (!touch.valid) {
|
|
return; // not a registered touchscreen
|
|
}
|
|
|
|
// calculate deadline - we allow the devices 50ms of not sending shit
|
|
auto deadline = get_system_milliseconds() - 50;
|
|
|
|
// check touch points
|
|
std::vector<DWORD> touch_removes;
|
|
auto touch_it = touch.touch_points.begin();
|
|
while (touch_it != touch.touch_points.end()) {
|
|
auto &hid_tp = *touch_it;
|
|
|
|
// see if it's behind the deadline
|
|
if (hid_tp.last_report < deadline) {
|
|
|
|
// oops it's gone
|
|
touch_removes.push_back(hid_tp.id);
|
|
touch_it = touch.touch_points.erase(touch_it);
|
|
|
|
} else {
|
|
|
|
// check the next device
|
|
touch_it++;
|
|
}
|
|
}
|
|
|
|
#ifndef SPICETOOLS_SPICECFG_STANDALONE
|
|
|
|
// remove from touch module
|
|
touch_remove_points(&touch_removes);
|
|
#endif
|
|
}
|
|
|
|
void update_timeouts(RawInputManager *manager) {
|
|
|
|
// iterate all devices
|
|
for (auto &device : manager->devices_get()) {
|
|
|
|
// check if it's a valid touchscreen
|
|
if (device.type == HID && device.hidInfo->touch.valid) {
|
|
|
|
// lock n' load
|
|
device.mutex->lock();
|
|
update_timeouts(&device);
|
|
device.mutex->unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool is_enabled(RawInputManager *manager) {
|
|
|
|
// check if disabled or manager is null
|
|
if (DISABLED || manager == nullptr)
|
|
return false;
|
|
|
|
// check if at least one device is marked as valid touchscreen
|
|
for (auto &device : manager->devices_get()) {
|
|
if (device.type == HID && device.hidInfo->touch.valid) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// no valid touch screen found
|
|
return false;
|
|
}
|
|
|
|
void display_update() {
|
|
|
|
// check if disabled
|
|
if (DISABLED)
|
|
return;
|
|
|
|
// determine monitor size
|
|
static RECT display_rect;
|
|
GetWindowRect(GetDesktopWindow(), &display_rect);
|
|
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
|
|
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
|
|
log_info("rawinput", "display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
|
|
|
|
// determine monitor orientation
|
|
DEVMODE display_mode{};
|
|
display_mode.dmSize = sizeof(DEVMODE);
|
|
if (!EnumDisplaySettingsEx(nullptr, ENUM_CURRENT_SETTINGS, &display_mode, EDS_RAWMODE)) {
|
|
log_info("rawinput", "failed to determine monitor mode");
|
|
} else if (display_mode.dmFields & DM_DISPLAYORIENTATION) {
|
|
DISPLAY_ORIENTATION = display_mode.dmDisplayOrientation;
|
|
switch (DISPLAY_ORIENTATION) {
|
|
case DMDO_DEFAULT:
|
|
log_info("rawinput", "display rotation: 0");
|
|
break;
|
|
case DMDO_90:
|
|
log_info("rawinput", "display rotation: 90");
|
|
break;
|
|
case DMDO_180:
|
|
log_info("rawinput", "display rotation: 180");
|
|
break;
|
|
case DMDO_270:
|
|
log_info("rawinput", "display rotation: 270");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// another XP fix
|
|
if (!IsWindowsVistaOrGreater()) {
|
|
switch (DISPLAY_ORIENTATION) {
|
|
case DMDO_90:
|
|
DISPLAY_ORIENTATION = DMDO_180;
|
|
log_info("rawinput", "flipping to 180");
|
|
break;
|
|
case DMDO_270:
|
|
DISPLAY_ORIENTATION = DMDO_90;
|
|
log_info("rawinput", "flipping to 90");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
log_info("rawinput", "failed to determine monitor orientation");
|
|
}
|
|
|
|
// mark as initialized
|
|
DISPLAY_INITIALIZED = true;
|
|
}
|
|
}
|