Index | Thread | Search

From:
Thomas Frohwein <tfrohwein@fastmail.com>
Subject:
Adding rumble/force feedback control to ujoy(4)
To:
tech@openbsd.org
Date:
Tue, 19 Nov 2024 09:40:41 -0500

Download raw body.

Thread
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)