#include "touch.h" #include #include #include #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; AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto; bool ASPECT_COMPENSATION_GAME = false; // state DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT; long DISPLAY_SIZE_X = 1920L; long DISPLAY_SIZE_Y = 1080L; long DISPLAY_NATIVE_X = 0L; long DISPLAY_NATIVE_Y = 0L; bool DISPLAY_INITIALIZED = false; static void update_current_display_mode(); bool aspect_compensation_enabled() { switch (ASPECT_COMPENSATION_MODE) { case AspectMode::On: return true; case AspectMode::Off: return false; default: return ASPECT_COMPENSATION_GAME; } } // compute the centered letterbox inset (fraction cropped from each side) that results // from showing the current display mode on a panel with a different native aspect // ratio. only one of inset_x / inset_y is ever non-zero. static void compute_letterbox_inset(double &inset_x, double &inset_y) { inset_x = 0.0; inset_y = 0.0; // native resolution in the current display orientation long native_x = DISPLAY_NATIVE_X; long native_y = DISPLAY_NATIVE_Y; if ((DISPLAY_SIZE_Y >= DISPLAY_SIZE_X) != (native_y >= native_x)) { long tmp = native_x; native_x = native_y; native_y = tmp; } // compare the displayed aspect ratio against the panel's native one; ignore // tiny mismatches (e.g. 1366x768 vs true 16:9) that would only add a sub-pixel // inset, so near-matched panels stay an exact no-op double current_aspect = (double) DISPLAY_SIZE_X / (double) DISPLAY_SIZE_Y; double native_aspect = (double) native_x / (double) native_y; constexpr double aspect_epsilon = 0.01; if (current_aspect < native_aspect - aspect_epsilon) { // image is narrower than the panel -> bars on left/right inset_x = (1.0 - current_aspect / native_aspect) / 2.0; } else if (current_aspect > native_aspect + aspect_epsilon) { // image is wider than the panel -> bars on top/bottom inset_y = (1.0 - native_aspect / current_aspect) / 2.0; } } // undo letterboxing: remap the normalized (0.0 - 1.0) digitizer coordinates, which span // the whole physical panel, into the displayed image region. returns false when the // touch lands in the black bars (outside the image) and should be ignored. static bool remap_aspect_compensation(double &norm_x, double &norm_y) { if (DISPLAY_NATIVE_X <= 0 || DISPLAY_NATIVE_Y <= 0) { return true; } double inset_x, inset_y; compute_letterbox_inset(inset_x, inset_y); // log once the first time the compensation actually engages (non-zero inset) static bool logged_active = false; if (!logged_active && (inset_x > 0.0 || inset_y > 0.0)) { logged_active = true; log_info("rawinput", "aspect compensation active: display {}x{}, native {}x{}, inset x={:.4f} y={:.4f}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y, (int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y, inset_x, inset_y); } // ignore touches inside the black bars (outside the displayed image) if ((inset_x > 0.0 && (norm_x < inset_x || norm_x > 1.0 - inset_x)) || (inset_y > 0.0 && (norm_y < inset_y || norm_y > 1.0 - inset_y))) { return false; } // remap the displayed image region back to the full 0.0 - 1.0 range if (inset_x > 0.0) { norm_x = (norm_x - inset_x) / (1.0 - 2.0 * inset_x); } if (inset_y > 0.0) { norm_y = (norm_y - inset_y) / (1.0 - 2.0 * inset_y); } return true; } // determine the native (maximum) resolution advertised by the primary display device. // used to detect and undo letterboxing when a non-native display mode is in use. // falls back to the current display size if enumeration yields nothing larger. static void detect_native_resolution() { DISPLAY_NATIVE_X = DISPLAY_SIZE_X; DISPLAY_NATIVE_Y = DISPLAY_SIZE_Y; DEVMODE native_mode{}; native_mode.dmSize = sizeof(native_mode); for (int i = 0; EnumDisplaySettings(nullptr, i, &native_mode); i++) { if ((long) native_mode.dmPelsWidth * (long) native_mode.dmPelsHeight > DISPLAY_NATIVE_X * DISPLAY_NATIVE_Y) { DISPLAY_NATIVE_X = (long) native_mode.dmPelsWidth; DISPLAY_NATIVE_Y = (long) native_mode.dmPelsHeight; } } } 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_warning("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) { update_current_display_mode(); } // 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 thread_local std::vector touch_points; touch_points.clear(); 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 thread_local std::vector touch_removes; thread_local std::vector touch_writes; thread_local std::vector touch_modifications; touch_removes.clear(); touch_writes.clear(); touch_modifications.clear(); touch_removes.reserve(touch.touch_points.size() + touch_points.size()); touch_writes.reserve(touch_points.size()); touch_modifications.reserve(touch_points.size()); // drop every touch point with the given id (marking it as released) auto remove_touch_point = [&] (DWORD id) { touch_removes.push_back(id); touch.touch_points.erase(std::remove_if( touch.touch_points.begin(), touch.touch_points.end(), [id] (const HIDTouchPoint &x) { return x.id == id; }), touch.touch_points.end()); }; 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_touch_point(hid_tp.id); } } else { // start from the normalized (0.0 - 1.0) digitizer coordinates double norm_x = hid_tp.x; double norm_y = hid_tp.y; // undo letterboxing so raw input touches line up with the displayed // image; touches that land in the black bars are treated as released if (aspect_compensation_enabled() && !remap_aspect_compensation(norm_x, norm_y)) { if (existing != touch.touch_points.end()) { remove_touch_point(hid_tp.id); } continue; } // scale the normalized coordinates to the monitor to obtain the // physical screen position of the touch TouchPoint tp { .id = hid_tp.id, .x = (long) (norm_x * DISPLAY_SIZE_X), .y = (long) (norm_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()) { // a HID digitizer reports only a fixed number of contacts per report and // cycles the remaining ones through following reports, so a contact can be // absent for up to this many reports before it comes back around auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1; auto system_time_ms = get_system_milliseconds(); // extend every tracked contact to the current cycle length without ever // shrinking it; otherwise a rising contact count (e.g. rapid taps) lengthens // the cycle and can starve a stationary held contact into being reaped before // it is reported again. the 50ms last-report timeout remains the hard backstop for (auto &hid_tp : touch.touch_points) { if (hid_tp.ttl < ttl) { hid_tp.ttl = ttl; } } // only contacts actually seen this report refresh their last-report time for (auto &hid_tp_id : touch_modifications) { for (auto &hid_tp : touch.touch_points) { if (hid_tp.id == hid_tp_id) { 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 thread_local std::vector touch_removes; touch_removes.clear(); touch_removes.reserve(touch.touch_points.size()); 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; } static void update_current_display_mode() { // 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", "primary 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; } void display_update() { // check if disabled if (DISABLED) return; update_current_display_mode(); if (aspect_compensation_enabled()) { // determine the native (maximum) resolution of the primary display; used to // detect and undo letterboxing when a non-native display mode is in use detect_native_resolution(); log_info("rawinput", "primary display native size: {}x{}", (int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y); } } }