// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // virtualPad.c - two virtual gamepads for the joystick gates of the ports // that read the host's gamepads (SDL3, Win32 under Wine, the web port in // Chromium). Linux only: they are uinput devices. // // virtualPad [--generic] [NAME] NAME prefixes the devices' names // (default "JoeyLib gate") // // Makes two uinput devices laid out as Xbox 360 pads (the xpad driver's // buttons and axes, Microsoft's USB ids), which SDL's, Wine's, and // Chromium's gamepad mappings all know: stick A is the first pad ("NAME pad // A"), stick B the second. With --generic they are plain joysticks instead // -- an X and a Y axis, a trigger and a thumb button, and pid.codes' test // ids -- which no gamepad mapping knows, so SDL offers them only as raw // joysticks. Then reads lines on stdin and holds or lets go of the controls // named: // // press CONTROL... CONTROL as joyGate.sh names them: A-left A-right // release CONTROL... A-up A-down A-button0 A-button1, and the same for B // quit remove the pads (as does end of input) // // The left stick moves to the end of its travel (left/up -32767, // right/down +32767; holding both of an axis's directions is the positive // one), button 0 is the south button (A) and button 1 the east (B) -- on a // generic stick, the trigger and the thumb button. Prints "ready" once both // devices exist, "ok" after each line it has applied, and "error: ..." for // a word it does not know (a line with an unknown command gets no "ok"). // // Needs write access to /dev/uinput (a desktop session's user usually has // it through udev's uaccess rule); exit 1 without it. The pads are the // whole machine's while they exist, not only the gate's. #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include "gateNet.h" #include "toolFile.h" #define TOOL_NAME "virtualPad" #define UINPUT_PATH "/dev/uinput" #define GENERIC_OPTION "--generic" #define DEFAULT_PREFIX "JoeyLib gate" #define PAD_COUNT 2u #define NS_PER_US 1000L // How long the kernel and the readers get to see a new device. #define SETTLE_SECONDS 1.0 #define WORD_SPACE " \t\r\n\v\f" #define STICK_SEPARATOR '-' #define XBOX360_VENDOR 0x045Eu #define XBOX360_PRODUCT 0x028Eu #define XBOX360_VERSION 0x0110u // pid.codes' vendor id and its test product, in no gamepad database. #define GENERIC_VENDOR 0x1209u #define GENERIC_PRODUCT 0x0001u #define GENERIC_VERSION 0x0100u #define STICK_MAX 32767 #define STICK_MIN (-32768) #define STICK_FUZZ 16 #define STICK_FLAT 128 #define TRIGGER_MAX 255 #define HAT_MIN (-1) #define HAT_MAX 1 #define FULL STICK_MAX // The lines a stick has, as the commands name them. typedef enum LineE { LINE_LEFT = 0, LINE_RIGHT, LINE_UP, LINE_DOWN, LINE_BUTTON0, LINE_BUTTON1, LINE_COUNT } LineE; // One axis: its code and range. typedef struct AxisT { uint16_t code; int32_t min; int32_t max; int32_t fuzz; int32_t flat; } AxisT; // What a pad is: its ids, its buttons and axes, and the buttons that are // button 0 and button 1. typedef struct LayoutT { uint16_t vendor; uint16_t product; uint16_t version; const uint16_t *buttons; size_t buttonCount; const AxisT *axes; size_t axisCount; uint16_t button0; uint16_t button1; } LayoutT; // A pad made: its uinput device, its layout, and the lines held. typedef struct PadT { int fd; const LayoutT *layout; bool held[LINE_COUNT]; } PadT; static void padApply(const PadT *pad); static void padClose(PadT *pad); static void padEmit(const PadT *pad, uint16_t type, uint16_t code, int32_t value); static void padIoctl(int fd, unsigned long request, unsigned long value); static void padOpen(PadT *pad, const char *prefix, char stick, const LayoutT *layout); static bool padsCommand(PadT *pads, char *line); static void uinputFail(void) __attribute__((noreturn)); static const char *const kLineNames[LINE_COUNT] = { "left", "right", "up", "down", "button0", "button1" }; static const uint16_t kXbox360Buttons[] = { BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR }; static const uint16_t kGenericButtons[] = { BTN_TRIGGER, BTN_THUMB }; // The stick's axes first: a generic pad has those alone. static const AxisT kXbox360Axes[] = { {ABS_X, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT}, {ABS_Y, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT}, {ABS_RX, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT}, {ABS_RY, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT}, {ABS_Z, 0, TRIGGER_MAX, 0, 0}, {ABS_RZ, 0, TRIGGER_MAX, 0, 0}, {ABS_HAT0X, HAT_MIN, HAT_MAX, 0, 0}, {ABS_HAT0Y, HAT_MIN, HAT_MAX, 0, 0} }; #define STICK_AXES 2u static const LayoutT kXbox360 = { XBOX360_VENDOR, XBOX360_PRODUCT, XBOX360_VERSION, kXbox360Buttons, sizeof(kXbox360Buttons) / sizeof(kXbox360Buttons[0]), kXbox360Axes, sizeof(kXbox360Axes) / sizeof(kXbox360Axes[0]), BTN_SOUTH, BTN_EAST }; static const LayoutT kGeneric = { GENERIC_VENDOR, GENERIC_PRODUCT, GENERIC_VERSION, kGenericButtons, sizeof(kGenericButtons) / sizeof(kGenericButtons[0]), kXbox360Axes, STICK_AXES, BTN_TRIGGER, BTN_THUMB }; static const char kStickNames[PAD_COUNT] = {'A', 'B'}; // Send the axes and buttons the held lines make, then a report. static void padApply(const PadT *pad) { int32_t x; int32_t y; x = pad->held[LINE_RIGHT] ? FULL : (pad->held[LINE_LEFT] ? -FULL : 0); y = pad->held[LINE_DOWN] ? FULL : (pad->held[LINE_UP] ? -FULL : 0); padEmit(pad, EV_ABS, ABS_X, x); padEmit(pad, EV_ABS, ABS_Y, y); padEmit(pad, EV_KEY, pad->layout->button0, pad->held[LINE_BUTTON0] ? 1 : 0); padEmit(pad, EV_KEY, pad->layout->button1, pad->held[LINE_BUTTON1] ? 1 : 0); padEmit(pad, EV_SYN, SYN_REPORT, 0); } static void padClose(PadT *pad) { padIoctl(pad->fd, UI_DEV_DESTROY, 0u); close(pad->fd); } static void padEmit(const PadT *pad, uint16_t type, uint16_t code, int32_t value) { struct input_event event; struct timespec now; memset(&event, 0, sizeof(event)); clock_gettime(CLOCK_REALTIME, &now); event.input_event_sec = now.tv_sec; event.input_event_usec = now.tv_nsec / NS_PER_US; event.type = type; event.code = code; event.value = value; if (write(pad->fd, &event, sizeof(event)) != (ssize_t)sizeof(event)) { uinputFail(); } } // One uinput ioctl taking a number; a failure ends the tool. static void padIoctl(int fd, unsigned long request, unsigned long value) { if (ioctl(fd, request, value) != 0) { uinputFail(); } } // The pad for stick, named " pad " (cut to what uinput holds). static void padOpen(PadT *pad, const char *prefix, char stick, const LayoutT *layout) { struct uinput_abs_setup abs; struct uinput_setup setup; size_t i; memset(pad, 0, sizeof(*pad)); pad->layout = layout; pad->fd = open(UINPUT_PATH, O_WRONLY | O_NONBLOCK); if (pad->fd < 0) { uinputFail(); } padIoctl(pad->fd, UI_SET_EVBIT, EV_KEY); for (i = 0; i < layout->buttonCount; i++) { padIoctl(pad->fd, UI_SET_KEYBIT, layout->buttons[i]); } padIoctl(pad->fd, UI_SET_EVBIT, EV_ABS); for (i = 0; i < layout->axisCount; i++) { memset(&abs, 0, sizeof(abs)); abs.code = layout->axes[i].code; abs.absinfo.minimum = layout->axes[i].min; abs.absinfo.maximum = layout->axes[i].max; abs.absinfo.fuzz = layout->axes[i].fuzz; abs.absinfo.flat = layout->axes[i].flat; padIoctl(pad->fd, UI_SET_ABSBIT, layout->axes[i].code); if (ioctl(pad->fd, UI_ABS_SETUP, &abs) != 0) { uinputFail(); } } memset(&setup, 0, sizeof(setup)); setup.id.bustype = BUS_USB; setup.id.vendor = layout->vendor; setup.id.product = layout->product; setup.id.version = layout->version; snprintf(setup.name, sizeof(setup.name), "%s pad %c", prefix, stick); if (ioctl(pad->fd, UI_DEV_SETUP, &setup) != 0) { uinputFail(); } padIoctl(pad->fd, UI_DEV_CREATE, 0u); } // One line of commands: press or release (each control held or let go, and // the pads touched sent again), or quit (false). static bool padsCommand(PadT *pads, char *line) { const char *verb; char *control; char *separator; bool press; bool touched[PAD_COUNT]; uint32_t p; uint32_t l; verb = strtok(line, WORD_SPACE); if (verb == NULL) { return true; } if (strcmp(verb, "quit") == 0) { return false; } press = strcmp(verb, "press") == 0; if (!press && strcmp(verb, "release") != 0) { printf("error: unknown command %s\n", verb); return true; } memset(touched, 0, sizeof(touched)); while ((control = strtok(NULL, WORD_SPACE)) != NULL) { separator = strchr(control, STICK_SEPARATOR); p = PAD_COUNT; l = LINE_COUNT; if (separator == control + 1) { for (p = 0; p < PAD_COUNT && kStickNames[p] != control[0]; p++) { // Up to the stick named. } for (l = 0; l < LINE_COUNT && strcmp(separator + 1, kLineNames[l]) != 0; l++) { // Up to the line named. } } if (p == PAD_COUNT || l == LINE_COUNT) { printf("error: unknown control %s\n", control); continue; } pads[p].held[l] = press; touched[p] = true; } for (p = 0; p < PAD_COUNT; p++) { if (touched[p]) { padApply(&pads[p]); } } printf("ok\n"); return true; } // What uinput refused, as the tool's last word (the kernel removes any // device made so far when its descriptor closes at exit). static void uinputFail(void) { toolFail("%s: %s", UINPUT_PATH, strerror(errno)); } int main(int argc, char **argv) { PadT pads[PAD_COUNT]; const LayoutT *layout; const char *prefix; char *line; size_t cap; uint32_t p; int first; gateNetStart(TOOL_NAME); layout = &kXbox360; first = 1; if (argc > first && strcmp(argv[first], GENERIC_OPTION) == 0) { layout = &kGeneric; first++; } prefix = argc > first ? argv[first] : DEFAULT_PREFIX; for (p = 0; p < PAD_COUNT; p++) { padOpen(&pads[p], prefix, kStickNames[p], layout); } gateNetSleep(SETTLE_SECONDS); for (p = 0; p < PAD_COUNT; p++) { padApply(&pads[p]); } printf("ready\n"); line = NULL; cap = 0u; while (getline(&line, &cap, stdin) >= 0 && padsCommand(pads, line)) { // Each line applied. } free(line); for (p = 0; p < PAD_COUNT; p++) { padClose(&pads[p]); } return 0; }