Use XInput2 RawMotion to generate MouseMotion events

The current system for capturing the mouse and generating motion events on X11
has issues with inaccurate and lopsided input. This is because both
XQueryPointer and XWarpPointer work in terms of integer coordinates when the
underlying X11 input driver may be tracking the mouse using subpixel
coordinates. When warping the pointer, the fractional part of the pointer
position is discarded.

To work around this issue, the fix uses raw motion events from XInput 2. These
events report relative motion and are not affected by pointer warping.
Additionally, this means Godot is able to detect motion at a higher resolution
under X11. Because this is raw mouse input, it is not affected by the user's
pointer speed and acceleration settings. This is the same system as SDL2 uses
for its relative motion.

Multitouch input on X requires XInput 2.2. Raw motion events require
XInput 2.0. Since 2.0 is old enough, this is now the minimum requirement to
use Godot on X.
This commit is contained in:
Cosmic Chip Socket 2018-08-17 18:59:26 -04:00 committed by Rémi Verschelde
parent 5f32fc8208
commit cf124b1415
3 changed files with 282 additions and 130 deletions

View file

@ -48,6 +48,11 @@ def can_build():
print("xrender not found.. x11 disabled.")
return False
x11_error = os.system("pkg-config xi --modversion > /dev/null ")
if (x11_error):
print("xi not found.. Aborting.")
return False
return True
def get_opts():
@ -170,13 +175,9 @@ def configure(env):
env.ParseConfig('pkg-config xinerama --cflags --libs')
env.ParseConfig('pkg-config xrandr --cflags --libs')
env.ParseConfig('pkg-config xrender --cflags --libs')
env.ParseConfig('pkg-config xi --cflags --libs')
if (env['touch']):
x11_error = os.system("pkg-config xi --modversion > /dev/null ")
if (x11_error):
print("xi not found.. cannot build with touch. Aborting.")
sys.exit(255)
env.ParseConfig('pkg-config xi --cflags --libs')
env.Append(CPPFLAGS=['-DTOUCH_ENABLED'])
# FIXME: Check for existence of the libs before parsing their flags with pkg-config

View file

@ -77,6 +77,13 @@
#include <X11/XKBlib.h>
// 2.2 is the first release with multitouch
#define XINPUT_CLIENT_VERSION_MAJOR 2
#define XINPUT_CLIENT_VERSION_MINOR 2
static const double abs_resolution_mult = 10000.0;
static const double abs_resolution_range_mult = 10.0;
void OS_X11::initialize_core() {
crash_handler.initialize();
@ -170,48 +177,12 @@ Error OS_X11::initialize(const VideoMode &p_desired, int p_video_driver, int p_a
}
}
#ifdef TOUCH_ENABLED
if (!XQueryExtension(x11_display, "XInputExtension", &touch.opcode, &event_base, &error_base)) {
print_verbose("XInput extension not available, touch support disabled.");
} else {
// 2.2 is the first release with multitouch
int xi_major = 2;
int xi_minor = 2;
if (XIQueryVersion(x11_display, &xi_major, &xi_minor) != Success) {
print_verbose(vformat("XInput 2.2 not available (server supports %d.%d), touch support disabled.", xi_major, xi_minor));
touch.opcode = 0;
} else {
int dev_count;
XIDeviceInfo *info = XIQueryDevice(x11_display, XIAllDevices, &dev_count);
for (int i = 0; i < dev_count; i++) {
XIDeviceInfo *dev = &info[i];
if (!dev->enabled)
continue;
if (!(dev->use == XIMasterPointer || dev->use == XIFloatingSlave))
continue;
bool direct_touch = false;
for (int j = 0; j < dev->num_classes; j++) {
if (dev->classes[j]->type == XITouchClass && ((XITouchClassInfo *)dev->classes[j])->mode == XIDirectTouch) {
direct_touch = true;
break;
}
}
if (direct_touch) {
touch.devices.push_back(dev->deviceid);
print_verbose("XInput: Using touch device: " + String(dev->name));
}
}
XIFreeDeviceInfo(info);
if (!touch.devices.size()) {
print_verbose("XInput: No touch devices found.");
}
}
if (!refresh_device_info()) {
OS::get_singleton()->alert("Your system does not support XInput 2.\n"
"Please upgrade your distribution.",
"Unable to initialize XInput");
return ERR_UNAVAILABLE;
}
#endif
xim = XOpenIM(x11_display, NULL, NULL, NULL);
@ -415,34 +386,42 @@ Error OS_X11::initialize(const VideoMode &p_desired, int p_video_driver, int p_a
XChangeWindowAttributes(x11_display, x11_window, CWEventMask, &new_attr);
static unsigned char all_mask_data[XIMaskLen(XI_LASTEVENT)] = {};
static unsigned char all_master_mask_data[XIMaskLen(XI_LASTEVENT)] = {};
xi.all_event_mask.deviceid = XIAllDevices;
xi.all_event_mask.mask_len = sizeof(all_mask_data);
xi.all_event_mask.mask = all_mask_data;
xi.all_master_event_mask.deviceid = XIAllMasterDevices;
xi.all_master_event_mask.mask_len = sizeof(all_master_mask_data);
xi.all_master_event_mask.mask = all_master_mask_data;
XISetMask(xi.all_event_mask.mask, XI_HierarchyChanged);
XISetMask(xi.all_master_event_mask.mask, XI_DeviceChanged);
XISetMask(xi.all_master_event_mask.mask, XI_RawMotion);
#ifdef TOUCH_ENABLED
if (touch.devices.size()) {
// Must be alive after this block
static unsigned char mask_data[XIMaskLen(XI_LASTEVENT)] = {};
touch.event_mask.deviceid = XIAllDevices;
touch.event_mask.mask_len = sizeof(mask_data);
touch.event_mask.mask = mask_data;
XISetMask(touch.event_mask.mask, XI_TouchBegin);
XISetMask(touch.event_mask.mask, XI_TouchUpdate);
XISetMask(touch.event_mask.mask, XI_TouchEnd);
XISetMask(touch.event_mask.mask, XI_TouchOwnership);
XISelectEvents(x11_display, x11_window, &touch.event_mask, 1);
// Disabled by now since grabbing also blocks mouse events
// (they are received as extended events instead of standard events)
/*XIClearMask(touch.event_mask.mask, XI_TouchOwnership);
// Grab touch devices to avoid OS gesture interference
for (int i = 0; i < touch.devices.size(); ++i) {
XIGrabDevice(x11_display, touch.devices[i], x11_window, CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, False, &touch.event_mask);
}*/
if (xi.touch_devices.size()) {
XISetMask(xi.all_event_mask.mask, XI_TouchBegin);
XISetMask(xi.all_event_mask.mask, XI_TouchUpdate);
XISetMask(xi.all_event_mask.mask, XI_TouchEnd);
XISetMask(xi.all_event_mask.mask, XI_TouchOwnership);
}
#endif
XISelectEvents(x11_display, x11_window, &xi.all_event_mask, 1);
XISelectEvents(x11_display, DefaultRootWindow(x11_display), &xi.all_master_event_mask, 1);
// Disabled by now since grabbing also blocks mouse events
// (they are received as extended events instead of standard events)
/*XIClearMask(xi.touch_event_mask.mask, XI_TouchOwnership);
// Grab touch devices to avoid OS gesture interference
for (int i = 0; i < xi.touch_devices.size(); ++i) {
XIGrabDevice(x11_display, xi.touch_devices[i], x11_window, CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, False, &xi.touch_event_mask);
}*/
/* set the titlebar name */
XStoreName(x11_display, x11_window, "Godot");
@ -592,6 +571,101 @@ Error OS_X11::initialize(const VideoMode &p_desired, int p_video_driver, int p_a
return OK;
}
bool OS_X11::refresh_device_info() {
int event_base, error_base;
print_verbose("XInput: Refreshing devices.");
if (!XQueryExtension(x11_display, "XInputExtension", &xi.opcode, &event_base, &error_base)) {
print_verbose("XInput extension not available. Please upgrade your distribution.");
return false;
}
int xi_major_query = XINPUT_CLIENT_VERSION_MAJOR;
int xi_minor_query = XINPUT_CLIENT_VERSION_MINOR;
if (XIQueryVersion(x11_display, &xi_major_query, &xi_minor_query) != Success) {
print_verbose(vformat("XInput 2 not available (server supports %d.%d).", xi_major_query, xi_minor_query));
xi.opcode = 0;
return false;
}
if (xi_major_query < XINPUT_CLIENT_VERSION_MAJOR || (xi_major_query == XINPUT_CLIENT_VERSION_MAJOR && xi_minor_query < XINPUT_CLIENT_VERSION_MINOR)) {
print_verbose(vformat("XInput %d.%d not available (server supports %d.%d). Touch input unavailable.",
XINPUT_CLIENT_VERSION_MAJOR, XINPUT_CLIENT_VERSION_MINOR, xi_major_query, xi_minor_query));
}
xi.absolute_devices.clear();
xi.touch_devices.clear();
int dev_count;
XIDeviceInfo *info = XIQueryDevice(x11_display, XIAllDevices, &dev_count);
for (int i = 0; i < dev_count; i++) {
XIDeviceInfo *dev = &info[i];
if (!dev->enabled)
continue;
if (!(dev->use == XIMasterPointer || dev->use == XIFloatingSlave))
continue;
bool direct_touch = false;
bool absolute_mode = false;
int resolution_x = 0;
int resolution_y = 0;
int range_min_x = 0;
int range_min_y = 0;
int range_max_x = 0;
int range_max_y = 0;
for (int j = 0; j < dev->num_classes; j++) {
#ifdef TOUCH_ENABLED
if (dev->classes[j]->type == XITouchClass && ((XITouchClassInfo *)dev->classes[j])->mode == XIDirectTouch) {
direct_touch = true;
}
#endif
if (dev->classes[j]->type == XIValuatorClass) {
XIValuatorClassInfo *class_info = (XIValuatorClassInfo *)dev->classes[j];
if (class_info->number == 0 && class_info->mode == XIModeAbsolute) {
resolution_x = class_info->resolution;
range_min_x = class_info->min;
range_max_x = class_info->max;
absolute_mode = true;
} else if (class_info->number == 1 && class_info->mode == XIModeAbsolute) {
resolution_y = class_info->resolution;
range_min_y = class_info->min;
range_max_y = class_info->max;
absolute_mode = true;
}
}
}
if (direct_touch) {
xi.touch_devices.push_back(dev->deviceid);
print_verbose("XInput: Using touch device: " + String(dev->name));
}
if (absolute_mode) {
// If no resolution was reported, use the min/max ranges.
if (resolution_x <= 0) {
resolution_x = (range_max_x - range_min_x) * abs_resolution_range_mult;
}
if (resolution_y <= 0) {
resolution_y = (range_max_y - range_min_y) * abs_resolution_range_mult;
}
xi.absolute_devices[dev->deviceid] = Vector2(abs_resolution_mult / resolution_x, abs_resolution_mult / resolution_y);
print_verbose("XInput: Absolute pointing device: " + String(dev->name));
}
}
XIFreeDeviceInfo(info);
#ifdef TOUCH_ENABLED
if (!xi.touch_devices.size()) {
print_verbose("XInput: No touch devices found.");
}
#endif
return true;
}
void OS_X11::xim_destroy_callback(::XIM im, ::XPointer client_data,
::XPointer call_data) {
@ -664,10 +738,10 @@ void OS_X11::finalize() {
#ifdef JOYDEV_ENABLED
memdelete(joypad);
#endif
#ifdef TOUCH_ENABLED
touch.devices.clear();
touch.state.clear();
#endif
xi.touch_devices.clear();
xi.state.clear();
memdelete(input);
visual_server->finish();
@ -727,21 +801,8 @@ void OS_X11::set_mouse_mode(MouseMode p_mode) {
if (mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED) {
while (true) {
//flush pending motion events
if (XPending(x11_display) > 0) {
XEvent event;
XPeekEvent(x11_display, &event);
if (event.type == MotionNotify) {
XNextEvent(x11_display, &event);
} else {
break;
}
} else {
break;
}
}
//flush pending motion events
flush_mouse_motion();
if (XGrabPointer(
x11_display, x11_window, True,
@ -782,6 +843,32 @@ void OS_X11::warp_mouse_position(const Point2 &p_to) {
}
}
void OS_X11::flush_mouse_motion() {
while (true) {
if (XPending(x11_display) > 0) {
XEvent event;
XPeekEvent(x11_display, &event);
if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) {
XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data;
if (event_data->evtype == XI_RawMotion) {
XNextEvent(x11_display, &event);
} else {
break;
}
} else {
break;
}
} else {
break;
}
}
xi.relative_motion.x = 0;
xi.relative_motion.y = 0;
}
OS::MouseMode OS_X11::get_mouse_mode() const {
return mouse_mode;
}
@ -1778,17 +1865,61 @@ void OS_X11::process_xevents() {
continue;
}
#ifdef TOUCH_ENABLED
if (XGetEventData(x11_display, &event.xcookie)) {
if (event.xcookie.type == GenericEvent && event.xcookie.extension == touch.opcode) {
if (event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) {
XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data;
int index = event_data->detail;
Vector2 pos = Vector2(event_data->event_x, event_data->event_y);
switch (event_data->evtype) {
case XI_HierarchyChanged:
case XI_DeviceChanged: {
refresh_device_info();
} break;
case XI_RawMotion: {
XIRawEvent *raw_event = (XIRawEvent *)event_data;
int device_id = raw_event->deviceid;
// Determine the axis used (called valuators in XInput for some forsaken reason)
// Mask is a bitmask indicating which axes are involved.
// We are interested in the values of axes 0 and 1.
if (raw_event->valuators.mask_len <= 0 || !XIMaskIsSet(raw_event->valuators.mask, 0) || !XIMaskIsSet(raw_event->valuators.mask, 1)) {
break;
}
double rel_x = raw_event->raw_values[0];
double rel_y = raw_event->raw_values[1];
// https://bugs.freedesktop.org/show_bug.cgi?id=71609
// http://lists.libsdl.org/pipermail/commits-libsdl.org/2015-June/000282.html
if (raw_event->time == xi.last_relative_time && rel_x == xi.relative_motion.x && rel_y == xi.relative_motion.y) {
break; // Flush duplicate to avoid overly fast motion
}
xi.old_raw_pos.x = xi.raw_pos.x;
xi.old_raw_pos.y = xi.raw_pos.y;
xi.raw_pos.x = rel_x;
xi.raw_pos.y = rel_y;
Map<int, Vector2>::Element *abs_info = xi.absolute_devices.find(device_id);
if (abs_info) {
// Absolute mode device
Vector2 mult = abs_info->value();
xi.relative_motion.x += (xi.raw_pos.x - xi.old_raw_pos.x) * mult.x;
xi.relative_motion.y += (xi.raw_pos.y - xi.old_raw_pos.y) * mult.y;
} else {
// Relative mode device
xi.relative_motion.x = xi.raw_pos.x;
xi.relative_motion.y = xi.raw_pos.y;
}
xi.last_relative_time = raw_event->time;
} break;
#ifdef TOUCH_ENABLED
case XI_TouchBegin: // Fall-through
// Disabled hand-in-hand with the grabbing
//XIAllowTouchEvents(x11_display, event_data->deviceid, event_data->detail, x11_window, XIAcceptTouch);
@ -1804,26 +1935,26 @@ void OS_X11::process_xevents() {
st->set_pressed(is_begin);
if (is_begin) {
if (touch.state.has(index)) // Defensive
if (xi.state.has(index)) // Defensive
break;
touch.state[index] = pos;
if (touch.state.size() == 1) {
xi.state[index] = pos;
if (xi.state.size() == 1) {
// X11 may send a motion event when a touch gesture begins, that would result
// in a spurious mouse motion event being sent to Godot; remember it to be able to filter it out
touch.mouse_pos_to_filter = pos;
xi.mouse_pos_to_filter = pos;
}
input->parse_input_event(st);
} else {
if (!touch.state.has(index)) // Defensive
if (!xi.state.has(index)) // Defensive
break;
touch.state.erase(index);
xi.state.erase(index);
input->parse_input_event(st);
}
} break;
case XI_TouchUpdate: {
Map<int, Vector2>::Element *curr_pos_elem = touch.state.find(index);
Map<int, Vector2>::Element *curr_pos_elem = xi.state.find(index);
if (!curr_pos_elem) { // Defensive
break;
}
@ -1840,11 +1971,11 @@ void OS_X11::process_xevents() {
curr_pos_elem->value() = pos;
}
} break;
#endif
}
}
}
XFreeEventData(x11_display, &event.xcookie);
#endif
switch (event.type) {
case Expose:
@ -1890,8 +2021,8 @@ void OS_X11::process_xevents() {
}
#ifdef TOUCH_ENABLED
// Grab touch devices to avoid OS gesture interference
/*for (int i = 0; i < touch.devices.size(); ++i) {
XIGrabDevice(x11_display, touch.devices[i], x11_window, CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, False, &touch.event_mask);
/*for (int i = 0; i < xi.touch_devices.size(); ++i) {
XIGrabDevice(x11_display, xi.touch_devices[i], x11_window, CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, False, &xi.touch_event_mask);
}*/
#endif
if (xic) {
@ -1912,12 +2043,12 @@ void OS_X11::process_xevents() {
}
#ifdef TOUCH_ENABLED
// Ungrab touch devices so input works as usual while we are unfocused
/*for (int i = 0; i < touch.devices.size(); ++i) {
XIUngrabDevice(x11_display, touch.devices[i], CurrentTime);
/*for (int i = 0; i < xi.touch_devices.size(); ++i) {
XIUngrabDevice(x11_display, xi.touch_devices[i], CurrentTime);
}*/
// Release every pointer to avoid sticky points
for (Map<int, Vector2>::Element *E = touch.state.front(); E; E = E->next()) {
for (Map<int, Vector2>::Element *E = xi.state.front(); E; E = E->next()) {
Ref<InputEventScreenTouch> st;
st.instance();
@ -1925,7 +2056,7 @@ void OS_X11::process_xevents() {
st->set_position(E->get());
input->parse_input_event(st);
}
touch.state.clear();
xi.state.clear();
#endif
if (xic) {
XUnsetICFocus(xic);
@ -2018,34 +2149,27 @@ void OS_X11::process_xevents() {
// Motion is also simple.
// A little hack is in order
// to be able to send relative motion events.
Point2i pos(event.xmotion.x, event.xmotion.y);
Point2 pos(event.xmotion.x, event.xmotion.y);
#ifdef TOUCH_ENABLED
// Avoidance of spurious mouse motion (see handling of touch)
bool filter = false;
// Adding some tolerance to match better Point2i to Vector2
if (touch.state.size() && Vector2(pos).distance_squared_to(touch.mouse_pos_to_filter) < 2) {
if (xi.state.size() && Vector2(pos).distance_squared_to(xi.mouse_pos_to_filter) < 2) {
filter = true;
}
// Invalidate to avoid filtering a possible legitimate similar event coming later
touch.mouse_pos_to_filter = Vector2(1e10, 1e10);
xi.mouse_pos_to_filter = Vector2(1e10, 1e10);
if (filter) {
break;
}
#endif
if (mouse_mode == MOUSE_MODE_CAPTURED) {
if (pos == Point2i(current_videomode.width / 2, current_videomode.height / 2)) {
//this sucks, it's a hack, etc and is a little inaccurate, etc.
//but nothing I can do, X11 sucks.
center = pos;
if (xi.relative_motion.x == 0 && xi.relative_motion.y == 0) {
break;
}
Point2i new_center = pos;
pos = last_mouse_pos + (pos - center);
pos = last_mouse_pos + xi.relative_motion;
center = new_center;
do_mouse_warp = window_has_focus; // warp the cursor if we're focused in
}
@ -2056,7 +2180,24 @@ void OS_X11::process_xevents() {
last_mouse_pos_valid = true;
}
Point2i rel = pos - last_mouse_pos;
// Hackish but relative mouse motion is already handled in the RawMotion event.
// RawMotion does not provide the absolute mouse position (whereas MotionNotify does).
// Therefore, RawMotion cannot be the authority on absolute mouse position.
// RawMotion provides more precision than MotionNotify, which doesn't sense subpixel motion.
// Therefore, MotionNotify cannot be the authority on relative mouse motion.
// This means we need to take a combined approach...
Point2 rel;
// Only use raw input if in capture mode. Otherwise use the classic behavior.
if (mouse_mode == MOUSE_MODE_CAPTURED) {
rel = xi.relative_motion;
} else {
rel = pos - last_mouse_pos;
}
// Reset to prevent lingering motion
xi.relative_motion.x = 0;
xi.relative_motion.y = 0;
if (mouse_mode == MOUSE_MODE_CAPTURED) {
pos = Point2i(current_videomode.width / 2, current_videomode.height / 2);
@ -2065,12 +2206,16 @@ void OS_X11::process_xevents() {
Ref<InputEventMouseMotion> mm;
mm.instance();
// Make the absolute position integral so it doesn't look _too_ weird :)
Point2i posi(pos);
get_key_modifier_state(event.xmotion.state, mm);
mm->set_button_mask(get_mouse_button_state());
mm->set_position(pos);
mm->set_global_position(pos);
input->set_mouse_position(pos);
mm->set_position(posi);
mm->set_global_position(posi);
input->set_mouse_position(posi);
mm->set_speed(input->get_last_mouse_speed());
mm->set_relative(rel);
last_mouse_pos = pos;

View file

@ -48,11 +48,9 @@
#include <X11/Xcursor/Xcursor.h>
#include <X11/Xlib.h>
#include <X11/extensions/XInput2.h>
#include <X11/extensions/Xrandr.h>
#include <X11/keysym.h>
#ifdef TOUCH_ENABLED
#include <X11/extensions/XInput2.h>
#endif
// Hints for X11 fullscreen
typedef struct {
@ -121,24 +119,32 @@ class OS_X11 : public OS_Unix {
bool im_active;
Vector2 im_position;
Point2i last_mouse_pos;
Point2 last_mouse_pos;
bool last_mouse_pos_valid;
Point2i last_click_pos;
uint64_t last_click_ms;
int last_click_button_index;
uint32_t last_button_state;
#ifdef TOUCH_ENABLED
struct {
int opcode;
Vector<int> devices;
XIEventMask event_mask;
Vector<int> touch_devices;
Map<int, Vector2> absolute_devices;
XIEventMask all_event_mask;
XIEventMask all_master_event_mask;
Map<int, Vector2> state;
Vector2 mouse_pos_to_filter;
} touch;
#endif
Vector2 relative_motion;
Vector2 raw_pos;
Vector2 old_raw_pos;
::Time last_relative_time;
} xi;
bool refresh_device_info();
unsigned int get_mouse_button_state(unsigned int p_x11_button, int p_x11_type);
void get_key_modifier_state(unsigned int p_x11_state, Ref<InputEventWithModifiers> state);
void flush_mouse_motion();
MouseMode mouse_mode;
Point2i center;