Просмотр исходного кода

Fix gamepads with Xbox-like remapping and SDL in lwjgl3 backend (#2579)

* LWJGL 3.4.0 and Remove OpenVR

@stephengold notified me that LWJGL 3.4 has been released and has also done some testing in their non jME projects with this new version already.  (big thanks for keeping a tab on this area)

I still have most of my own jME projects running lwjgl2 ('ve been procrastinating upgrading to LWJGL3 for quite a while now, long overdue), so I will finally do so in my own projects so I can help test as well. 

OpenVR is also no longer supported in LWJGL 3.4.0 so it appears it needed removed, but (unless I'm mistaken) this should not be an issue since the Tamarin VR library is using the newer openXR, and openVR is considered outdated and deprecated. Any jME apps still relying on openVR can still use v3.9 or earlier without issue (@richardTingle correct me if I'm incorrect on any of this)

* Remove OpenVr

* Delete jme3-vr directory

* Remove OpenVR References from BulletDebugAppState.java

* Change int to long for LWJGL 3.4.0 compatibility

* Replace switch with if statements

* Change int to long in LwjglPlatform.java

* map every controller to an xbox-like layout

* cleanup glfw mapper, and add SDL based joystick handling

* use isEmpty instead of isBlank

---------

Co-authored-by: Ryan McDonough <[email protected]>
Riccardo Balbo 12 часов назад
Родитель
Сommit
8b7e7997c8

+ 1 - 0
.gitignore

@@ -50,3 +50,4 @@ javadoc_deploy.pub
 !.vscode/settings.json
 !.vscode/JME_style.xml
 !.vscode/extensions.json
+joysticks-*.txt

+ 1 - 0
gradle/libs.versions.toml

@@ -32,6 +32,7 @@ lwjgl3-jemalloc = { module = "org.lwjgl:lwjgl-jemalloc", version.ref = "lwjgl3"
 lwjgl3-openal   = { module = "org.lwjgl:lwjgl-openal",   version.ref = "lwjgl3" }
 lwjgl3-opencl   = { module = "org.lwjgl:lwjgl-opencl",   version.ref = "lwjgl3" }
 lwjgl3-opengl   = { module = "org.lwjgl:lwjgl-opengl",   version.ref = "lwjgl3" }
+lwjgl3-sdl      = { module = "org.lwjgl:lwjgl-sdl",      version.ref = "lwjgl3" }
 
 mokito-core = "org.mockito:mockito-core:3.12.4"
 

+ 8 - 1
jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java

@@ -48,6 +48,7 @@ public class DefaultJoystickAxis implements JoystickAxis {
     private final boolean isAnalog;
     private final boolean isRelative;
     private float deadZone;
+    private float jitterThreshold = 0f;
 
     /**
      *  Creates a new joystick axis instance. Only used internally.
@@ -166,6 +167,12 @@ public class DefaultJoystickAxis implements JoystickAxis {
     public String toString() {
         return "JoystickAxis[name=" + name + ", parent=" + parent.getName() + ", id=" + axisIndex
                                     + ", logicalId=" + logicalId + ", isAnalog=" + isAnalog
-                                    + ", isRelative=" + isRelative + ", deadZone=" + deadZone + "]";
+                                    + ", isRelative=" + isRelative + ", deadZone=" + deadZone + 
+                                    ", jitterThreshold=" + jitterThreshold + "]";
+    }
+
+    @Override
+    public float getJitterThreshold() {
+        return jitterThreshold;
     }
 }

+ 19 - 21
jme3-core/src/main/java/com/jme3/input/JoystickAxis.java

@@ -44,27 +44,14 @@ public interface JoystickAxis {
     public static final String Z_ROTATION = "rz";
     public static final String LEFT_TRIGGER = "rx";
     public static final String RIGHT_TRIGGER = "ry";
-    
-    // Note: the left/right trigger bit may be a bit controversial in
-    // the sense that this is one case where XBox controllers make a lot
-    // more sense.
-    // I've seen the following mappings for various things:
-    // 
-    // Axis          | XBox  | Non-Xbox (generally) (includes actual Sony PS4 controllers)
-    // --------------+-------+---------------
-    // left trigger  | z     | rx   (also button 6)
-    // right trigger | rz    | ry   (also button 7)
-    // left stick x  | x     | x
-    // left stick y  | y     | y
-    // right stick x | rx    | z
-    // right stick y | ry    | rz
-    //
-    // The issue is that in all cases I've seen, the XBox controllers will
-    // use the name "xbox" somewhere in their name.  The Non-XBox controllers
-    // never mention anything uniform... even the PS4 controller only calls
-    // itself "Wireless Controller".  In that light, it seems easier to make
-    // the default the ugly case and the "XBox" way the exception because it
-    // can more easily be identified.
+
+    public static final String AXIS_XBOX_LEFT_TRIGGER = LEFT_TRIGGER;
+    public static final String AXIS_XBOX_RIGHT_TRIGGER = RIGHT_TRIGGER;
+    public static final String AXIS_XBOX_LEFT_THUMB_STICK_X = X_AXIS;
+    public static final String AXIS_XBOX_LEFT_THUMB_STICK_Y = Y_AXIS;
+    public static final String AXIS_XBOX_RIGHT_THUMB_STICK_X = Z_AXIS;
+    public static final String AXIS_XBOX_RIGHT_THUMB_STICK_Y = Z_ROTATION;
+
 
     public static final String POV_X = "pov_x";
     public static final String POV_Y = "pov_y";
@@ -128,4 +115,15 @@ public interface JoystickAxis {
      * @return the radius of the dead zone
      */
     public float getDeadZone();
+
+
+    /**
+     * Returns the suggested jitter threshold for this axis. Movements with a delta
+     * smaller than this threshold will be ignored by the backend input system
+     * 
+     * @return the jitter threshold
+     */
+    public default float getJitterThreshold(){
+        return 0;
+    }
 }

+ 20 - 0
jme3-core/src/main/java/com/jme3/input/JoystickButton.java

@@ -55,6 +55,26 @@ public interface JoystickButton {
     public static final String BUTTON_14 = "14";
     public static final String BUTTON_15 = "15";
 
+
+    public static final String BUTTON_XBOX_A = BUTTON_2;
+    public static final String BUTTON_XBOX_B = BUTTON_1;
+    public static final String BUTTON_XBOX_X = BUTTON_3;
+    public static final String BUTTON_XBOX_Y = BUTTON_0;
+    public static final String BUTTON_XBOX_LB = BUTTON_4;
+    public static final String BUTTON_XBOX_RB = BUTTON_5;
+    public static final String BUTTON_XBOX_LT = BUTTON_6;
+    public static final String BUTTON_XBOX_RT = BUTTON_7;
+    public static final String BUTTON_XBOX_BACK = BUTTON_8;
+    public static final String BUTTON_XBOX_START = BUTTON_9;
+    public static final String BUTTON_XBOX_L3 = BUTTON_10;
+    public static final String BUTTON_XBOX_R3 = BUTTON_11;
+
+    public static final String BUTTON_XBOX_DPAD_UP = BUTTON_12;
+    public static final String BUTTON_XBOX_DPAD_DOWN = BUTTON_13;
+    public static final String BUTTON_XBOX_DPAD_LEFT = BUTTON_14;
+    public static final String BUTTON_XBOX_DPAD_RIGHT = BUTTON_15;
+
+
     /**
      * Assign the mapping name to receive events from the given button index
      * on the joystick.

+ 110 - 0
jme3-core/src/main/java/com/jme3/system/AppSettings.java

@@ -268,6 +268,31 @@ public final class AppSettings extends HashMap<String, Object> {
      */
     public static final String JOAL = "JOAL";
 
+    /**
+     * Map gamepads to Xbox-like layout.
+     */
+    public static final String JOYSTICKS_XBOX_MAPPER = "JOYSTICKS_XBOX_MAPPER";
+
+    /**
+     * Map gamepads to an Xbox-like layout, with fallback to raw if the gamepad is not recognized.
+     */
+    public static final String JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER = "JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER";
+
+    /**
+     * Map gamepads to an Xbox-like layout using the legacy jME input
+     */
+    public static final String JOYSTICKS_XBOX_LEGACY_MAPPER = "JOYSTICKS_XBOX_LEGACY_MAPPER";
+
+    /**
+     * Map gamepads using the legacy jME mapper and input.
+     */
+    public static final String JOYSTICKS_LEGACY_MAPPER = "JOYSTICKS_LEGACY_MAPPER";
+
+    /**
+     * Don't map gamepads, use raw events instead (ie. bring your own mapper)
+     */
+    public static final String JOYSTICKS_RAW_MAPPER = "JOYSTICKS_RAW_MAPPER";
+
     static {
         defaults.put("Display", 0);
         defaults.put("CenterWindow", true);
@@ -300,6 +325,10 @@ public final class AppSettings extends HashMap<String, Object> {
         defaults.put("WindowYPosition", 0);
         defaults.put("WindowXPosition", 0);
         defaults.put("X11PlatformPreferred", false);
+        defaults.put("JoysticksMapper", JOYSTICKS_XBOX_MAPPER);
+        defaults.put("JoysticksTriggerToButtonThreshold", 0.5f);
+        defaults.put("JoysticksAxisJitterThreshold", 0.0001f);
+        defaults.put("SDLGameControllerDBResourcePath", "");
         //  defaults.put("Icons", null);
     }
 
@@ -1612,4 +1641,85 @@ public final class AppSettings extends HashMap<String, Object> {
     public boolean isX11PlatformPreferred() {
         return getBoolean("X11PlatformPreferred");
     }
+
+    /**
+     * Set which joystick mapping to use for normalization of controller inputs
+     *
+     * @param mapper
+     *            JOYSTICKS_MAPPER_* constant defining which mapping to use
+     */
+    public void setJoysticksMapper(String mapper) {
+        putString("JoysticksMapper", mapper);
+    }
+
+    /**
+     * Get which joystick mapping to use for normalization of controller inputs
+     */
+    public String getJoysticksMapper() {
+        return getString("JoysticksMapper");
+    }
+
+    /**
+     * Sets the threshold above which an analog trigger should also generate a button-press event.
+     * If the value is set to -1, the trigger will never generate button-press events.
+     *
+     * <p>
+     * This is intended to normalize behavior between controllers that expose triggers as analog
+     * axes and controllers that expose triggers as digital buttons.
+     *
+     * @param threshold the trigger threshold in the range [0, 1] (default: 0.5f) 
+     */
+    public void setJoysticksTriggerToButtonThreshold(float threshold) {
+        putFloat("JoysticksTriggerToButtonThreshold", threshold);
+    }
+
+    /**
+     * Gets the threshold above which an analog trigger should also generate a button-press event.
+     *
+     * @return the trigger threshold in the range [0, 1] (default: 0.5f)
+     * @see #setJoysticksTriggerToButtonThreshold(float)
+     */
+    public float getJoysticksTriggerToButtonThreshold() {
+        return getFloat("JoysticksTriggerToButtonThreshold");
+    }
+
+    /**
+     * Sets the jitter threshold for joystick axes.
+     * 
+     * <p>
+     * Axis movements with a delta smaller than this threshold will be ignored. This is intended to reduce
+     * noise from analog joysticks.
+     */
+    public void setJoysticksAxisJitterThreshold(float threshold) {
+        putFloat("JoysticksAxisJitterThreshold", threshold);
+    }
+
+    /**
+     * Gets the jitter threshold for joystick axes.
+     * 
+     * @return the jitter threshold
+     * @see #setJoysticksAxisJitterThreshold(float)
+     */
+    public float getJoysticksAxisJitterThreshold() {
+        return getFloat("JoysticksAxisJitterThreshold");
+    }
+
+    /**
+     * Set resource path for a custom SDL game controller database.
+     * 
+     * @param path
+     */
+    public void setSDLGameControllerDBResourcePath(String path) {
+        putString("SDLGameControllerDBResourcePath", path);
+    }
+
+    /**
+     * Get resource path for a custom SDL game controller database.
+     * 
+     * @return resource path
+     */
+    public String getSDLGameControllerDBResourcePath() {
+        return getString("SDLGameControllerDBResourcePath");
+    }
 }
+

+ 2 - 2
jme3-examples/build.gradle

@@ -19,8 +19,8 @@ dependencies {
     implementation project(':jme3-effects')
     implementation project(':jme3-jbullet')
     implementation project(':jme3-jogg')
-    implementation project(':jme3-lwjgl')
-//    implementation project(':jme3-lwjgl3')
+    // implementation project(':jme3-lwjgl')
+    implementation project(':jme3-lwjgl3')
     implementation project(':jme3-networking')
     implementation project(':jme3-niftygui')
     implementation project(':jme3-plugins')

+ 32 - 25
jme3-examples/src/main/java/jme3test/input/TestJoystick.java

@@ -46,7 +46,9 @@ public class TestJoystick extends SimpleApplication {
     public static void main(String[] args){
         TestJoystick app = new TestJoystick();
         AppSettings settings = new AppSettings(true);
+        settings.setJoysticksMapper(AppSettings.JOYSTICKS_XBOX_MAPPER);
         settings.setUseJoysticks(true);
+        settings.setX11PlatformPreferred(true);
         app.setSettings(settings);
         app.start();
     }
@@ -155,7 +157,7 @@ public class TestJoystick extends SimpleApplication {
             
         } 
     }
- 
+
     /**
      *  Easier to watch for all button and axis events with a raw input listener.
      */   
@@ -184,6 +186,7 @@ public class TestJoystick extends SimpleApplication {
             gamepad.setAxisValue( evt.getAxis(), value );
             if( value != 0 ) {
                 lastValues.put(evt.getAxis(), value);
+                evt.getAxis().getJoystick().rumble(0.5f);
             } 
         }
 
@@ -191,6 +194,7 @@ public class TestJoystick extends SimpleApplication {
         public void onJoyButtonEvent(JoyButtonEvent evt) {
             setViewedJoystick( evt.getButton().getJoystick() );
             gamepad.setButtonValue( evt.getButton(), evt.isPressed() ); 
+            evt.getButton().getJoystick().rumble(1f);
         }
 
         @Override
@@ -255,24 +259,24 @@ public class TestJoystick extends SimpleApplication {
             attachChild(rightStick);
 
             // A "standard" mapping... fits a majority of my game pads
-            addButton( JoystickButton.BUTTON_0, 371, 512 - 176, 42, 42 );
-            addButton( JoystickButton.BUTTON_1, 407, 512 - 212, 42, 42 );
-            addButton( JoystickButton.BUTTON_2, 371, 512 - 248, 42, 42 );
-            addButton( JoystickButton.BUTTON_3, 334, 512 - 212, 42, 42 );
+            addButton( JoystickButton.BUTTON_XBOX_Y, 371, 512 - 176, 42, 42 );
+            addButton( JoystickButton.BUTTON_XBOX_B, 407, 512 - 212, 42, 42 );
+            addButton( JoystickButton.BUTTON_XBOX_A, 371, 512 - 248, 42, 42 );
+            addButton( JoystickButton.BUTTON_XBOX_X, 334, 512 - 212, 42, 42 );
  
             // Front buttons  Some of these have the top ones and the bottoms ones flipped.           
-            addButton( JoystickButton.BUTTON_4, 67, 512 - 111, 95, 21 );
-            addButton( JoystickButton.BUTTON_5, 348, 512 - 111, 95, 21 );
-            addButton( JoystickButton.BUTTON_6, 67, 512 - 89, 95, 21 );
-            addButton( JoystickButton.BUTTON_7, 348, 512 - 89, 95, 21 );
+            addButton( JoystickButton.BUTTON_XBOX_LB, 67, 512 - 111, 95, 21 );
+            addButton( JoystickButton.BUTTON_XBOX_RB, 348, 512 - 111, 95, 21 );
+            addButton( JoystickButton.BUTTON_XBOX_LT, 67, 512 - 89, 95, 21 );
+            addButton( JoystickButton.BUTTON_XBOX_RT, 348, 512 - 89, 95, 21 );
  
             // Select and start buttons           
-            addButton( JoystickButton.BUTTON_8, 206, 512 - 198, 48, 30 );
-            addButton( JoystickButton.BUTTON_9, 262, 512 - 198, 48, 30 );
+            addButton( JoystickButton.BUTTON_XBOX_BACK, 206, 512 - 198, 48, 30 );
+            addButton( JoystickButton.BUTTON_XBOX_START, 262, 512 - 198, 48, 30 );
             
             // Joystick push buttons
-            addButton( JoystickButton.BUTTON_10, 147, 512 - 300, 75, 70 );
-            addButton( JoystickButton.BUTTON_11, 285, 512 - 300, 75, 70 );
+            addButton( JoystickButton.BUTTON_XBOX_L3, 147, 512 - 300, 75, 70 );
+            addButton( JoystickButton.BUTTON_XBOX_R3, 285, 512 - 300, 75, 70 );
 
             // Fake button highlights for the POV axes
             //
@@ -280,10 +284,14 @@ public class TestJoystick extends SimpleApplication {
             //  -X  +X
             //    -Y
             //
-            addButton( "POV +Y", 96, 512 - 174, 40, 38 );
-            addButton( "POV +X", 128, 512 - 208, 40, 38 );
-            addButton( "POV -Y", 96, 512 - 239, 40, 38 );
-            addButton( "POV -X", 65, 512 - 208, 40, 38 );
+            // addButton( "POV +Y", 96, 512 - 174, 40, 38 );
+            // addButton( "POV +X", 128, 512 - 208, 40, 38 );
+            // addButton( "POV -Y", 96, 512 - 239, 40, 38 );
+            // addButton( "POV -X", 65, 512 - 208, 40, 38 );
+            addButton( JoystickButton.BUTTON_XBOX_DPAD_UP, 96, 512 - 174, 40, 38 );
+            addButton( JoystickButton.BUTTON_XBOX_DPAD_RIGHT, 128, 512 - 208, 40, 38 );
+            addButton( JoystickButton.BUTTON_XBOX_DPAD_DOWN, 96, 512 - 239, 40, 38 );
+            addButton( JoystickButton.BUTTON_XBOX_DPAD_LEFT, 65, 512 - 208, 40, 38 );
 
             resetPositions();                                               
         }
@@ -295,22 +303,21 @@ public class TestJoystick extends SimpleApplication {
         }
  
         public void setAxisValue( JoystickAxis axis, float value ) {
-                
-            System.out.println( "Axis:" + axis.getName() + "(id:" + axis.getLogicalId() + ")=" + value );
-            if( axis == axis.getJoystick().getXAxis() ) {
+
+            if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X)){
                 setXAxis(value);
-            } else if( axis == axis.getJoystick().getYAxis() ) {
+            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y)){
                 setYAxis(-value);
-            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.Z_AXIS) ) {
+            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X)) {
                 // Note: in the above condition, we could check the axis name, but
                 //       I have at least one joystick that reports 2 "Z Axis" axes.
                 //       In this particular case, the first one is the right one so
                 //       a name based lookup will find the proper one.  It's a problem
                 //       because the erroneous axis sends a constant stream of values.
                 setZAxis(value);
-            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.Z_ROTATION) ) {
+            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y) ) {
                 setZRotation(-value);
-            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.LEFT_TRIGGER) ) {
+            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_LEFT_TRIGGER) ) {
                 if( axis.getJoystick().getButton(JoystickButton.BUTTON_6) == null ) {
                     // left/right triggers sometimes only show up as axes
                     boolean pressed = value != 0;
@@ -318,7 +325,7 @@ public class TestJoystick extends SimpleApplication {
                         setButtonValue(JoystickButton.BUTTON_6, pressed);
                     }
                 }
-            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.RIGHT_TRIGGER) ) {
+            } else if( axis == axis.getJoystick().getAxis(JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER) ) {
                 if( axis.getJoystick().getButton(JoystickButton.BUTTON_7) == null ) {
                     // left/right triggers sometimes only show up as axes
                     boolean pressed = value != 0;

+ 9 - 0
jme3-lwjgl3/build.gradle

@@ -12,6 +12,7 @@ dependencies {
     api libs.lwjgl3.openal
     api libs.lwjgl3.opencl
     api libs.lwjgl3.opengl
+    api libs.lwjgl3.sdl
 
     runtimeOnly(variantOf(libs.lwjgl3.base){ classifier('natives-windows') })
     runtimeOnly(variantOf(libs.lwjgl3.base){ classifier('natives-windows-x86') })
@@ -52,6 +53,14 @@ dependencies {
     runtimeOnly(variantOf(libs.lwjgl3.openal){ classifier('natives-linux-arm64') })
     runtimeOnly(variantOf(libs.lwjgl3.openal){ classifier('natives-macos') })
     runtimeOnly(variantOf(libs.lwjgl3.openal){ classifier('natives-macos-arm64') })
+
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-windows') })
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-windows-x86') })
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-linux') })
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-linux-arm32') })
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-linux-arm64') })
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-macos') })
+    runtimeOnly(variantOf(libs.lwjgl3.sdl){ classifier('natives-macos-arm64') })
 }
 
 javadoc {

+ 334 - 74
jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java

@@ -34,30 +34,65 @@ package com.jme3.input.lwjgl;
 import com.jme3.input.*;
 import com.jme3.input.event.JoyAxisEvent;
 import com.jme3.input.event.JoyButtonEvent;
+import com.jme3.math.FastMath;
+
 import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.logging.Level;
 import java.util.logging.Logger;
+import com.jme3.system.AppSettings;
+import org.lwjgl.glfw.GLFWGamepadState;
 import static org.lwjgl.glfw.GLFW.*;
 
 /**
  * The LWJGL implementation of {@link JoyInput}.
  *
- * @author Daniel Johansson (dannyjo)
+ * @author Daniel Johansson (dannyjo), Riccardo Balbo
  * @since 3.1
  */
 public class GlfwJoystickInput implements JoyInput {
-
     private static final Logger LOGGER = Logger.getLogger(GlfwJoystickInput.class.getName());
+    private static final int POV_X_AXIS_ID = 7;
+    private static final int POV_Y_AXIS_ID = 8;
 
-    private RawInputListener listener;
-
+    private final AppSettings settings;
     private final Map<Integer, GlfwJoystick> joysticks = new HashMap<>();
-
     private final Map<JoystickButton, Boolean> joyButtonPressed = new HashMap<>();
+    private final Map<JoystickAxis, Float> joyAxisValues = new HashMap<>();
 
     private boolean initialized = false;
+    private float virtualTriggerThreshold;
+    private boolean xboxStyle;
+    private float globalJitterThreshold = 0f;
+    private GLFWGamepadState gamepadState;
+    private RawInputListener listener;
+
+    public GlfwJoystickInput(AppSettings settings) {
+        this.settings = settings;
+        try {
+            String path = settings.getSDLGameControllerDBResourcePath();
+            if (path != null && !path.trim().isEmpty()) {
+                ByteBuffer bbf = SdlGameControllerDb.getGamecontrollerDb(path);
+                if (!glfwUpdateGamepadMappings(bbf)) throw new Exception("Failed to load");
+            }
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Unable to load gamecontrollerdb, fallback to glfw default mappings",
+                    e);
+        }
+    }
+
+    @Override
+    public void initialize() {
+        gamepadState = GLFWGamepadState.create();
+
+        virtualTriggerThreshold = settings.getJoysticksTriggerToButtonThreshold();
+        xboxStyle = settings.getJoysticksMapper().equals(AppSettings.JOYSTICKS_XBOX_LEGACY_MAPPER);
+        globalJitterThreshold = settings.getJoysticksAxisJitterThreshold();
+
+        initialized = true;
+    }
 
     @Override
     public void setJoyRumble(final int joyId, final float amount) {
@@ -78,9 +113,9 @@ public class GlfwJoystickInput implements JoyInput {
 
     public void reloadJoysticks() {
         joysticks.clear();
-
+        joyButtonPressed.clear();
+        joyAxisValues.clear();
         InputManager inputManager = (InputManager) listener;
-
         Joystick[] joysticks = loadJoysticks(inputManager);
         inputManager.setJoysticks(joysticks);
     }
@@ -90,43 +125,95 @@ public class GlfwJoystickInput implements JoyInput {
 
         for (int i = 0; i < GLFW_JOYSTICK_LAST; i++) {
             if (glfwJoystickPresent(i)) {
-                final String name = glfwGetJoystickName(i);
-                final GlfwJoystick joystick = new GlfwJoystick(inputManager, this, i, name);
-                joysticks.put(i, joystick);
-
-                final FloatBuffer floatBuffer = glfwGetJoystickAxes(i);
-
-                int axisIndex = 0;
-                while (floatBuffer.hasRemaining()) {
-                    floatBuffer.get();
-
-                    final String logicalId = JoystickCompatibilityMappings.remapAxis(joystick.getName(), convertAxisIndex(axisIndex));
-                    final JoystickAxis joystickAxis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, convertAxisIndex(axisIndex), logicalId, true, false, 0.0f);
-                    joystick.addAxis(axisIndex, joystickAxis);
-                    axisIndex++;
+                boolean isGlfwGamepad = xboxStyle && glfwJoystickIsGamepad(i);
+
+                String name;
+                if (isGlfwGamepad) {
+                    name = glfwGetGamepadName(i);
+                } else {
+                    name = glfwGetJoystickName(i);
+                    LOGGER.log(Level.WARNING,
+                            "Unknown controller detected: {0} - guid: {1}. Fallback to raw input handling",
+                            new Object[] { name, glfwGetJoystickGUID(i) });
                 }
 
-                final ByteBuffer byteBuffer = glfwGetJoystickButtons(i);
+                GlfwJoystick joystick = new GlfwJoystick(inputManager, this, i, name, isGlfwGamepad);
+                joysticks.put(i, joystick);
 
-                if (byteBuffer != null) {
-                    int buttonIndex = 0;
-                    while (byteBuffer.hasRemaining()) {
-                        byteBuffer.get();
+                if(!isGlfwGamepad){
+                    // RAW axis
+                    FloatBuffer floatBuffer = glfwGetJoystickAxes(i);
+                    if (floatBuffer == null) continue;
+
+                    int axisIndex = 0;
+                    while (floatBuffer.hasRemaining()) {
+                        floatBuffer.get();
+
+                        String logicalId = JoystickCompatibilityMappings.remapAxis(joystick.getName(),
+                                convertAxisIndex(axisIndex));
+                        JoystickAxis joystickAxis = new DefaultJoystickAxis(inputManager, joystick, axisIndex,
+                                convertAxisIndex(axisIndex), logicalId, true, false, 0.0f);
+                        joystick.addAxis(axisIndex, joystickAxis);
+                        axisIndex++;
+                    }
 
-                        final String logicalId = JoystickCompatibilityMappings.remapButton(joystick.getName(), String.valueOf(buttonIndex));
-                        final JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex, String.valueOf(buttonIndex), logicalId);
+                    // raw buttons
+                    ByteBuffer byteBuffer = glfwGetJoystickButtons(i);
+
+                    if (byteBuffer != null) {
+                        int buttonIndex = 0;
+                        while (byteBuffer.hasRemaining()) {
+                            byteBuffer.get();
+
+                            String logicalId = JoystickCompatibilityMappings.remapButton(joystick.getName(),
+                                    String.valueOf(buttonIndex));
+                            JoystickButton button = new DefaultJoystickButton(inputManager, joystick,
+                                    buttonIndex, String.valueOf(buttonIndex), logicalId);
+                            joystick.addButton(button);
+                            joyButtonPressed.put(button, false);
+                            buttonIndex++;
+                        }
+                    }
+                } else {
+                    // Managed axis
+                    for (int axisIndex = 0; axisIndex <= GLFW_GAMEPAD_AXIS_LAST; axisIndex++) {
+                        String logicalId = remapAxisToJme(axisIndex);
+                        if (logicalId == null) continue;
+                        String axisName = logicalId; // no need to remap with JoystickCompatibilityMappings as
+                                                     // glfw already handles remapping
+                        JoystickAxis axis = new DefaultJoystickAxis(inputManager, joystick, axisIndex,
+                                axisName, logicalId, true, false, 0.0f);
+                        joystick.addAxis(axisIndex, axis);
+                    }
+
+                    // Virtual POV axes for D-pad.
+                    JoystickAxis povX = new DefaultJoystickAxis(inputManager, joystick, POV_X_AXIS_ID,
+                            JoystickAxis.POV_X, JoystickAxis.POV_X, true, false, 0.0f);
+                    joystick.addAxis(POV_X_AXIS_ID, povX);
+
+                    JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, POV_Y_AXIS_ID,
+                            JoystickAxis.POV_Y, JoystickAxis.POV_Y, true, false, 0.0f);
+                    joystick.addAxis(POV_Y_AXIS_ID, povY);
+
+                    // managed buttons
+                    for (int buttonIndex = 0; buttonIndex <= GLFW_GAMEPAD_BUTTON_LAST; buttonIndex++) {
+                        String logicalId = remapButtonToJme(buttonIndex);
+                        if (logicalId == null) continue;
+                        String buttonName = logicalId;
+                        JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex,
+                                buttonName, logicalId);
                         joystick.addButton(button);
                         joyButtonPressed.put(button, false);
-                        buttonIndex++;
                     }
                 }
+
             }
         }
 
         return joysticks.values().toArray(new GlfwJoystick[joysticks.size()]);
     }
-
-    private String convertAxisIndex(final int index) {
+ 
+    private String convertAxisIndex(int index) {
         if (index == 0) {
             return "pov_x";
         } else if (index == 1) {
@@ -136,52 +223,194 @@ public class GlfwJoystickInput implements JoyInput {
         } else if (index == 3) {
             return "rz";
         }
-
         return String.valueOf(index);
     }
 
-    @Override
-    public void initialize() {
-        initialized = true;
-    }
-
     @Override
     public void update() {
         float rawValue, value;
-        for (final Map.Entry<Integer, GlfwJoystick> entry : joysticks.entrySet()) {
+        for (Map.Entry<Integer, GlfwJoystick> entry : joysticks.entrySet()) {
+            if (!glfwJoystickPresent(entry.getKey())) continue;
+            if (!entry.getValue().isGlfwGamepad()) {
+
+                // Axes
+                FloatBuffer axisValues = glfwGetJoystickAxes(entry.getKey());
+
+                // if a joystick is added or removed, the callback reloads the joysticks.
+                // when the callback is called and reloads the joystick, this iterator may already have started iterating.
+                // To avoid a NullPointerException we null-check the axisValues and bytebuffer objects.
+                // If the joystick it's iterating over no-longer exists it will return null.
+
+                if (axisValues != null) {
+                    for (JoystickAxis axis : entry.getValue().getAxes()) {
+                        rawValue = axisValues.get(axis.getAxisId());
+                        value = JoystickCompatibilityMappings.remapAxisRange(axis, rawValue);
+                        // listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue));
+                        updateAxis(axis, value, rawValue);
+                    }
+                }
+
+                // Buttons
+                ByteBuffer byteBuffer = glfwGetJoystickButtons(entry.getKey());
 
-            // Axes
-            final FloatBuffer axisValues = glfwGetJoystickAxes(entry.getKey());
+                if (byteBuffer != null) {
+                    for (JoystickButton button : entry.getValue().getButtons()) {
+                        boolean pressed = byteBuffer.get(button.getButtonId()) == GLFW_PRESS;
+                        updateButton(button, pressed);
+                    }
+                }
+            } else {
+                if (!glfwGetGamepadState(entry.getKey(), gamepadState)) return;
+                Joystick joystick = entry.getValue();
 
-            // if a joystick is added or removed, the callback reloads the joysticks.
-            // when the callback is called and reloads the joystick, this iterator may already have started iterating.
-            // To avoid a NullPointerException we null-check the axisValues and bytebuffer objects.
-            // If the joystick it's iterating over no-longer exists it will return null.
+                FloatBuffer axes = gamepadState.axes();
 
-            if (axisValues != null) {
-                for (final JoystickAxis axis : entry.getValue().getAxes()) {
-                    rawValue = axisValues.get(axis.getAxisId());
-                    value = JoystickCompatibilityMappings.remapAxisRange(axis, rawValue);
-                    listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue));
+                // handle axes (skip virtual POV axes)
+                for (JoystickAxis axis : entry.getValue().getAxes()) {
+                    int axisId = axis.getAxisId();
+                    if (axisId == POV_X_AXIS_ID || axisId == POV_Y_AXIS_ID) continue;
+                    if (axisId < 0 || axisId > GLFW_GAMEPAD_AXIS_LAST) continue;
+
+                    rawValue = axes.get(axisId);
+                    rawValue = remapAxisValueToJme(axisId, rawValue);
+                    value = rawValue; // scaling handled by GLFW
+
+                    updateAxis(axis, value, rawValue);
                 }
-            }
 
-            // Buttons
-            final ByteBuffer byteBuffer = glfwGetJoystickButtons(entry.getKey());
+                // virtual trigger buttons
+                if (virtualTriggerThreshold > 0.0f) {
+                    float leftTrigger = remapAxisValueToJme(GLFW_GAMEPAD_AXIS_LEFT_TRIGGER,
+                            axes.get(GLFW_GAMEPAD_AXIS_LEFT_TRIGGER));
+                    float rightTrigger = remapAxisValueToJme(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER,
+                            axes.get(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER));
+                    updateButton(joystick.getButton(JoystickButton.BUTTON_XBOX_LT),
+                            leftTrigger > virtualTriggerThreshold);
+                    updateButton(joystick.getButton(JoystickButton.BUTTON_XBOX_RT),
+                            rightTrigger > virtualTriggerThreshold);
+                }
 
-            if (byteBuffer != null) {
-                for (final JoystickButton button : entry.getValue().getButtons()) {
-                    final boolean pressed = byteBuffer.get(button.getButtonId()) == GLFW_PRESS;
+                ByteBuffer buttons = gamepadState.buttons();
+             
+                for (int btnIndex = 0; btnIndex <= GLFW_GAMEPAD_BUTTON_LAST; btnIndex++) {
+                    String jmeButtonIndex = remapButtonToJme(btnIndex);
+                    if (jmeButtonIndex == null) continue;
 
-                    if (joyButtonPressed.get(button) != pressed) {
-                        joyButtonPressed.put(button, pressed);
-                        listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed));
-                    }
+                    JoystickButton button = joystick.getButton(jmeButtonIndex);
+                    if (button == null) continue;
+
+                    boolean pressed = buttons.get(btnIndex) == GLFW_PRESS;
+                    updateButton(button, pressed);
+                }
+
+                // D-pad to virtual POV axes
+                boolean dpadUp = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_UP) == GLFW_PRESS;
+                boolean dpadDown = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_DOWN) == GLFW_PRESS;
+                boolean dpadLeft = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_LEFT) == GLFW_PRESS;
+                boolean dpadRight = buttons.get(GLFW_GAMEPAD_BUTTON_DPAD_RIGHT) == GLFW_PRESS;
+
+                float povX = dpadLeft ? -1f : (dpadRight ? 1f : 0f);
+                float povY = dpadDown ? -1f : (dpadUp ? 1f : 0f);
+
+                JoystickAxis povXAxis = joystick.getPovXAxis();
+                if (povXAxis != null) {
+                    updateAxis(povXAxis, povX, povX);
+                }
+
+                JoystickAxis povYAxis = joystick.getPovYAxis();
+                if (povYAxis != null) {
+                    updateAxis(povYAxis, povY, povY);
                 }
             }
         }
     }
 
+
+ 
+    private String remapAxisToJme(int glfwAxisIndex) {
+        switch (glfwAxisIndex) {
+            case GLFW_GAMEPAD_AXIS_LEFT_X:
+                return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X;
+            case GLFW_GAMEPAD_AXIS_LEFT_Y:
+                return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y;
+            case GLFW_GAMEPAD_AXIS_RIGHT_X:
+                return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X;
+            case GLFW_GAMEPAD_AXIS_RIGHT_Y:
+                return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y;
+            case GLFW_GAMEPAD_AXIS_LEFT_TRIGGER:
+                return JoystickAxis.AXIS_XBOX_LEFT_TRIGGER;
+            case GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER:
+                return JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER;
+            default:
+                return null;
+
+        }
+    }
+
+    private String remapButtonToJme(int glfwButtonIndex) {
+        switch (glfwButtonIndex) {
+            case GLFW_GAMEPAD_BUTTON_Y:
+                return JoystickButton.BUTTON_XBOX_Y;
+            case GLFW_GAMEPAD_BUTTON_B:
+                return JoystickButton.BUTTON_XBOX_B;
+            case GLFW_GAMEPAD_BUTTON_A:
+                return JoystickButton.BUTTON_XBOX_A;
+            case GLFW_GAMEPAD_BUTTON_X:
+                return JoystickButton.BUTTON_XBOX_X;
+            case GLFW_GAMEPAD_BUTTON_LEFT_BUMPER:
+                return JoystickButton.BUTTON_XBOX_LB;   
+            case GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER:
+                return JoystickButton.BUTTON_XBOX_RB;
+            case GLFW_GAMEPAD_BUTTON_BACK:
+                return JoystickButton.BUTTON_XBOX_BACK;
+            case GLFW_GAMEPAD_BUTTON_START:
+                return JoystickButton.BUTTON_XBOX_START;
+            case GLFW_GAMEPAD_BUTTON_LEFT_THUMB:
+                return JoystickButton.BUTTON_XBOX_L3;
+            case GLFW_GAMEPAD_BUTTON_RIGHT_THUMB:
+                return JoystickButton.BUTTON_XBOX_R3;
+            case GLFW_GAMEPAD_BUTTON_DPAD_UP:
+                return JoystickButton.BUTTON_XBOX_DPAD_UP;
+            case GLFW_GAMEPAD_BUTTON_DPAD_DOWN:
+                return JoystickButton.BUTTON_XBOX_DPAD_DOWN;
+            case GLFW_GAMEPAD_BUTTON_DPAD_LEFT:
+                return JoystickButton.BUTTON_XBOX_DPAD_LEFT;
+            case GLFW_GAMEPAD_BUTTON_DPAD_RIGHT:
+                return JoystickButton.BUTTON_XBOX_DPAD_RIGHT;
+            default:
+                return null;
+        
+        }
+    }
+
+    private static float remapAxisValueToJme(int axisId, float v) {
+        if (axisId == GLFW_GAMEPAD_AXIS_LEFT_TRIGGER || axisId == GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) {
+            if (v < -1f) v = -1f;
+            if (v > 1f) v = 1f;
+            return (v + 1f) * 0.5f;
+        }
+        return v;
+    }
+
+    private void updateButton(JoystickButton button, boolean pressed) {
+        if (button == null) return;
+        Boolean old = joyButtonPressed.get(button);
+        if (old == null || old != pressed) {
+            joyButtonPressed.put(button, pressed);
+            listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed));
+        }
+    }
+
+    private void updateAxis(JoystickAxis axis, float value, float rawValue) {
+        if (axis == null) return;
+        Float old = joyAxisValues.get(axis);
+        float jitter = FastMath.clamp(Math.max(axis.getJitterThreshold(), globalJitterThreshold), 0f, 1f);
+        if (old == null || FastMath.abs(old - value) > jitter) {
+            joyAxisValues.put(axis, value);
+            listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue));
+        }
+    }
+
     @Override
     public void destroy() {
         initialized = false;
@@ -199,41 +428,71 @@ public class GlfwJoystickInput implements JoyInput {
 
     @Override
     public long getInputTimeNanos() {
-        return 0;
+        return (long) (glfwGetTime() * 1000000000);
     }
 
-    protected class GlfwJoystick extends AbstractJoystick {
-
+    private static class GlfwJoystick extends AbstractJoystick {
+        private final boolean isGlfwGamepad;
+        private JoystickAxis xAxis;
+        private JoystickAxis yAxis;
         private JoystickAxis povAxisX;
         private JoystickAxis povAxisY;
 
-        public GlfwJoystick(final InputManager inputManager, final JoyInput joyInput, final int joyId, final String name) {
+        public GlfwJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name,
+                boolean gamepad) {
             super(inputManager, joyInput, joyId, name);
+            this.isGlfwGamepad = gamepad;
         }
 
-        public void addAxis(final int index, final JoystickAxis axis) {
-            super.addAxis(axis);
+        public boolean isGlfwGamepad() {
+            return isGlfwGamepad;
+        }
 
-            if (index == 0) {
-                povAxisX = axis;
-            } else if (index == 1) {
-                povAxisY = axis;
+        public void addAxis(int index, JoystickAxis axis) {
+            super.addAxis(axis);
+            if (isGlfwGamepad) {
+                switch (index) {
+                    case GLFW_GAMEPAD_AXIS_LEFT_X: {
+                        xAxis = axis;
+                        break;
+                    }
+                    case GLFW_GAMEPAD_AXIS_LEFT_Y: {
+                        yAxis = axis;
+                        break;
+                    }
+                    case POV_X_AXIS_ID: {
+                        povAxisX = axis;
+                        break;
+                    }
+                    case POV_Y_AXIS_ID: {
+                        povAxisY = axis;
+                        break;
+                    }
+                }
+            } else {
+                if (index == 0) {
+                    xAxis = axis;
+                    povAxisX = axis;
+                } else if (index == 1) {
+                    yAxis = axis;
+                    povAxisY = axis;
+                }
             }
         }
 
         @Override
-        protected void addButton(final JoystickButton button) {
+        protected void addButton(JoystickButton button) {
             super.addButton(button);
         }
 
         @Override
         public JoystickAxis getXAxis() {
-            return povAxisX;
+            return xAxis;
         }
 
         @Override
         public JoystickAxis getYAxis() {
-            return povAxisY;
+            return yAxis;
         }
 
         @Override
@@ -248,12 +507,13 @@ public class GlfwJoystickInput implements JoyInput {
 
         @Override
         public int getXAxisIndex() {
-            return povAxisX.getAxisId();
+            return xAxis != null ? xAxis.getAxisId() : 0;
         }
 
         @Override
         public int getYAxisIndex() {
-            return povAxisY.getAxisId();
+            return yAxis != null ? yAxis.getAxisId() : 1;
         }
     }
+
 }

+ 39 - 0
jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlGameControllerDb.java

@@ -0,0 +1,39 @@
+package com.jme3.input.lwjgl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.util.BufferUtils;
+import com.jme3.util.res.Resources;
+
+public class SdlGameControllerDb {
+    private static final Logger LOGGER = Logger.getLogger(SdlGameControllerDb.class.getName());
+
+    public static ByteBuffer getGamecontrollerDb(String path) throws Exception {
+        try ( InputStream gamecontrollerdbIs = Resources.getResourceAsStream(path)) {
+            if(gamecontrollerdbIs == null) throw new Exception("Resource not found");
+
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            
+            byte data[] = new byte[4096];
+            int read;
+            while ((read = gamecontrollerdbIs.read(data)) != -1) {
+                bos.write(data, 0, read);
+            }
+            data = bos.toByteArray();
+            
+            ByteBuffer gamecontrollerdb = BufferUtils.createByteBuffer(data.length + 1);
+            gamecontrollerdb.put(data);
+            gamecontrollerdb.put((byte)0); // null-terminate
+            gamecontrollerdb.flip();
+            LOGGER.log(Level.INFO, "Loaded gamecontrollerdb from {0}", path);
+            return gamecontrollerdb;
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Unable to load "+path+" ", e);
+            throw e;
+        }
+    }
+}

+ 621 - 0
jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java

@@ -0,0 +1,621 @@
+package com.jme3.input.lwjgl;
+
+import com.jme3.input.*;
+import com.jme3.input.event.JoyAxisEvent;
+import com.jme3.input.event.JoyButtonEvent;
+import com.jme3.math.FastMath;
+import com.jme3.system.AppSettings;
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.lwjgl.sdl.*;
+import org.lwjgl.system.MemoryStack;
+
+import static org.lwjgl.sdl.SDLInit.*;
+import static org.lwjgl.sdl.SDLEvents.*;
+import static org.lwjgl.sdl.SDLGamepad.*;
+import static org.lwjgl.sdl.SDLJoystick.*;
+import static org.lwjgl.sdl.SDLError.*;
+import static org.lwjgl.sdl.SDLTimer.*;
+
+/**
+ * The SDL based implementation of {@link JoyInput}.
+ *
+ * @author Riccardo Balbo
+ */
+public class SdlJoystickInput implements JoyInput {
+
+    private static final Logger LOGGER = Logger.getLogger(SdlJoystickInput.class.getName());
+    private static final int POV_X_AXIS_ID = 7;
+    private static final int POV_Y_AXIS_ID = 8;
+
+    private final AppSettings settings;
+    private final Map<Integer, SdlJoystick> joysticks = new HashMap<>();
+    private final Map<JoystickButton, Boolean> joyButtonPressed = new HashMap<>();
+    private final Map<JoystickAxis, Float> joyAxisValues = new HashMap<>();
+    private final int flags = SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_EVENTS;
+
+    private boolean initialized;
+    private float virtualTriggerThreshold;
+    private float globalJitterThreshold;
+    private boolean loadGamepads;
+    private boolean loadRaw;
+
+    private RawInputListener listener;
+
+    public SdlJoystickInput(AppSettings settings) {
+        this.settings = settings;
+        try {
+            String path = settings.getSDLGameControllerDBResourcePath();
+            if (path != null && !path.trim().isEmpty()) {
+                ByteBuffer bbf = SdlGameControllerDb.getGamecontrollerDb(path);
+                if (SDL_AddGamepadMapping(bbf) == -1) {
+                    throw new Exception("Failed to load");
+                }
+            }
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Unable to load gamecontrollerdb, fallback to sdl default mappings", e);
+        }
+    }
+
+    @Override
+    public void initialize() {
+        if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_EVENTS)) {
+            String err = SDL_GetError();
+            throw new IllegalStateException("SDL_InitSubSystem failed: " + err);
+        }
+        virtualTriggerThreshold = settings.getJoysticksTriggerToButtonThreshold();
+        globalJitterThreshold = settings.getJoysticksAxisJitterThreshold();
+
+        String mapper = settings.getJoysticksMapper();
+        switch (mapper) {
+            case AppSettings.JOYSTICKS_RAW_MAPPER:
+                loadGamepads = false;
+                loadRaw = true;
+                break;
+            case AppSettings.JOYSTICKS_XBOX_MAPPER:
+                loadGamepads = true;
+                loadRaw = false;
+                break;
+            default:
+            case AppSettings.JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER:
+                loadGamepads = true;
+                loadRaw = true;
+                break;
+        }
+
+        initialized = true;
+    }
+
+    private void onDeviceConnected(int deviceIndex, boolean isGamepad) {
+        if (loadGamepads && !isGamepad && SDL_IsGamepad(deviceIndex)) {
+            // SDL will fire both GAMEPAD and JOYSTICK events for recognized
+            // gamepads, so here we check if the joystick is expected to be
+            // a gamepad, and skip it if so to avoid duplicates.
+            return;
+        }
+
+        InputManager inputManager = (InputManager) listener;
+
+        SdlJoystick joystick;
+        if (isGamepad) {
+            long gamepad = SDL_OpenGamepad(deviceIndex);
+            if (gamepad == 0L) {
+                LOGGER.log(Level.FINE, "SDL failed to open gamepad for id {0}: {1}",
+                        new Object[] { deviceIndex, SDL_GetError() });
+                return;
+            }
+
+            String name = SDL_GetGamepadName(gamepad);
+
+            joystick = new SdlJoystick(inputManager, this, deviceIndex, name, gamepad, 0L);
+            joysticks.put(deviceIndex, joystick);
+
+            // Managed axes (standard layout)
+            for (int axisIndex = 0; axisIndex < SDL_GAMEPAD_AXIS_COUNT; axisIndex++) {
+                String logicalId = remapAxisToJme(axisIndex);
+                if (logicalId == null) continue;
+
+                String axisName = getAxisLabel(joystick, axisIndex);
+
+                JoystickAxis axis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, axisName,
+                        logicalId, true, false, 0.0f);
+                joystick.addAxis(axisIndex, axis);
+            }
+
+            // Managed buttons: map SDL gamepad buttons into your JME logical ids
+            for (int buttonIndex = 0; buttonIndex < SDL_GAMEPAD_BUTTON_COUNT; buttonIndex++) {
+                String logicalId = remapButtonToJme(buttonIndex);
+                if (logicalId == null) continue;
+                String buttonName = getButtonLabel(joystick, buttonIndex);
+
+                JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex,
+                        buttonName, logicalId);
+                joystick.addButton(button);
+                joyButtonPressed.put(button, false);
+            }
+
+        } else {
+            long joy = SDL_OpenJoystick(deviceIndex);
+            if (joy == 0L) return;
+
+            String name = SDL_GetJoystickName(joy);
+            joystick = new SdlJoystick(inputManager, this, deviceIndex, name, 0L, joy);
+            joysticks.put(deviceIndex, joystick);
+
+            int numAxes = SDL_GetNumJoystickAxes(joy);
+            int numButtons = SDL_GetNumJoystickButtons(joy);
+
+            for (int axisIndex = 0; axisIndex < numAxes; axisIndex++) {
+                String logicalId = String.valueOf(axisIndex);
+                String axisName = logicalId;
+
+                JoystickAxis axis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, axisName,
+                        logicalId, true, false, 0.0f);
+
+                joystick.addAxis(axisIndex, axis);
+            }
+
+            for (int buttonIndex = 0; buttonIndex < numButtons; buttonIndex++) {
+                String logicalId = String.valueOf(buttonIndex);
+                String buttonName = logicalId;
+                JoystickButton button = new DefaultJoystickButton(inputManager, joystick, buttonIndex,
+                        buttonName, logicalId);
+                joystick.addButton(button);
+            }
+        }
+
+        // Virtual POV axes for D-pad.
+        JoystickAxis povX = new DefaultJoystickAxis(inputManager, joystick, POV_X_AXIS_ID, JoystickAxis.POV_X,
+                JoystickAxis.POV_X, true, false, 0.0f);
+        joystick.addAxis(POV_X_AXIS_ID, povX);
+
+        JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, POV_Y_AXIS_ID, JoystickAxis.POV_Y,
+                JoystickAxis.POV_Y, true, false, 0.0f);
+        joystick.addAxis(POV_Y_AXIS_ID, povY);
+
+        ((InputManager) listener).fireJoystickConnectedEvent(joystick);
+
+    }
+
+    private void destroyJoystick(SdlJoystick joystick) {
+        if (joystick.isGamepad()) {
+            if (joystick.gamepad != 0L) {
+                SDL_CloseGamepad(joystick.gamepad);
+            }
+        } else {
+            if (joystick.joystick != 0L) {
+                SDL_CloseJoystick(joystick.joystick);
+            }
+        }
+    }
+
+    private void onDeviceDisconnected(int deviceIndex) {
+        SdlJoystick joystick = joysticks.get(deviceIndex);
+        if (joystick == null) return;
+
+        // clear all states associated with this joystick
+        joyButtonPressed.entrySet().removeIf(e -> e.getKey().getJoystick() == joystick);
+        joyAxisValues.entrySet().removeIf(e -> e.getKey().getJoystick() == joystick);
+        joysticks.remove(deviceIndex);
+
+        // free resources
+        destroyJoystick(joystick);
+
+        ((InputManager) listener).fireJoystickDisconnectedEvent(joystick);
+    }
+
+    @Override
+    public Joystick[] loadJoysticks(InputManager inputManager) {
+
+        for (SdlJoystick js : joysticks.values()) destroyJoystick(js);
+        joysticks.clear();
+
+        joyButtonPressed.clear();
+        joyAxisValues.clear();
+
+        if (loadGamepads) {
+            // load managed gamepads
+            IntBuffer gamepads = SDL_GetGamepads();
+            if (gamepads != null) {
+                while (gamepads.hasRemaining()) {
+                    int deviceId = gamepads.get();
+                    onDeviceConnected(deviceId, true);
+                }
+            }
+        }
+
+        if (loadRaw) {
+            // load raw gamepads
+            IntBuffer joys = SDL_GetJoysticks();
+            if (joys != null) {
+                while (joys.hasRemaining()) {
+                    int deviceId = joys.get();
+                    onDeviceConnected(deviceId, false);
+                }
+            }
+        }
+
+        return joysticks.values().toArray(new Joystick[0]);
+    }
+
+    @Override
+    public void update() {
+        handleConnectionEvents();
+        handleInputEvents();
+    }
+
+    private void handleInputEvents() {
+        float rawValue, value;
+        for (SdlJoystick js : joysticks.values()) {
+            if (js.isGamepad()) {
+                long gp = js.gamepad;
+
+                // for(int axisIndex=0; axisIndex<SDL_GAMEPAD_AXIS_COUNT; axisIndex++){
+                for (JoystickAxis axis : js.getAxes()) {
+                    int axisIndex = axis.getAxisId();
+                    String jmeAxisId = axis.getLogicalId();
+
+                    short v = SDL_GetGamepadAxis(gp, axisIndex);
+
+                    rawValue = remapAxisValueToJme(axisIndex, v);
+                    value = rawValue; // SDL handles scaling
+                    updateAxis(axis, value, rawValue);
+
+                    // Virtual trigger buttons (same idea as your GLFW code)
+                    if (virtualTriggerThreshold > 0f) {
+                        if (jmeAxisId == JoystickAxis.AXIS_XBOX_LEFT_TRIGGER) {
+                            updateButton(js.getButton(JoystickButton.BUTTON_XBOX_LT),
+                                    value > virtualTriggerThreshold);
+                        } else if (jmeAxisId == JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER) {
+                            updateButton(js.getButton(JoystickButton.BUTTON_XBOX_RT),
+                                    value > virtualTriggerThreshold);
+                        }
+                    }
+
+                    // Dpad -> virtual POV axes
+                    float povXValue = 0f;
+                    float povYValue = 0f;
+
+                    // button handling
+                    // for (int b = 0; b <= SDL_GAMEPAD_BUTTON_COUNT; b++) {
+                    for (JoystickButton button : js.getButtons()) {
+                        int b = button.getButtonId();
+                        String jmeButtonId = button.getLogicalId();
+
+                        boolean pressed = SDL_GetGamepadButton(gp, b);
+                        updateButton(button, pressed);
+
+                        // Dpad -> virtual POV axes
+                        if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_UP) {
+                            povYValue += pressed ? 1f : 0f;
+                        } else if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_DOWN) {
+                            povYValue += pressed ? -1f : 0f;
+                        } else if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_LEFT) {
+                            povXValue += pressed ? -1f : 0f;
+                        } else if (jmeButtonId == JoystickButton.BUTTON_XBOX_DPAD_RIGHT) {
+                            povXValue += pressed ? 1f : 0f;
+                        }
+                    }
+
+                    JoystickAxis povXAxis = js.getPovXAxis();
+                    if (povXAxis != null) {
+                        updateAxis(povXAxis, povXValue, povXValue);
+                    }
+
+                    JoystickAxis povYAxis = js.getPovYAxis();
+                    if (povYAxis != null) {
+                        updateAxis(povYAxis, povYValue, povYValue);
+                    }
+                }
+            } else {
+                long joy = js.joystick;
+
+                for (JoystickAxis axis : js.getAxes()) {
+                    short v = SDL_GetJoystickAxis(joy, axis.getAxisId());
+                    rawValue = v;
+                    value = v;
+                    updateAxis(axis, value, rawValue);
+                }
+
+                for (JoystickButton button : js.getButtons()) {
+                    boolean pressed = SDL_GetJoystickButton(joy, button.getButtonId());
+                    updateButton(button, pressed);
+                }
+            }
+        }
+    }
+
+    private void handleConnectionEvents() {
+        try (MemoryStack stack = MemoryStack.stackPush()) {
+            SDL_Event evt = SDL_Event.malloc(stack);
+            while (SDL_PollEvent(evt)) {
+                int type = evt.type();
+                if (type == SDL_EVENT_GAMEPAD_ADDED) {
+                    if (loadGamepads) {
+                        int which = evt.gdevice().which();
+                        onDeviceConnected(which, true);
+                    }
+                } else if (type == SDL_EVENT_GAMEPAD_REMOVED) {
+                    int which = evt.gdevice().which();
+                    onDeviceDisconnected(which);
+                } else if (type == SDL_EVENT_JOYSTICK_ADDED) {
+                    if (loadRaw) {
+                        int which = evt.jdevice().which();
+                        onDeviceConnected(which, false);
+                    }
+                } else if (type == SDL_EVENT_JOYSTICK_REMOVED) {
+                    int which = evt.jdevice().which();
+                    onDeviceDisconnected(which);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setJoyRumble(int joyId, float amount) {
+        setJoyRumble(joyId, amount, amount, 100f / 1000f);
+    }
+
+    public void setJoyRumble(int joyId, float highFrequency, float lowFrequency, float duration) {
+        SdlJoystick js = joysticks.get(joyId);
+        if (js == null) return;
+
+        highFrequency = FastMath.clamp(highFrequency, 0f, 1f);
+        lowFrequency = FastMath.clamp(lowFrequency, 0f, 1f);
+
+        if (js.isGamepad() && js.gamepad != 0L) {
+            int ampHigh = (int) (highFrequency * 0xFFFF);
+            int ampLow = (int) (lowFrequency * 0xFFFF);
+            int durationMs = (int) (duration * 1000f);
+            SDL_RumbleGamepad(js.gamepad, (short) ampHigh, (short) ampLow, durationMs);
+        }
+    }
+
+    private String getButtonLabel(SdlJoystick gamepad, int sdlButtonIndex) {
+        int label = SDL_GetGamepadButtonLabel(gamepad.gamepad, sdlButtonIndex);
+        switch (label) {
+            case SDL_GAMEPAD_BUTTON_LABEL_A:
+                return "A";
+            case SDL_GAMEPAD_BUTTON_LABEL_B:
+                return "B";
+            case SDL_GAMEPAD_BUTTON_LABEL_X:
+                return "X";
+            case SDL_GAMEPAD_BUTTON_LABEL_Y:
+                return "Y";
+
+            case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
+                return "CROSS";
+            case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
+                return "CIRCLE";
+            case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
+                return "SQUARE";
+            case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
+                return "TRIANGLE";
+
+            case SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN:
+            default:
+                return "" + sdlButtonIndex;
+        }
+    }
+
+    private String getAxisLabel(SdlJoystick gamepad, int sdlAxisIndex) {
+        switch (sdlAxisIndex) {
+            case SDL_GAMEPAD_AXIS_LEFTX:
+                return "LEFT THUMB STICK (X)";
+            case SDL_GAMEPAD_AXIS_LEFTY:
+                return "LEFT THUMB STICK (Y)";
+            case SDL_GAMEPAD_AXIS_RIGHTX:
+                return "RIGHT THUMB STICK (X)";
+            case SDL_GAMEPAD_AXIS_RIGHTY:
+                return "RIGHT THUMB STICK (Y)";
+            case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
+                return "LEFT TRIGGER";
+            case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
+                return "RIGHT TRIGGER";
+            default:
+                return "" + sdlAxisIndex;
+        }
+    }
+
+    private String remapAxisToJme(int sdlAxisIndex) {
+        switch (sdlAxisIndex) {
+            case SDL_GAMEPAD_AXIS_LEFTX:
+                return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X;
+            case SDL_GAMEPAD_AXIS_LEFTY:
+                return JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y;
+            case SDL_GAMEPAD_AXIS_RIGHTX:
+                return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X;
+            case SDL_GAMEPAD_AXIS_RIGHTY:
+                return JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y;
+            case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
+                return JoystickAxis.AXIS_XBOX_LEFT_TRIGGER;
+            case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
+                return JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER;
+            default:
+                return null;
+        }
+    }
+
+    private String remapButtonToJme(int sdlButtonIndex) {
+        switch (sdlButtonIndex) {
+            case SDL_GAMEPAD_BUTTON_NORTH:
+                return JoystickButton.BUTTON_XBOX_Y;
+            case SDL_GAMEPAD_BUTTON_EAST:
+                return JoystickButton.BUTTON_XBOX_B;
+            case SDL_GAMEPAD_BUTTON_SOUTH:
+                return JoystickButton.BUTTON_XBOX_A;
+            case SDL_GAMEPAD_BUTTON_WEST:
+                return JoystickButton.BUTTON_XBOX_X;
+            case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
+                return JoystickButton.BUTTON_XBOX_LB;
+            case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
+                return JoystickButton.BUTTON_XBOX_RB;
+            case SDL_GAMEPAD_BUTTON_BACK:
+                return JoystickButton.BUTTON_XBOX_BACK;
+            case SDL_GAMEPAD_BUTTON_START:
+                return JoystickButton.BUTTON_XBOX_START;
+            case SDL_GAMEPAD_BUTTON_LEFT_STICK:
+                return JoystickButton.BUTTON_XBOX_L3;
+            case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
+                return JoystickButton.BUTTON_XBOX_R3;
+            case SDL_GAMEPAD_BUTTON_DPAD_UP:
+                return JoystickButton.BUTTON_XBOX_DPAD_UP;
+            case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
+                return JoystickButton.BUTTON_XBOX_DPAD_DOWN;
+            case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
+                return JoystickButton.BUTTON_XBOX_DPAD_LEFT;
+            case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
+                return JoystickButton.BUTTON_XBOX_DPAD_RIGHT;
+            default:
+                return null;
+        }
+    }
+
+    private float remapAxisValueToJme(int axisId, short v) {
+        if (axisId == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || axisId == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
+            // [0..32767] -> [0..1]
+            if (v <= 0) return 0f;
+            return Math.min(1f, v / 32767f);
+        } else {
+            // [-32768..32767] -> [-1..1]
+            if (v == Short.MIN_VALUE) return -1f;
+            return v / 32767f;
+        }
+    }
+
+    private void updateButton(JoystickButton button, boolean pressed) {
+        if (button == null) return;
+        Boolean old = joyButtonPressed.get(button);
+        if (old == null || old != pressed) {
+            joyButtonPressed.put(button, pressed);
+            listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed));
+        }
+    }
+
+    private void updateAxis(JoystickAxis axis, float value, float rawValue) {
+        if (axis == null) return;
+        Float old = joyAxisValues.get(axis);
+        float jitter = FastMath.clamp(Math.max(axis.getJitterThreshold(), globalJitterThreshold), 0f, 1f);
+        if (old == null || FastMath.abs(old - value) > jitter) {
+            joyAxisValues.put(axis, value);
+            listener.onJoyAxisEvent(new JoyAxisEvent(axis, value, rawValue));
+        }
+    }
+
+    @Override
+    public void destroy() {
+        // Close devices
+        for (SdlJoystick js : joysticks.values()) {
+            if (js.gamepad != 0L) SDL_CloseGamepad(js.gamepad);
+            if (js.joystick != 0L) SDL_CloseJoystick(js.joystick);
+        }
+        joysticks.clear();
+
+        // Quit subsystems
+        SDL_QuitSubSystem(flags);
+
+        initialized = false;
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    @Override
+    public void setInputListener(final RawInputListener listener) {
+        this.listener = listener;
+    }
+
+    @Override
+    public long getInputTimeNanos() {
+        return SDL_GetTicksNS();
+    }
+
+    private static class SdlJoystick extends AbstractJoystick {
+
+        private JoystickAxis xAxis;
+        private JoystickAxis yAxis;
+        private JoystickAxis povAxisX;
+        private JoystickAxis povAxisY;
+
+        long gamepad;
+        long joystick;
+
+        SdlJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name, long gamepad,
+                long joystick) {
+            super(inputManager, joyInput, joyId, name);
+            this.gamepad = gamepad;
+            this.joystick = joystick;
+
+        }
+
+        boolean isGamepad() {
+            return gamepad != 0L;
+        }
+
+        void addAxis(int index, JoystickAxis axis) {
+            super.addAxis(axis);
+            switch (index) {
+                case SDL_GAMEPAD_AXIS_LEFTX: {
+                    xAxis = axis;
+                    break;
+                }
+                case SDL_GAMEPAD_AXIS_LEFTY: {
+                    yAxis = axis;
+                    break;
+                }
+                case POV_X_AXIS_ID: {
+                    povAxisX = axis;
+                    break;
+                }
+                case POV_Y_AXIS_ID: {
+                    povAxisY = axis;
+                    break;
+                }
+            }
+
+        }
+
+        @Override
+        public JoystickAxis getXAxis() {
+            return xAxis;
+        }
+
+        @Override
+        public JoystickAxis getYAxis() {
+            return yAxis;
+        }
+
+        @Override
+        public JoystickAxis getPovXAxis() {
+            return povAxisX;
+        }
+
+        @Override
+        public JoystickAxis getPovYAxis() {
+            return povAxisY;
+        }
+
+        @Override
+        public int getXAxisIndex() {
+            return xAxis != null ? xAxis.getAxisId() : 0;
+        }
+
+        @Override
+        public int getYAxisIndex() {
+            return yAxis != null ? yAxis.getAxisId() : 1;
+        }
+
+        @Override
+        public void addButton(JoystickButton button) {
+            super.addButton(button);
+        }
+
+    }
+}

+ 11 - 9
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

@@ -32,6 +32,7 @@
 
 package com.jme3.system.lwjgl;
 
+import com.jme3.input.JoyInput;
 import com.jme3.input.lwjgl.GlfwJoystickInput;
 import com.jme3.input.lwjgl.GlfwKeyInput;
 import com.jme3.input.lwjgl.GlfwMouseInput;
@@ -124,7 +125,7 @@ public abstract class LwjglContext implements JmeContext {
 
     protected GlfwKeyInput keyInput;
     protected GlfwMouseInput mouseInput;
-    protected GlfwJoystickInput joyInput;
+    protected JoyInput joyInput;
 
     protected Timer timer;
 
@@ -275,17 +276,18 @@ public abstract class LwjglContext implements JmeContext {
             GLFW.glfwSetJoystickCallback(new GLFWJoystickCallback() {
                 @Override
                 public void invoke(int jid, int event) {
+                    if (!(joyInput instanceof GlfwJoystickInput)) return;
 
                     // Invoke the disconnected event before we reload the joysticks or lose the reference to it.
                     // Invoke the connected event after we reload the joysticks to obtain the reference to it.
-
-                    if ( event == GLFW.GLFW_CONNECTED ) {
-                        joyInput.reloadJoysticks();
-                        joyInput.fireJoystickConnectedEvent(jid);
-                    }
-                    else {
-                        joyInput.fireJoystickDisconnectedEvent(jid);
-                        joyInput.reloadJoysticks();
+                    GlfwJoystickInput glfwJoyInput = (GlfwJoystickInput) joyInput;
+
+                    if (event == GLFW.GLFW_CONNECTED) {
+                        glfwJoyInput.reloadJoysticks();
+                        glfwJoyInput.fireJoystickConnectedEvent(jid);
+                    } else {
+                        glfwJoyInput.fireJoystickDisconnectedEvent(jid);
+                        glfwJoyInput.reloadJoysticks();
                     }
                 }
             });

+ 14 - 3
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java

@@ -42,6 +42,7 @@ import com.jme3.input.TouchInput;
 import com.jme3.input.lwjgl.GlfwJoystickInput;
 import com.jme3.input.lwjgl.GlfwKeyInput;
 import com.jme3.input.lwjgl.GlfwMouseInput;
+import com.jme3.input.lwjgl.SdlJoystickInput;
 import com.jme3.math.Vector2f;
 import com.jme3.system.AppSettings;
 import com.jme3.system.Displays;
@@ -53,11 +54,9 @@ import com.jme3.util.SafeArrayList;
 import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
 import java.nio.ByteBuffer;
-import java.nio.IntBuffer;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -834,7 +833,19 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
     @Override
     public JoyInput getJoyInput() {
         if (joyInput == null) {
-            joyInput = new GlfwJoystickInput();
+            boolean useSdl = true;
+
+            String mapper = settings.getJoysticksMapper();
+            if (AppSettings.JOYSTICKS_LEGACY_MAPPER.equals(mapper)
+                    || AppSettings.JOYSTICKS_XBOX_LEGACY_MAPPER.equals(mapper)) {
+                useSdl = false;
+            }
+
+            if (useSdl) {
+                joyInput = new SdlJoystickInput(settings);
+            } else {
+                joyInput = new GlfwJoystickInput(settings);
+            }
         }
         return joyInput;
     }