Linux Gamepad Input on jme3-lwjgl Splits Input Between Two Logical Gamepads

I’m using jMonkey for my split-screen multiplayer game, and I’ve seem to come across a rather bizarre bug with the gamepad input when using the jme3-lwjgl module. Essentially, when a gamepad is connected (I’m using a Wireless Xbox 360 controller via Microsoft’s official receiver), jMonkey sees two joysticks instead of 1. Now, the first joystick will properly register all axis inputs (joysticks and triggers). However, all button inputs will instead be registered on the second gamepad. According to my tests, this problem will scale across multiple gamepads, with axis inputs registering on the even indexes (starting at 0) and buttons registering on the odd ones.
This problem isn’t very obvious with the default TestJoystick example. I actually found it through my own utility. I exported it to https://github.com/Markil3/JMEControllerConfig if anyone wants to test it out themselves. Just change the main project’s build.gradle to rely on jme3-lwjgl as a dependency rather than jme3-lwjgl3.
I’m not sure where the problem originates from quite yet. I don’t have anything like this problem showing up on Godot, so I doubt it is something wrong with my configuration. If this problem can be duplicated in Windows, that would confirm that (I’ll post an update when I get around to testing it on my Windows installation). This likely means that there is a bug somewhere in the JME code or in LWJGL2. At the moment, this issue is solved by switching to use LWJGL3 (this has its own issues with gamepads, but that is a problem for a different post). I just figured we might as well get the ball rolling on tracking down the source of the error, if at the very least to maintain some documentation here as to what is going on for anyone else running into problems.

1 Like

Here are the dumps from the TestJoystick example, if that is useful. In both cases, I’ve connected a single Xbox 360 Wireless gamepad to my Linux machine.

With LWJGL2:

Joystick[0]:Xbox 360 Wireless Receiver
  buttons:10
   JoystickButton[name=A, parent=Xbox 360 Wireless Receiver, id=0, logicalId=2]
   JoystickButton[name=B, parent=Xbox 360 Wireless Receiver, id=1, logicalId=1]
   JoystickButton[name=X, parent=Xbox 360 Wireless Receiver, id=2, logicalId=3]
   JoystickButton[name=Y, parent=Xbox 360 Wireless Receiver, id=3, logicalId=0]
   JoystickButton[name=Left Thumb, parent=Xbox 360 Wireless Receiver, id=4, logicalId=4]
   JoystickButton[name=Right Thumb, parent=Xbox 360 Wireless Receiver, id=5, logicalId=5]
   JoystickButton[name=Select, parent=Xbox 360 Wireless Receiver, id=6, logicalId=8]
   JoystickButton[name=Mode, parent=Xbox 360 Wireless Receiver, id=7, logicalId=9]
   JoystickButton[name=Left Thumb 3, parent=Xbox 360 Wireless Receiver, id=8, logicalId=10]
   JoystickButton[name=Right Thumb 3, parent=Xbox 360 Wireless Receiver, id=9, logicalId=11]
  axes:9
   JoystickAxis[name=x, parent=Xbox 360 Wireless Receiver, id=0, logicalId=x, isAnalog=true, isRelative=false, deadZone=9.765774E-4]
   JoystickAxis[name=y, parent=Xbox 360 Wireless Receiver, id=1, logicalId=y, isAnalog=true, isRelative=false, deadZone=9.765774E-4]
   JoystickAxis[name=z, parent=Xbox 360 Wireless Receiver, id=2, logicalId=rx, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=rx, parent=Xbox 360 Wireless Receiver, id=3, logicalId=z, isAnalog=true, isRelative=false, deadZone=9.765774E-4]
   JoystickAxis[name=ry, parent=Xbox 360 Wireless Receiver, id=4, logicalId=rz, isAnalog=true, isRelative=false, deadZone=9.765774E-4]
   JoystickAxis[name=rz, parent=Xbox 360 Wireless Receiver, id=5, logicalId=ry, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov, parent=Xbox 360 Wireless Receiver, id=6, logicalId=pov, isAnalog=false, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov_x, parent=Xbox 360 Wireless Receiver, id=7, logicalId=pov_x, isAnalog=false, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov_y, parent=Xbox 360 Wireless Receiver, id=8, logicalId=pov_y, isAnalog=false, isRelative=false, deadZone=0.0]
Joystick[1]:Xbox 360 Wireless Receiver
  buttons:10
   JoystickButton[name=A, parent=Xbox 360 Wireless Receiver, id=0, logicalId=2]
   JoystickButton[name=B, parent=Xbox 360 Wireless Receiver, id=1, logicalId=1]
   JoystickButton[name=X, parent=Xbox 360 Wireless Receiver, id=2, logicalId=3]
   JoystickButton[name=Y, parent=Xbox 360 Wireless Receiver, id=3, logicalId=0]
   JoystickButton[name=Left Thumb, parent=Xbox 360 Wireless Receiver, id=4, logicalId=4]
   JoystickButton[name=Right Thumb, parent=Xbox 360 Wireless Receiver, id=5, logicalId=5]
   JoystickButton[name=Select, parent=Xbox 360 Wireless Receiver, id=6, logicalId=8]
   JoystickButton[name=Mode, parent=Xbox 360 Wireless Receiver, id=7, logicalId=9]
   JoystickButton[name=Left Thumb 3, parent=Xbox 360 Wireless Receiver, id=8, logicalId=10]
   JoystickButton[name=Right Thumb 3, parent=Xbox 360 Wireless Receiver, id=9, logicalId=11]
  axes:9
   JoystickAxis[name=x, parent=Xbox 360 Wireless Receiver, id=0, logicalId=x, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=y, parent=Xbox 360 Wireless Receiver, id=1, logicalId=y, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=z, parent=Xbox 360 Wireless Receiver, id=2, logicalId=rx, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=rx, parent=Xbox 360 Wireless Receiver, id=3, logicalId=z, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=ry, parent=Xbox 360 Wireless Receiver, id=4, logicalId=rz, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=rz, parent=Xbox 360 Wireless Receiver, id=5, logicalId=ry, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov, parent=Xbox 360 Wireless Receiver, id=6, logicalId=pov, isAnalog=false, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov_x, parent=Xbox 360 Wireless Receiver, id=7, logicalId=pov_x, isAnalog=false, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov_y, parent=Xbox 360 Wireless Receiver, id=8, logicalId=pov_y, isAnalog=false, isRelative=false, deadZone=0.0]

With LWJGL3:

Joystick[0]:Xbox 360 Wireless Receiver
  buttons:19
   JoystickButton[name=0, parent=Xbox 360 Wireless Receiver, id=0, logicalId=2]
   JoystickButton[name=1, parent=Xbox 360 Wireless Receiver, id=1, logicalId=1]
   JoystickButton[name=2, parent=Xbox 360 Wireless Receiver, id=2, logicalId=3]
   JoystickButton[name=3, parent=Xbox 360 Wireless Receiver, id=3, logicalId=0]
   JoystickButton[name=4, parent=Xbox 360 Wireless Receiver, id=4, logicalId=4]
   JoystickButton[name=5, parent=Xbox 360 Wireless Receiver, id=5, logicalId=5]
   JoystickButton[name=6, parent=Xbox 360 Wireless Receiver, id=6, logicalId=8]
   JoystickButton[name=7, parent=Xbox 360 Wireless Receiver, id=7, logicalId=9]
   JoystickButton[name=8, parent=Xbox 360 Wireless Receiver, id=8, logicalId=10]
   JoystickButton[name=9, parent=Xbox 360 Wireless Receiver, id=9, logicalId=11]
   JoystickButton[name=10, parent=Xbox 360 Wireless Receiver, id=10, logicalId=10]
   JoystickButton[name=11, parent=Xbox 360 Wireless Receiver, id=11, logicalId=11]
   JoystickButton[name=12, parent=Xbox 360 Wireless Receiver, id=12, logicalId=12]
   JoystickButton[name=13, parent=Xbox 360 Wireless Receiver, id=13, logicalId=13]
   JoystickButton[name=14, parent=Xbox 360 Wireless Receiver, id=14, logicalId=14]
   JoystickButton[name=15, parent=Xbox 360 Wireless Receiver, id=15, logicalId=15]
   JoystickButton[name=16, parent=Xbox 360 Wireless Receiver, id=16, logicalId=16]
   JoystickButton[name=17, parent=Xbox 360 Wireless Receiver, id=17, logicalId=17]
   JoystickButton[name=18, parent=Xbox 360 Wireless Receiver, id=18, logicalId=18]
  axes:6
   JoystickAxis[name=pov_x, parent=Xbox 360 Wireless Receiver, id=0, logicalId=pov_x, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=pov_y, parent=Xbox 360 Wireless Receiver, id=1, logicalId=pov_y, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=z, parent=Xbox 360 Wireless Receiver, id=2, logicalId=rx, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=rz, parent=Xbox 360 Wireless Receiver, id=3, logicalId=ry, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=4, parent=Xbox 360 Wireless Receiver, id=4, logicalId=4, isAnalog=true, isRelative=false, deadZone=0.0]
   JoystickAxis[name=5, parent=Xbox 360 Wireless Receiver, id=5, logicalId=5, isAnalog=true, isRelative=false, deadZone=0.0]

Without actually looking at the code, there are fundamental differences on how lwjgl handled input between version 2 and 3. I would guess that this bug is actually within lwjgl2 itself and not jme. But I have not looked at the code you linked to yet.

My code is simply a utility that demonstrates the problem better than TestJoystick. I don’t entirely know where it is, myself. It would probably involve someone who is familiar with the internals of jme3-lwjgl and LWJGL2 to track it down. In a pinch, I could do it myself, but I just thought I’d alert the experts on the matter.

1 Like

Note that I think no one has ever reported this on Windows… so in addition to being lwjgl2 specific, it also seems to be Linux specific.

Wonder what’s getting confused in there.

I’ve just checked, and the problem does not occur on Windows 10 when I use the same gampad. Unless jmonkey is using some native library I don’t know about, then the problem is likely occurring in the jinput.so library.
It would be interesting to see if this problem occurs with other gamepads on Linux. However, I only have the wireless Xbox 360 ones, so I can’t really help there.
With that said, I’m working on porting my ControllerConfig application to Godot to make sure that it isn’t just an issue with my OpenGL in general.

Not an expert, but this may be less a bug and more a “Legacy API Limit”

Possibly relevant simillarish issue with a different game engine: https://steamcommunity.com/app/113020/discussions/4/1745644504178583492/ (Found with a quick google search)

Linux currently has two APIs for HID. (Why not? There are multiple ways to do everything else…) The older joystick API and a newer one that is based on evdev. The LWJGL 2.x series began in April 2009, so may be using the legacy API.

If you want to see how the old API reads your gamepad, make sure that it is the only pad/joystick connected, and then ls /dev/input/ if you have both js0 and js1 in the results, run jstest for each of them, and see which axes are mapped where. If they are duplicates, you know that the issue is more at the driver/old API level.

3 Likes

I only see js0. I don’t think that is the issue. Good thought, though.

I just tested it on Godot and only saw one gamepad, so I don’t think it is a problem with OpenGL itself. This likely means that there is an issue with the Linux implementation of jinput, then.

From what I understand, LWJGL2 outsources the code for gamepad input to the JInput library. jMonkey uses LWJGL version 2.9.3. Looking at the POM for that library in the Maven Central repository shows that LWJGL relies on JInput version 2.0.5. Now, I’m not entirely sure when this was released, but the JInput Github repository shows version 2.0.7 being released on 2017. Also, the LWJGL repository remarks that the jinput.jar file hasn’t been updated in 12 years.
My suspicions are that, perhaps by overriding LWJGL’s preference for 2.0.5 with a more up-to-date version of JInput (the latest release, 2.0.9, was published in May of 2018, with the latest commit being in January 2019), it could be possible fix the issue. I have a fork of jMonkey on my computer, so I’ll try testing that out later this afternoon and seeing if that works.

So, just updating the jme3-lwjgl module to rely on the 2.0.9 dependencies actually seems to fix the issue. The joypad dump only reports one gamepad, and all the checks and tests seem to run fine.

if (!hasProperty('mainClass')) {
    ext.mainClass = ''
}

dependencies {
    compile project(':jme3-core')
    compile project(':jme3-desktop')
    compile 'org.lwjgl.lwjgl:lwjgl:2.9.3'
    /*
     * I just added the following two lines
     */
    compile 'net.java.jinput:jinput:2.0.9'
    compile 'net.java.jinput:jinput:2.0.9:natives-all'
}

If no one else has any problems with it, I was thinking about uploading the change to my fork on GitHub and submitting a pull request. Might as well get this fixed before 3.4.

5 Likes

I guess you may want to exclude jinput from lwjgl there, not sure if it’s needed, but it’d be more explicit and may help

When I check out the dependencies in Android Studio, Version 2.0.5 doesn’t actually show up, just 2.0.9. I’m pretty sure that 2.0.5 is not used in the final dependencies.

Also, according to the Gradle documentation:

Gradle resolves any dependency version conflicts by selecting the latest version found in the dependency graph.

Nevertheless, if you feel like it would make things clearer, then I can add that.

if (!hasProperty('mainClass')) {
    ext.mainClass = ''
}

dependencies {
    compile project(':jme3-core')
    compile project(':jme3-desktop')
    compile ('org.lwjgl.lwjgl:lwjgl:2.9.3') {
        exclude group: 'net.java.jinput', module: 'jinput'
        exclude group: 'net.java.jinput', module: 'jinput-platform'
    }
    compile 'net.java.jinput:jinput:2.0.9'
    compile 'net.java.jinput:jinput:2.0.9:natives-all'
}
1 Like

You are right, but in case someone wonders what jinput does and removes it “because it’s part of lwjgl”, or upgrades lwjgl, which may override jinput, that information would be lost.

And I in this case wonder what the difference between jinput-platform and natives-all is.

Basically, jinput-platform is the dependency 2.0.5 uses to load its native libraries, and natives-all is the one 2.0.9 uses. I’m guessing they changed the project structure somewhere down the line.

True. In that case, we should probably at least add a comment to the source explaining the problem and referencing this thread.

if (!hasProperty('mainClass')) {
    ext.mainClass = ''
}

dependencies {
    compile project(':jme3-core')
    compile project(':jme3-desktop')
    compile 'org.lwjgl.lwjgl:lwjgl:2.9.3'
    /*
     * Upgrades the default jinput-2.0.5 to jinput-2.0.9 to fix a bug with gamepads on Linux.
     * See https://hub.jmonkeyengine.org/t/linux-gamepad-input-on-jme3-lwjgl-splits-input-between-two-logical-gamepads/44023/10
     */
    compile 'net.java.jinput:jinput:2.0.9'
    compile 'net.java.jinput:jinput:2.0.9:natives-all'
}

Thanks! With that backing information, we can merge a high quality PR.
The only question remaining would be if we could add a test, much like @sgold always does these.
Unfortunately mechanical testing won’t help us here, so it would need human interaction.
Not sure if that’s worth it.

Since human interaction is required, could TestJoystick serve as a test?

I think that the TestJoystick example does a pretty good job at showing the primary symptom of the bug if you’re paying attention to the dumps. However, it tends to hide the second problem of the inputs being split across the perceived gamepads. The utility I linked was how I found that problem, and does a better job of showing it. If we really wanted to be thorough, we could replace TestJoystick with my utility (not an entirely unreasonable idea, considering that my utility was originally derived from TestJoystick). If anyone is interested, I could put that in the PR for review. However, as I said, TestJoystick as it is will show the main problem well enough.

1 Like

So, what’s the plan? At the moment, I was thinking of just pushing the new build.gradle to my fork and submitting a pull request. On the other hand, if we think that TestJoystick is in need of an update, let me know and I’ll post my new version for us to discuss and implement alongside it.
At this point, I’m just waiting for a sign of consensus before I take further action.

What changes did you make? To me the existing one illustrates the problem enough to continue… but I’m just wondering what else was done.