Hacking XPAD Kernel Driver for Fun and Profits (X3 Albion Prelude)

Reproduced from https://sites.google.com/site/sbobovyc/home/guides/xbox-one-controller-for-x3-albion-prelude

After a long hiatus from X games, I decided to fire up X3 Albion Prelude on my Linux laptop. I played X3 Reunion and X3 Terran Conflict exclusively with a keyboard and mouse, but this time around I wanted to try using my XBox One controller for flying, dog fighting and possibly some management. So, I plugged in the controller and started the game launcher which had an option for joystick configuration.


The game uses SDL2 for joystick support, and my controller was detected correctly though I had to check “Use as Xbox controller” to disabled the Rudder and Throttle. Clicking on “Remap Buttons” brought up the following dialog box.


I had to click “Reset to Xbox controller” to make the button codes map to button names. After this, I started the game and took some time to adjust myself to using the controller for this game. Everything seemed to work fine, except for the D-pad. Instead of doing actions such as docking and bringing up comms, the camera perspective changed. Digging through the in-game controls, it looked like I was using a hat switch to change views. It may be possible to remap the hat switch to actions I wanted on the D-pad, but I would not be satisfied if I could not figure out why this is happening. Clearly, this game was designed with various controllers in mind, including the Xbox 360 controller. Were the button mappings wrong? I decided to dig deep.

First, I wrote a python script that used pysdl2 to check the events generated by the Xbox One controller:

After doing some research, I realized that there already exists a tool for this called jstst, but I wanted to play around with pysdl2 at some point anyway. With my tool and jsts I was able to see that the D-pad buttons were not recognized as buttons by SDL2, but as a hat switch. This makes sense given how the game was switching views when I pressed buttons on the D-pad. Luckily, I can take a look at the driver code to figure out what’s going on.

Very quickly I spotted the culprit of this behavior in xpad.c:

Looking in xpadone_process_buttons():

551         /* digital pad */
552         if (xpad->mapping & MAP_DPAD_TO_BUTTONS) {
553                 /* dpad as buttons (left, right, up, down) */
554                 input_report_key(dev, BTN_TRIGGER_HAPPY1, data[5] & 0x04);
555                 input_report_key(dev, BTN_TRIGGER_HAPPY2, data[5] & 0x08);
556                 input_report_key(dev, BTN_TRIGGER_HAPPY3, data[5] & 0x01);
557                 input_report_key(dev, BTN_TRIGGER_HAPPY4, data[5] & 0x02);
558         } else {
559                 input_report_abs(dev, ABS_HAT0X,
560                                  !!(data[5] & 0x08) - !!(data[5] & 0x04));
561                 input_report_abs(dev, ABS_HAT0Y,
562                                  !!(data[5] & 0x02) - !!(data[5] & 0x01));
563         }

So xpad->mapping is responsible for determining if the D-pad is mapped to buttons or to a hat switch. This struct field is set during xpad_probe():

1130 if (dpad_to_buttons)
1131 xpad->mapping |= MAP_DPAD_TO_BUTTONS;

The variable dpad_to_buttons is a module parameter:
module_param(dpad_to_buttons, bool, S_IRUGO);

This can be easily seen with modinfo:
$ modinfo –parameters xpad
dpad_to_buttons:Map D-PAD to buttons rather than axes for unknown pads (bool)
triggers_to_buttons:Map triggers to buttons rather than axes for unknown pads (bool)
sticks_to_null:Do not map sticks at all for unknown pads (bool)

I can check the value of the module parameter through the proc filesystem:
$ cat /sys/module/xpad/parameters/dpad_to_buttons

Afterwards, I unloaded the kernel module then loaded the module with the dpad mapping set:

$ sudo modprobe -r xpad
$ sudo modprobe xpad dpad_to_buttons=1
$ cat /sys/module/xpad/parameters/dpad_to_buttons

Thinking that I had found the fix, I then made sure that the module was always loaded with that parameter set to true by editing /etc/modprobe.d/xboxone.conf:
# Map Xbox One D-pad to buttons instead of hat
options xpad dpad_to_buttons=1

I ran my python script and saw that the D-pad events were still being interpreted as hat switch events. After reading the xpad documentation, I realized that dpad_to_buttons only works with controllers which are unknown.


It was now time to modify some kernel source. I grabbed a copy of my running kernel’s source and set things up for compiling modules:
$ cd ~
$ cp /usr/src/linux-source-4.2.0/linux-source-4.2.0.tar.bz2 .
$ tar xf linux-source-4.2.0.tar.bz2
$ cp /boot/config-uname -r .config
$ make oldconfig
$ cp -v /usr/src/linux-headers-uname -r/Module.symvers .

I then modified drivers/input/joystick/xpad.c:
{ 0x045e, 0x02d1, "Microsoft X-Box One pad", MAP_DPAD_TO_BUTTONS, XTYPE _XBOXONE }

And compiled and installed the new module:
$ make -C /lib/modules/4.2.0-19-generic/build M=$(pwd) drivers/input/ff-memless.ko
$ make -C /lib/modules/4.2.0-19-generic/build M=$(pwd) drivers/input/joystick/xpad.ko
$ mv /lib/modules/uname -r/kernel/drivers/input/joystick/xpad.ko /lib/modules/uname -r/kernel/drivers/input/joystick/xpad.ko.bk
$ cp drivers/input/joystick/xpad.ko /lib/modules/uname -r/kernel/drivers/input/joystick/

And finally reloaded the module:
$ rmmod xpad
$ modprob xpad

My python script now showed that D-pad events were button events! I started up the game and everything worked as it should, so great success. I could have continued to play the game,
but I decided to revert the code back to original. Instead of modifying the controller definition, I modified xpad_prob to use controller type and module parameter to set the D-pad mapping:

if (xpad->xtype == XTYPE_XBOXONE) {
if (dpad_to_buttons)
xpad->mapping |= MAP_DPAD_TO_BUTTONS;

I reloaded the driver with the dpad_to_buttons set and the controller worked as expected.

Future work
Changing the behavior of the kernel module through the module parameter is cumbersome since the module has to be reloaded. There is a userspace Xbox controller driver called xboxdrv, but
I thought that it was more work setting that up than compiling a custom module. In theory, I can change the module parameter permissions from 0111 to 211 with S_IRUGO|S_IWUSR so that I could
use the proc filesystem to change the D-pad mapping without unloading the driver. While I was doing research for this topic, I ran across a different solution that was used for Xbox 360 wireless controller https://github.com/dtor/input/commit/5ee8bda943de20017636845a5c8d7069a4a283b8

I really hope this behavior is rationalized by the Linux Kernel developers so that game and library developers don’t have to deal with this BS.


I am glad I got this working because X3 is fun and I am happy to spend more of my gaming time playing on Linux.

--- linux-source-4.2.0/drivers/input/joystick/xpad.c.orig 2015-12-18 20:48:43.505725676 -0500
+++ linux-source-4.2.0/drivers/input/joystick/xpad.c 2015-12-17 21:01:08.956996392 -0500
@@ -1135,6 +1135,12 @@ static int xpad_probe(struct usb_interfa
xpad->mapping |= MAP_STICKS_TO_NULL;

+ if (xpad->xtype == XTYPE_XBOXONE) {
+ if (dpad_to_buttons)
+ xpad->mapping |= MAP_DPAD_TO_BUTTONS;
+ }
xpad->dev = input_dev;
usb_make_path(udev, xpad->phys, sizeof(xpad->phys));
strlcat(xpad->phys, "/input0", sizeof(xpad->phys));