Download raw body.
Adding rumble/force feedback control to ujoy(4)
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 <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
-#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/tty.h>
#include <sys/fcntl.h>
@@ -29,9 +28,11 @@
#include <dev/usb/usbhid.h>
#include <dev/usb/usbdi.h>
+#include <dev/usb/usbdi_util.h>
#include <dev/usb/uhidev.h>
#include <dev/usb/uhid.h>
+#include <dev/usb/ujoyio.h>
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 <thfr@openbsd.org>
+ *
+ * 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 <sys/ioctl.h>
+
+/*
+ * 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 <dev/usb/ujoyio.h>
+
+ #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)
Adding rumble/force feedback control to ujoy(4)