From: Thomas Frohwein Subject: Adding rumble/force feedback control to ujoy(4) To: tech@openbsd.org Date: Tue, 19 Nov 2024 09:40:41 -0500 Hi tech@, The attached diff enables support for haptic feedback (aka. force feedback, vibration, or rumble) for some ujoy(4) gamecontrollers via a new ioctl UJOYIO_RUMBLE. It takes a struct ujoy_rumble that is made based on the GIP protocol[3] and contains the settings for motors and the effect duration and delay. A convenience macro UOJOYIO_RESET_RUMBLE is provided which sets all motor activity to 0, and it can for example be used during initialization to check the return value for support of UJOYIO_RUMBLE without activating the motors, as done in the included sdl2 implementation. Whereas Linux has a complex system of layers to queue the calls and several custom ioctls (see xpad.c[1], input.h[2], the evdev system), this approach is a lot more straight forward. It offers an ioctl for the various parameters of direct motor control used by the XBox One controller, as documented in the MS-GIPUSB [3] specification. The Xbox 360 controller is also implemented - it is a lot simpler, basically only takes values for left and right vibration (which is all that sdl2 works with anyway). There are a number of third-party gamecontrollers that also use these protocols. I have a very simple test program for the driver function hosted at [4]. I've included a diff for sdl2 to enable the functionality. With this, many games can use the feature, including supertuxkart and sdlpop from ports, and a number of proprietary games that run with games/indierunner, such as: Bleed, Bleed 2, FEZ, Salt & Sanctuary, Cryptark, Apotheon, Chasm, Dust: An Elysian Tail. sdl2 also comes with interactive test programs, not compiled by the port, but the one to test the rumble/haptic feedback can be built like this: $ cd test $ ./configure $ gmake testjoystick $ ./testjoystick Pressing the button 0 triggers a half-second long controller vibration if supported. I have tested it with the original Xbox One controller (model 1537) and an original Xbox 360 controller. In the end, this is a function that has been used for over 15 years now to enhance the immersive experience when using a controller. Generally, games that support it also have a settings option to disable it in their in-game menus for those who prefer to play the games without the functionality. Input appreciated! This was largely completed and tested at h2k24. Thanks to bentley@, brynet@, jan@ for input on this project. [1] https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c [2] https://github.com/torvalds/linux/blob/master/include/linux/input.h [3] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc [4] https://github.com/rfht/rumbletest-openbsd Index: sys/dev/usb/uhidev.c =================================================================== RCS file: /cvs/src/sys/dev/usb/uhidev.c,v diff -u -p -r1.110 uhidev.c --- sys/dev/usb/uhidev.c 23 May 2024 03:21:09 -0000 1.110 +++ sys/dev/usb/uhidev.c 19 Nov 2024 14:14:31 -0000 @@ -365,6 +365,7 @@ uhidev_use_rdesc(struct uhidev_softc *sc } else if ((id->bInterfaceClass == UICLASS_VENDOR && id->bInterfaceSubClass == UISUBCLASS_XBOX360_CONTROLLER && id->bInterfaceProtocol == UIPROTO_XBOX360_GAMEPAD)) { + sc->sc_flags |= UHIDEV_F_XB360; /* The Xbox 360 gamepad has no report descriptor. */ size = sizeof(uhid_xb360gp_report_descr); descptr = uhid_xb360gp_report_descr; Index: sys/dev/usb/uhidev.h =================================================================== RCS file: /cvs/src/sys/dev/usb/uhidev.h,v diff -u -p -r1.41 uhidev.h --- sys/dev/usb/uhidev.h 21 Mar 2022 12:18:52 -0000 1.41 +++ sys/dev/usb/uhidev.h 19 Nov 2024 14:14:31 -0000 @@ -58,6 +58,7 @@ struct uhidev_softc { u_int sc_flags; #define UHIDEV_F_XB1 0x0001 /* Xbox One controller */ +#define UHIDEV_F_XB360 0x0002 /* Xbox 360 controller */ }; struct uhidev { Index: sys/dev/usb/ujoy.c =================================================================== RCS file: /cvs/src/sys/dev/usb/ujoy.c,v diff -u -p -r1.5 ujoy.c --- sys/dev/usb/ujoy.c 23 May 2024 03:21:09 -0000 1.5 +++ sys/dev/usb/ujoy.c 19 Nov 2024 14:14:31 -0000 @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -29,9 +28,11 @@ #include #include +#include #include #include +#include int ujoy_match(struct device *, void *, void *); @@ -113,6 +114,62 @@ ujoy_match(struct device *parent, void * } int +ujoyio_rumble(struct uhidev *scd, u_long cmd, caddr_t addr, int flag, struct proc *p) +{ + struct uhidev_softc *sc = scd->sc_parent; + struct ujoy_rumble *ur = (struct ujoy_rumble *)addr; + usbd_status err; + u_int8_t *data; + u_int8_t s; + int error; + + /* + * no checking is done on inputs, consumers should adhere to + * MS-GIPUSB Direct Motor Command, section 3.1.5.6.1. + * Third byte needs to be 0x00, otherwise it will be interpreted + * as an incomplete package and controller will not accept + * further incoming packages. + */ + + /* + * xb1: max vibration/impulse value is 100/0x64 + * x360: max vibration/impulse is 255 + */ + + uint8_t data_xb1[] = { GIP_CMD_RUMBLE, 0x00, 0x00, GIP_PL_LEN(9), + 0x00, ur->motors, ur->left_impulse / 655, ur->right_impulse / 655, + ur->left_vibration / 655, ur->right_vibration / 655, ur->duration, ur->delay, + ur->repeat }; + + uint8_t data_xb360[] = { 0x00, 0x08, 0x00, ur->left_vibration / 256, + ur->right_vibration / 256, 0x00, 0x00, 0x00 }; + + if (sc->sc_flags & UHIDEV_F_XB1) { + data = data_xb1; + s = sizeof(data_xb1); + } else if (sc->sc_flags & UHIDEV_F_XB360) { + data = data_xb360; + s = sizeof(data_xb360); + } else { + /* currently only supported on XBox One/360 controller types */ + return (ENODEV); + } + + usbd_setup_xfer(sc->sc_oxfer, sc->sc_opipe, 0, + data, s, + USBD_SYNCHRONOUS | USBD_CATCH, USBD_NO_TIMEOUT, + NULL); + err = usbd_transfer(sc->sc_oxfer); + if (err != USBD_NORMAL_COMPLETION) { + printf("ujoyopen: ujoyio_rumble failed, " + "error=%d\n", err); + error = EIO; + return (error); + } + return (0); +} + +int ujoyopen(dev_t dev, int flag, int mode, struct proc *p) { /* Restrict ujoy devices to read operations */ @@ -124,6 +181,9 @@ ujoyopen(dev_t dev, int flag, int mode, int ujoyioctl(dev_t dev, u_long cmd, caddr_t addr, int flag, struct proc *p) { + struct uhid_softc *scd; + int error; + switch (cmd) { case FIONBIO: case FIOASYNC: @@ -132,6 +192,17 @@ ujoyioctl(dev_t dev, u_long cmd, caddr_t case USB_GET_REPORT_DESC: case USB_GET_REPORT_ID: break; + case UJOYIO_RUMBLE: + /* like in uhidioctl() */ + if ((scd = uhid_lookup(dev)) == NULL) + return (ENXIO); + scd->sc_refcnt++; + if (usbd_is_dying(scd->sc_hdev.sc_udev)) + return (EIO); + error = ujoyio_rumble(&scd->sc_hdev, cmd, addr, flag, p); + if (--scd->sc_refcnt < 0) + usb_detach_wakeup(&scd->sc_hdev.sc_dev); + return (error); default: return (EPERM); } Index: sys/dev/usb/ujoyio.h =================================================================== RCS file: sys/dev/usb/ujoyio.h diff -N sys/dev/usb/ujoyio.h --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ sys/dev/usb/ujoyio.h 19 Nov 2024 14:14:31 -0000 @@ -0,0 +1,74 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2024 Thomas Frohwein + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _DEV_USB_UJOYIO_H +#define _DEV_USB_UJOYIO_H + +#include + +/* + * Cherry-picked from Gaming Input Protocol (GIP) Extension, see + * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc + * + * FORMAT + * Offset Name + * 0 GIP_CMD_* + * 1 GIP_FLAGS + * 2 GIP_SEQUENCE_ID + * 3 GIP Payload Length (pad header to even number of bytes) + * 4+ Payload + */ + +#define BIT(x) (1U << (x)) + +#ifdef _KERNEL +#define GIP_CMD_SET_STATE 0x05 +#define GIP_CMD_RUMBLE 0x09 /* downstream */ +#define GIP_OPT_INTERNAL BIT(5) +#define GIP_PL_LEN(N) (N) /* length of command payload */ +#endif /* _KERNEL */ + +#define GIP_MOTOR_R BIT(0) /* right vibration motor */ +#define GIP_MOTOR_L BIT(1) /* left vibration motor */ +#define GIP_MOTOR_RT BIT(2) /* right impulse motor */ +#define GIP_MOTOR_LT BIT(3) /* left impulse motor */ +#define GIP_MOTOR_ALL (GIP_MOTOR_R | GIP_MOTOR_L | GIP_MOTOR_RT | \ + GIP_MOTOR_LT) + +struct ujoy_rumble { + u_int8_t motors; /* XB1 only; bitmask of GIP_MOTOR_* */ + + /* impulse and vibration are 0x00 to 0xffff */ + u_int16_t left_impulse; /* XB1 only */ + u_int16_t right_impulse; /* XB1 only */ + u_int16_t left_vibration; + u_int16_t right_vibration; + + u_int8_t duration; /* XB1 only: 0 to 255 in 10ms steps */ + u_int8_t delay; /* XB1 only: 0 to 255 in 10ms steps */ + u_int8_t repeat; /* XB1 only: 0 to 255 repetitions */ +}; + +/* send to stop all activity; can also send to check if rumble is supported */ +#define UJOYIO_RESET_RUMBLE (struct ujoy_rumble)\ + {GIP_MOTOR_ALL, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00} + +#define UJOYIO_RUMBLE _IOW('J', 7, struct ujoy_rumble) + +#endif /* _DEV_USB_UJOYIO_H */ Index: share/man/man4/ujoy.4 =================================================================== RCS file: /cvs/src/share/man/man4/ujoy.4,v diff -u -p -r1.2 ujoy.4 --- share/man/man4/ujoy.4 27 Jan 2021 14:58:06 -0000 1.2 +++ share/man/man4/ujoy.4 19 Nov 2024 14:14:31 -0000 @@ -38,10 +38,88 @@ and a subset of operations of the generic .Xr uhid 4 device. +.Pp +The +.Nm +driver implements a user interface for haptic feedback +.Pq direct motor control +for selected devices based on the Gaming Input Protocol USB Extension via the +following +.Xr ioctl 2 +call. +It is defined in +.In dev/usb/ujoyio.h . +.Bl -tag -width Ds +.It Dv UJOYIO_RUMBLE +.Pq Li "struct ujoy_rumble" +Send direct motor command. +The argument structure is as follows: +.Bd -literal -offset indent +struct ujoy_rumble { + u_int8_t motors; + u_int16_t left_impulse; + u_int16_t right_impulse; + u_int16_t left_vibration; + u_int16_t right_vibration; + u_int8_t duration; + u_int8_t delay; + u_int8_t repeat; +}; +.Ed +.Pp +.Va motors +is a bit mask of +.Dv GIP_MOTOR_LT , +.Dv GIP_MOTOR_RT , +.Dv GIP_MOTOR_L , +and +.Dv GIP_MOTOR_R . +.Bl -tag -width Ds +.It Dv GIP_MOTOR_LT +left impulse motor +.It Dv GIP_MOTOR_RT +right impulse motor +.It Dv GIP_MOTOR_L +left vibration motor +.It Dv GIP_MOTOR_R +right vibration motor +.El +.Pp +.Dv GIP_MOTOR_ALL +can be used to address all motors. +.Pp +.Va left_impulse , +.Va right_impulse , +.Va left_vibrations , +and +.Va right_vibration +are the respective percentage of motor activity, from +.Dv 0x00 +to +.Dv 0x64 . +.Va duration +and +.Va delay +take values from 0 to 255, representing a multiple of 10ms intervals. +.Va repeat +ranges from 0 to 255 repetitions. +.El +.Pp +Sending +.Dv UJOYIO_RESET_RUMBLE +resets all motors and can be used to check if motor control is supported. .Sh FILES .Bl -tag -width /dev/ujoy/* -compact .It Pa /dev/ujoy/* .El +.Sh EXAMPLES +Check quietly if motor control is supported by sending the reset command: +.Bd -literal -offset indent +int fd = open("/dev/ujoy/0", O_RDONLY | O_CLOEXEC); +if (ioctl(fd, UJOYIO_RUMBLE, UJOYIO_RESET_RUMBLE) == 0) { + /* motor control is supported */ +} +.Ed .Sh SEE ALSO .Xr uhid 4 , .Xr uhidev 4 , @@ -51,3 +129,7 @@ The .Nm driver first appeared in .Ox 6.9 . +The +.Dv UJOYIO_RUMBLE +interface was introduced in +.Ox 7.7 . Index: Makefile =================================================================== RCS file: /cvs/ports/devel/sdl2/Makefile,v diff -u -p -r1.63 Makefile --- Makefile 7 Nov 2024 09:54:42 -0000 1.63 +++ Makefile 16 Nov 2024 06:39:57 -0000 @@ -3,7 +3,7 @@ COMMENT= cross-platform multimedia libra V= 2.30.9 DISTNAME= SDL2-${V} PKGNAME= sdl2-${V} -REVISION= 1 +REVISION= 2 CATEGORIES= devel SITES= https://www.libsdl.org/release/ @@ -64,6 +64,8 @@ CONFIGURE_ARGS+= --enable-altivec=no .endif # tests in test subdir, but interactive and not hooked up to build +# to build them: +# cd ${WRKSRC}/test; ./configure; gmake NO_TEST= Yes pre-configure: Index: patches/patch-src_joystick_bsd_SDL_bsdjoystick_c =================================================================== RCS file: /cvs/ports/devel/sdl2/patches/patch-src_joystick_bsd_SDL_bsdjoystick_c,v diff -u -p -r1.13 patch-src_joystick_bsd_SDL_bsdjoystick_c --- patches/patch-src_joystick_bsd_SDL_bsdjoystick_c 24 Feb 2024 14:20:49 -0000 1.13 +++ patches/patch-src_joystick_bsd_SDL_bsdjoystick_c 16 Nov 2024 06:39:57 -0000 @@ -6,11 +6,19 @@ backport D-pad calculation by bitwise op https://github.com/libsdl-org/SDL/pull/7996 break from axis/hat assignments to not override hat with 0 after HUG_DPAD_* +add Rumble support Index: src/joystick/bsd/SDL_bsdjoystick.c --- src/joystick/bsd/SDL_bsdjoystick.c.orig +++ src/joystick/bsd/SDL_bsdjoystick.c -@@ -93,40 +93,11 @@ +@@ -87,46 +87,18 @@ + #define MAX_JOYS (MAX_UHID_JOYS + MAX_JOY_JOYS) + + #ifdef __OpenBSD__ ++#include + + #define HUG_DPAD_UP 0x90 + #define HUG_DPAD_DOWN 0x91 #define HUG_DPAD_RIGHT 0x92 #define HUG_DPAD_LEFT 0x93 @@ -51,7 +59,42 @@ Index: src/joystick/bsd/SDL_bsdjoystick. #endif struct report -@@ -716,27 +687,55 @@ static void BSD_JoystickUpdate(SDL_Joystick *joy) +@@ -195,6 +167,7 @@ struct joystick_hwdata + struct report_desc *repdesc; + struct report inreport; + int axis_map[JOYAXE_count]; /* map present JOYAXE_* to 0,1,.. */ ++ SDL_bool ff_rumble; + }; + + /* A linked list of available joysticks */ +@@ -289,6 +262,7 @@ CreateHwData(const char *path) + struct report *rep = NULL; + int fd; + int i; ++ int err; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd == -1) { +@@ -401,6 +375,18 @@ CreateHwData(const char *path) + SDL_SetError("%s: Not a joystick, ignoring", path); + goto usberr; + } ++ ++#ifdef __OpenBSD__ ++ SDL_AssertJoysticksLocked(); ++ ++ /* send silent test packet to ujoy(4) to see if ff_rumble is supported */ ++ err = ioctl(fd, UJOYIO_RUMBLE, &UJOYIO_RESET_RUMBLE); ++ if (err == 0) { ++ hw->ff_rumble = SDL_TRUE; ++ } else { ++ hw->ff_rumble = SDL_FALSE; ++ } ++#endif /* __OpenBSD__ */ + } + + /* The poll blocks the event thread. */ +@@ -716,27 +702,55 @@ static void BSD_JoystickUpdate(SDL_Joystick *joy) /* scaleaxe */ v = (Sint32)hid_get_data(REP_BUF_DATA(rep), &hitem); v = (((SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN) * (v - hitem.logical_minimum)) / (hitem.logical_maximum - hitem.logical_minimum)) + SDL_JOYSTICK_AXIS_MIN; @@ -115,3 +158,58 @@ Index: src/joystick/bsd/SDL_bsdjoystick. #endif break; } +@@ -836,7 +850,37 @@ static void report_free(struct report *r) + + static int BSD_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) + { ++#ifndef __OpenBSD__ + return SDL_Unsupported(); ++#else ++ struct ujoy_rumble *ur; ++ int retval; ++ ++ ur = SDL_malloc(sizeof(*ur)); ++ ur->motors = GIP_MOTOR_ALL; ++ ur->left_impulse = 0x00; ++ ur->right_impulse = 0x00; ++ ur->left_vibration = low_frequency_rumble; ++ ur->right_vibration = high_frequency_rumble; ++ /* ++ * Duration and repeat are hardcoded because this is managed at the level ++ * of SDL_JoystickRumble. ++ */ ++ ur->duration = 0xff; ++ ur->delay = 0x00; ++ ur->repeat = 0xff; ++ ++ /* send effect to the device */ ++ SDL_AssertJoysticksLocked(); ++ retval = ioctl(joystick->hwdata->fd, UJOYIO_RUMBLE, ur); ++ free(ur); ++ ++ if (retval != 0) { ++ return SDL_SetError("Error trying to play rumble effect: %s", strerror(errno)); ++ } ++ ++ return 0; ++#endif /* __OpenBSD__ */ + } + + static int BSD_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) +@@ -851,7 +895,15 @@ static SDL_bool BSD_JoystickGetGamepadMapping(int devi + + static Uint32 BSD_JoystickGetCapabilities(SDL_Joystick *joystick) + { +- return 0; ++ Uint32 result = 0; ++ ++ SDL_AssertJoysticksLocked(); ++ ++ if (joystick->hwdata->ff_rumble) { ++ result |= SDL_JOYCAP_RUMBLE; ++ } ++ ++ return result; + } + + static int BSD_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)