blob: e4f367fd8fbab4e5bf0cd6858733edbfef4545fe [file] [log] [blame]
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include "../../SDL_internal.h"
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
#include "../../events/SDL_pen_c.h"
#include "../SDL_sysvideo.h"
#include "SDL_x11pen.h"
#include "SDL_x11video.h"
#include "SDL_x11xinput2.h"
#define PEN_ERASER_ID_MAXLEN 256 /* Max # characters of device name to scan */
#define PEN_ERASER_NAME_TAG "eraser" /* String constant to identify erasers */
#define DEBUG_PEN (SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM | SDL_PEN_DEBUG_UNKNOWN_WACOM | SDL_PEN_DEBUG_NOSERIAL_WACOM)
#define SDL_PEN_AXIS_VALUATOR_MISSING -1
/* X11-specific information attached to each pen */
typedef struct xinput2_pen
{
float axis_min[SDL_PEN_NUM_AXES];
float axis_max[SDL_PEN_NUM_AXES];
float slider_bias; /* shift value to add to PEN_AXIS_SLIDER (before normalisation) */
float rotation_bias; /* rotation to add to PEN_AXIS_ROTATION (after normalisation) */
Sint8 valuator_for_axis[SDL_PEN_NUM_AXES]; /* SDL_PEN_AXIS_VALUATOR_MISSING if not supported */
} xinput2_pen;
/* X11 atoms */
static struct
{
int initialized; /* initialised to 0 */
Atom device_product_id;
Atom abs_pressure;
Atom abs_tilt_x;
Atom abs_tilt_y;
Atom wacom_serial_ids;
Atom wacom_tool_type;
} pen_atoms;
/*
* Mapping from X11 device IDs to pen IDs
*
* In X11, the same device ID may represent any number of pens. We
* thus cannot directly use device IDs as pen IDs.
*/
static struct
{
int num_pens_known; /* Number of currently known pens (based on their GUID); used to give pen ID to new pens */
int num_entries; /* Number of X11 device IDs that correspond to pens */
struct pen_device_id_mapping
{
Uint32 deviceid;
Uint32 pen_id;
} * entries; /* Current pen to device ID mappings */
} pen_map;
typedef enum
{
SDL_PEN_VENDOR_UNKNOWN = 0,
SDL_PEN_VENDOR_WACOM
} sdl_pen_vendor;
/* Information to identify pens during discovery */
typedef struct
{
sdl_pen_vendor vendor;
SDL_GUID guid;
SDL_PenSubtype heuristic_type; /* Distinguish pen+eraser devices with shared bus ID */
Uint32 devicetype_id, serial; /* used by PEN_VENDOR_WACOM */
Uint32 deviceid;
} pen_identity;
int X11_PenIDFromDeviceID(int deviceid)
{
int i;
for (i = 0; i < pen_map.num_entries; ++i) {
if (pen_map.entries[i].deviceid == deviceid) {
return pen_map.entries[i].pen_id;
}
}
return SDL_PEN_INVALID;
}
static void pen_atoms_ensure_initialized(SDL_VideoDevice *_this)
{
SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
if (pen_atoms.initialized) {
return;
}
/* Create atoms if they don't exist yet to pre-empt hotplugging updates */
pen_atoms.device_product_id = X11_XInternAtom(data->display, "Device Product ID", False);
pen_atoms.wacom_serial_ids = X11_XInternAtom(data->display, "Wacom Serial IDs", False);
pen_atoms.wacom_tool_type = X11_XInternAtom(data->display, "Wacom Tool Type", False);
pen_atoms.abs_pressure = X11_XInternAtom(data->display, "Abs Pressure", False);
pen_atoms.abs_tilt_x = X11_XInternAtom(data->display, "Abs Tilt X", False);
pen_atoms.abs_tilt_y = X11_XInternAtom(data->display, "Abs Tilt Y", False);
pen_atoms.initialized = 1;
}
/* Read out an integer property and store into a preallocated Sint32 array, extending 8 and 16 bit values suitably.
Returns number of Sint32s written (<= max_words), or 0 on error. */
static size_t xinput2_pen_get_int_property(SDL_VideoDevice *_this, int deviceid, Atom property, Sint32 *dest, size_t max_words)
{
const SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
Atom type_return;
int format_return;
unsigned long num_items_return;
unsigned long bytes_after_return;
unsigned char *output;
if (property == None) {
return 0;
}
if (Success != X11_XIGetProperty(data->display, deviceid,
property,
0, max_words, False,
XA_INTEGER, &type_return, &format_return,
&num_items_return, &bytes_after_return,
&output) ||
num_items_return == 0 || output == NULL) {
return 0;
}
if (type_return == XA_INTEGER) {
int k;
const int to_copy = SDL_min(max_words, num_items_return);
if (format_return == 8) {
Sint8 *numdata = (Sint8 *)output;
for (k = 0; k < to_copy; ++k) {
dest[k] = numdata[k];
}
} else if (format_return == 16) {
Sint16 *numdata = (Sint16 *)output;
for (k = 0; k < to_copy; ++k) {
dest[k] = numdata[k];
}
} else {
SDL_memcpy(dest, output, sizeof(Sint32) * to_copy);
}
X11_XFree(output);
return to_copy;
}
return 0; /* type mismatch */
}
/* 32 bit vendor + device ID from evdev */
static Uint32 xinput2_pen_evdevid(SDL_VideoDevice *_this, int deviceid)
{
#if !(SDL_PEN_DEBUG_NOID)
Sint32 ids[2];
pen_atoms_ensure_initialized(_this);
if (2 != xinput2_pen_get_int_property(_this, deviceid, pen_atoms.device_product_id, ids, 2)) {
return 0;
}
return ((ids[0] << 16) | (ids[1] & 0xffff));
#else /* Testing: pretend that we have no ID (not sure if this can happen IRL) */
return 0;
#endif
}
/* Gets reasonably-unique GUID for the device */
static void xinput2_pen_update_generic_guid(SDL_VideoDevice *_this, pen_identity *pident, int deviceid)
{
Uint32 evdevid = xinput2_pen_evdevid(_this, deviceid); /* also initialises pen_atoms */
if (!evdevid) {
/* Fallback: if no evdevid is available; try to at least distinguish devices within the
current session. This is a poor GUID and our last resort. */
evdevid = deviceid;
}
SDL_PenUpdateGUIDForGeneric(&pident->guid, 0, evdevid);
}
/* Identify Wacom devices (if SDL_TRUE is returned) and extract their device type and serial IDs */
static SDL_bool xinput2_wacom_deviceid(SDL_VideoDevice *_this, int deviceid, Uint32 *wacom_devicetype_id, Uint32 *wacom_serial)
{
#if !(SDL_PEN_DEBUG_NONWACOM) /* Can be disabled for testing */
Sint32 serial_id_buf[3];
int result;
pen_atoms_ensure_initialized(_this);
if ((result = xinput2_pen_get_int_property(_this, deviceid, pen_atoms.wacom_serial_ids, serial_id_buf, 3)) == 3) {
*wacom_devicetype_id = serial_id_buf[2];
*wacom_serial = serial_id_buf[1];
#if SDL_PEN_DEBUG_NOSERIAL_WACOM /* Disabled for testing? */
*wacom_serial = 0;
#endif
return SDL_TRUE;
}
#endif
return SDL_FALSE;
}
/* Heuristically determines if device is an eraser */
static SDL_bool xinput2_pen_is_eraser(SDL_VideoDevice *_this, int deviceid, char *devicename)
{
SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
char dev_name[PEN_ERASER_ID_MAXLEN];
int k;
pen_atoms_ensure_initialized(_this);
if (pen_atoms.wacom_tool_type != None) {
Atom type_return;
int format_return;
unsigned long num_items_return;
unsigned long bytes_after_return;
unsigned char *tooltype_name_info = NULL;
/* Try Wacom-specific method */
if (Success == X11_XIGetProperty(data->display, deviceid,
pen_atoms.wacom_tool_type,
0, 32, False,
AnyPropertyType, &type_return, &format_return,
&num_items_return, &bytes_after_return,
&tooltype_name_info) &&
tooltype_name_info != NULL && num_items_return > 0) {
SDL_bool result = SDL_FALSE;
char *tooltype_name = NULL;
if (type_return == XA_ATOM) {
/* Atom instead of string? Un-intern */
Atom atom = *((Atom *)tooltype_name_info);
if (atom != None) {
tooltype_name = X11_XGetAtomName(data->display, atom);
}
} else if (type_return == XA_STRING && format_return == 8) {
tooltype_name = (char *)tooltype_name_info;
}
if (tooltype_name) {
if (0 == SDL_strcasecmp(tooltype_name, PEN_ERASER_NAME_TAG)) {
result = SDL_TRUE;
}
X11_XFree(tooltype_name_info);
return result;
}
}
}
/* Non-Wacom device? */
/* We assume that a device is an eraser if its name contains the string "eraser".
* Unfortunately there doesn't seem to be a clean way to distinguish these cases (as of 2022-03). */
SDL_strlcpy(dev_name, devicename, PEN_ERASER_ID_MAXLEN);
/* lowercase device name string so we can use strstr() */
for (k = 0; dev_name[k]; ++k) {
dev_name[k] = SDL_tolower(dev_name[k]);
}
return (SDL_strstr(dev_name, PEN_ERASER_NAME_TAG)) ? SDL_TRUE : SDL_FALSE;
}
/* Gets GUID and other identifying information for the device using the best known method */
static pen_identity xinput2_identify_pen(SDL_VideoDevice *_this, int deviceid, char *name)
{
pen_identity pident;
pident.devicetype_id = 0ul;
pident.serial = 0ul;
pident.deviceid = deviceid;
pident.heuristic_type = SDL_PEN_TYPE_PEN;
SDL_memset(pident.guid.data, 0, sizeof(pident.guid.data));
if (xinput2_pen_is_eraser(_this, deviceid, name)) {
pident.heuristic_type = SDL_PEN_TYPE_ERASER;
}
if (xinput2_wacom_deviceid(_this, deviceid, &pident.devicetype_id, &pident.serial)) {
pident.vendor = SDL_PEN_VENDOR_WACOM;
SDL_PenUpdateGUIDForWacom(&pident.guid, pident.devicetype_id, pident.serial);
#if DEBUG_PEN
printf("[pen] Pen %d reports Wacom device_id %x\n",
deviceid, pident.devicetype_id);
#endif
} else {
pident.vendor = SDL_PEN_VENDOR_UNKNOWN;
}
if (!pident.serial) {
/* If the pen has a serial number, we can move it across tablets and retain its identity.
Otherwise, we use the evdev ID as part of its GUID, which may mean that we identify it with the tablet. */
xinput2_pen_update_generic_guid(_this, &pident, deviceid);
}
SDL_PenUpdateGUIDForType(&pident.guid, pident.heuristic_type);
return pident;
}
static void xinput2_pen_free_deviceinfo(Uint32 deviceid, void *x11_peninfo, void *context)
{
SDL_free(x11_peninfo);
}
static void xinput2_merge_deviceinfo(xinput2_pen *dest, xinput2_pen *src)
{
*dest = *src;
}
/**
* Fill in vendor-specific device information, if available
*
* For Wacom pens: identify number of buttons and extra axis (if present)
*
* \param _this global state
* \param dev The device to analyse
* \param pen The pen to initialise
* \param pident Pen identity information
* \param[out] valuator_5 Meaning of the valuator with offset 5, if any
* (written only if known and if the device has a 6th axis,
* e.g., for the Wacom Art Pen and Wacom Airbrush Pen)
* \param[out] axes Bitmask of all possibly supported axes
*
* This function identifies Wacom device types through a Wacom-specific device ID.
* It then fills in pen details from an internal database.
* If the device seems to be a Wacom pen/eraser but can't be identified, the function
* leaves "axes" untouched and sets the other outputs to common defaults.
*
* There is no explicit support for other vendors, though vendors that
* emulate the Wacom API might be supported.
*
* Unsupported devices will keep the default settings.
*/
static void xinput2_vendor_peninfo(SDL_VideoDevice *_this, const XIDeviceInfo *dev, SDL_Pen *pen, pen_identity pident, int *valuator_5, Uint32 *axes)
{
switch (pident.vendor) {
case SDL_PEN_VENDOR_WACOM:
{
if (SDL_PenModifyForWacomID(pen, pident.devicetype_id, axes)) {
if (*axes & SDL_PEN_AXIS_SLIDER_MASK) {
/* Air Brush Pen or eraser */
*valuator_5 = SDL_PEN_AXIS_SLIDER;
} else if (*axes & SDL_PEN_AXIS_ROTATION_MASK) {
/* Art Pen or eraser, or 6D Art Pen */
*valuator_5 = SDL_PEN_AXIS_ROTATION;
}
return;
} else {
#if DEBUG_PEN
printf("[pen] Could not identify wacom pen %d with device id %x, using default settings\n",
pident.deviceid, pident.devicetype_id);
#endif
break;
}
}
default:
#if DEBUG_PEN
printf("[pen] Pen %d is not from a known vendor\n", pident.deviceid);
#endif
break;
}
/* Fall back to default heuristics for identifying device type */
SDL_strlcpy(pen->name, dev->name, SDL_PEN_MAX_NAME);
pen->type = pident.heuristic_type;
}
/* Does this device have a valuator for pressure sensitivity? */
static SDL_bool xinput2_device_is_pen(SDL_VideoDevice *_this, const XIDeviceInfo *dev)
{
int classct;
pen_atoms_ensure_initialized(_this);
for (classct = 0; classct < dev->num_classes; ++classct) {
const XIAnyClassInfo *classinfo = dev->classes[classct];
switch (classinfo->type) {
case XIValuatorClass:
{
XIValuatorClassInfo *val_classinfo = (XIValuatorClassInfo *)classinfo;
Atom vname = val_classinfo->label;
if (vname == pen_atoms.abs_pressure) {
return SDL_TRUE;
}
}
}
}
return SDL_FALSE;
}
void X11_InitPen(SDL_VideoDevice *_this)
{
SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
int i;
XIDeviceInfo *device_info;
int num_device_info;
device_info = X11_XIQueryDevice(data->display, XIAllDevices, &num_device_info);
if (!device_info) {
return;
}
/* Reset the device id -> pen map */
if (pen_map.entries) {
SDL_free(pen_map.entries);
pen_map.entries = NULL;
pen_map.num_entries = 0;
}
SDL_PenGCMark();
for (i = 0; i < num_device_info; ++i) {
const XIDeviceInfo *dev = &device_info[i];
int classct;
xinput2_pen pen_device;
Uint32 capabilities = 0;
Uint32 axis_mask = ~0; /* Permitted axes (default: all) */
int valuator_5_axis = -1; /* For Wacom devices, the 6th valuator (offset 5) has a model-specific meaning */
pen_identity pident;
SDL_PenID pen_id;
SDL_Pen *pen;
int old_num_pens_known = pen_map.num_pens_known;
int k;
/* Only track physical devices that are enabled and look like pens */
if (dev->use != XISlavePointer || dev->enabled == 0 || !xinput2_device_is_pen(_this, dev)) {
continue;
}
pen_device.slider_bias = 0.0f;
pen_device.rotation_bias = 0.0f;
for (k = 0; k < SDL_PEN_NUM_AXES; ++k) {
pen_device.valuator_for_axis[k] = SDL_PEN_AXIS_VALUATOR_MISSING;
}
pident = xinput2_identify_pen(_this, dev->deviceid, dev->name);
pen_id = SDL_GetPenFromGUID(pident.guid);
if (pen_id == SDL_PEN_INVALID) {
/* We have never met this pen */
pen_id = ++pen_map.num_pens_known; /* start at 1 */
}
pen = SDL_PenModifyBegin(pen_id);
/* Complement XF86 driver information with vendor-specific details */
xinput2_vendor_peninfo(_this, dev, pen, pident, &valuator_5_axis, &axis_mask);
for (classct = 0; classct < dev->num_classes; ++classct) {
const XIAnyClassInfo *classinfo = dev->classes[classct];
switch (classinfo->type) {
case XIValuatorClass:
{
XIValuatorClassInfo *val_classinfo = (XIValuatorClassInfo *)classinfo;
Sint8 valuator_nr = val_classinfo->number;
Atom vname = val_classinfo->label;
int axis = -1;
float min = (float)val_classinfo->min;
float max = (float)val_classinfo->max;
if (vname == pen_atoms.abs_pressure) {
axis = SDL_PEN_AXIS_PRESSURE;
} else if (vname == pen_atoms.abs_tilt_x) {
axis = SDL_PEN_AXIS_XTILT;
} else if (vname == pen_atoms.abs_tilt_y) {
axis = SDL_PEN_AXIS_YTILT;
}
if (axis == -1 && valuator_nr == 5) {
/* Wacom model-specific axis support */
/* The meaning of the various axes is highly underspecitied in Xinput2.
* As of 2023-08-26, Wacom seems to be the only vendor to support these axes, so the code below
* captures the de-facto standard. */
axis = valuator_5_axis;
switch (axis) {
case SDL_PEN_AXIS_SLIDER:
/* cf. xinput2_wacom_peninfo for how this axis is used.
In all current cases, our API wants this value in 0..1, but the xf86 driver
starts at a negative offset, so we normalise here. */
pen_device.slider_bias = -min;
max -= min;
min = 0.0f;
break;
case SDL_PEN_AXIS_ROTATION:
/* The "0" value points to the left, rather than up, so we must
rotate 90 degrees counter-clockwise to have 0 point to the top. */
pen_device.rotation_bias = -90.0f;
break;
default:
break;
}
}
if (axis >= 0) {
capabilities |= SDL_PEN_AXIS_CAPABILITY(axis);
pen_device.valuator_for_axis[axis] = valuator_nr;
pen_device.axis_min[axis] = min;
pen_device.axis_max[axis] = max;
}
break;
}
default:
break;
}
}
/* We have a pen if and only if the device measures pressure */
if (capabilities & SDL_PEN_AXIS_PRESSURE_MASK) {
xinput2_pen *xinput2_deviceinfo;
Uint64 guid_a, guid_b;
/* Done collecting data, write to pen */
SDL_PenModifyAddCapabilities(pen, capabilities);
pen->guid = pident.guid;
if (pen->deviceinfo) {
/* Updating a known pen */
xinput2_deviceinfo = (xinput2_pen *)pen->deviceinfo;
xinput2_merge_deviceinfo(xinput2_deviceinfo, &pen_device);
} else {
/* Registering a new pen */
xinput2_deviceinfo = SDL_malloc(sizeof(xinput2_pen));
SDL_memcpy(xinput2_deviceinfo, &pen_device, sizeof(xinput2_pen));
}
pen->deviceinfo = xinput2_deviceinfo;
#if DEBUG_PEN
printf("[pen] pen %d [%04x] valuators pressure=%d, xtilt=%d, ytilt=%d [%s]\n",
pen->header.id, pen->header.flags,
pen_device.valuator_for_axis[SDL_PEN_AXIS_PRESSURE],
pen_device.valuator_for_axis[SDL_PEN_AXIS_XTILT],
pen_device.valuator_for_axis[SDL_PEN_AXIS_YTILT],
pen->name);
#endif
SDL_memcpy(&guid_a, &pen->guid.data[0], 8);
SDL_memcpy(&guid_b, &pen->guid.data[8], 8);
if (!(guid_a | guid_b)) {
#if DEBUG_PEN
printf("[pen] (pen eliminated due to zero GUID)\n");
#endif
pen->type = SDL_PEN_TYPE_NONE;
}
} else {
/* Not a pen, mark for deletion */
pen->type = SDL_PEN_TYPE_NONE;
}
SDL_PenModifyEnd(pen, SDL_TRUE);
if (pen->type != SDL_PEN_TYPE_NONE) {
const int map_pos = pen_map.num_entries;
/* We found a pen: add mapping */
if (pen_map.entries == NULL) {
pen_map.entries = SDL_calloc(sizeof(struct pen_device_id_mapping), 1);
pen_map.num_entries = 1;
} else {
pen_map.num_entries += 1;
pen_map.entries = SDL_realloc(pen_map.entries,
pen_map.num_entries * (sizeof(struct pen_device_id_mapping)));
}
pen_map.entries[map_pos].deviceid = dev->deviceid;
pen_map.entries[map_pos].pen_id = pen_id;
} else {
/* Revert pen number allocation */
pen_map.num_pens_known = old_num_pens_known;
}
}
X11_XIFreeDeviceInfo(device_info);
SDL_PenGCSweep(NULL, xinput2_pen_free_deviceinfo);
}
static void xinput2_normalize_pen_axes(const SDL_Pen *peninfo,
const xinput2_pen *xpen,
/* inout-mode paramters: */
float *coords)
{
int axis;
/* Normalise axes */
for (axis = 0; axis < SDL_PEN_NUM_AXES; ++axis) {
int valuator = xpen->valuator_for_axis[axis];
if (valuator != SDL_PEN_AXIS_VALUATOR_MISSING) {
float value = coords[axis];
float min = xpen->axis_min[axis];
float max = xpen->axis_max[axis];
if (axis == SDL_PEN_AXIS_SLIDER) {
value += xpen->slider_bias;
}
/* min ... 0 ... max */
if (min < 0.0) {
/* Normalise so that 0 remains 0.0 */
if (value < 0) {
value = value / (-min);
} else {
if (max == 0.0) {
value = 0.0f;
} else {
value = value / max;
}
}
} else {
/* 0 ... min ... max */
/* including 0.0 = min */
if (max == 0.0) {
value = 0.0f;
} else {
value = (value - min) / max;
}
}
switch (axis) {
case SDL_PEN_AXIS_XTILT:
case SDL_PEN_AXIS_YTILT:
if (peninfo->info.max_tilt > 0.0f) {
value *= peninfo->info.max_tilt; /* normalise to physical max */
}
break;
case SDL_PEN_AXIS_ROTATION:
/* normalised to -1..1, so let's convert to degrees */
value *= 180.0f;
value += xpen->rotation_bias;
/* handle simple over/underflow */
if (value >= 180.0f) {
value -= 360.0f;
} else if (value < -180.0f) {
value += 360.0f;
}
break;
default:
break;
}
coords[axis] = value;
}
}
}
void X11_PenAxesFromValuators(const SDL_Pen *peninfo,
const double *input_values, const unsigned char *mask, const int mask_len,
/* out-mode parameters: */
float axis_values[SDL_PEN_NUM_AXES])
{
const xinput2_pen *pen = (xinput2_pen *)peninfo->deviceinfo;
int i;
for (i = 0; i < SDL_PEN_NUM_AXES; ++i) {
const int valuator = pen->valuator_for_axis[i];
if (valuator == SDL_PEN_AXIS_VALUATOR_MISSING || valuator >= mask_len * 8 || !(XIMaskIsSet(mask, valuator))) {
axis_values[i] = 0.0f;
} else {
axis_values[i] = (float)input_values[valuator];
}
}
xinput2_normalize_pen_axes(peninfo, pen, axis_values);
}
#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */
/* vi: set ts=4 sw=4 expandtab: */