isXander / isXander/Controlify
[Bug] SDLNativesLoader.loadFromLwjgl() is unimplemented on 26.3 (fix included)
@isXander is already working on this.
Since Sep 19, 2026.
- Dominant language
- Java
- Stars
- 341
- Forks
- 97
- Avg merge
- 1h
- Merged PRs (30d)
- 1
Description
Current Behaviour
Controller is not detected at all — neither in the main menu nor in the Controlify controller settings screen — despite being plugged in before launch.
The log shows SDLNativesLoader failing to load any SDL3 native and falling all the way through the loader chain:
[SDLNativesLoader] System did not provide SDL
java.lang.UnsatisfiedLinkError: no SDL3 in java.library.path: ...
...followed by:
Failed to initialize controller manager
java.lang.IllegalStateException: Could not load SDL natives
The only workaround I found was manually downloading SDL3.dll and adding -Ddev.isxander.sdl.library= as a JVM launch argument.
Expected Behaviour
Controlify should detect a usable SDL3 native automatically, with no manual JVM arguments or manually-placed files required.
This should already be possible on 26.3: Minecraft itself bundles lwjgl-sdl and uses SDL3 for its own window creation, so LWJGL has already extracted a valid SDL3 native to the directory pointed at by -Dorg.lwjgl.system.SharedLibraryExtractPath (the same JVM property the launcher already sets for this version) before Controlify ever initializes. Controlify's own loadFromLwjgl() strategy is meant to cover exactly this case, but never actually does anything (see below).
Screenshots
No response
Reproduction Steps
- Install Controlify 3.3.3+mc26.3-snapshot-7 on Minecraft 26.3 (Fabric), with a controller connected.
- Launch the game and load into a world.
- Open the Controlify controller settings screen, or just check whether inputs are recognized in-game.
- Controller is not detected. Check latest.log for the SDLNativesLoader errors described above.
Logs
[Render thread/INFO]: Initializing Controlify...
[Render thread/INFO]: Selected and locked Controlify profile 0
[Render thread/WARN]: [SDLNativesLoader] System did not provide SDL
java.lang.UnsatisfiedLinkError: no SDL3 in java.library.path: C:\Users\<user>\AppData\Roaming\ModrinthApp\meta\natives\26.3-0.19.5/java
at java.base/java.lang.ClassLoader.loadLibrary(Unknown Source)
at java.base/java.lang.Runtime.loadLibrary0(Unknown Source)
at java.base/java.lang.System.loadLibrary(Unknown Source)
at knot//dev.isxander.sdl.ffm.SdlNativeLibrary.configure(SdlNativeLibrary.java:29)
at knot//dev.isxander.sdl.ffm.SdlFfmLoader.create(SdlFfmLoader.java:17)
at knot//dev.isxander.controlify.driver.sdl.SDLNativesLoader.loadFromSystem(SDLNativesLoader.java:185)
at knot//dev.isxander.controlify.driver.sdl.SDLNativesLoader.load(SDLNativesLoader.java:57)
at knot//dev.isxander.controlify.Controlify.initializeControlify(Controlify.java:274)
[Render thread/ERROR]: Failed to initialize controller manager
java.lang.IllegalStateException: Could not load SDL natives
at knot//dev.isxander.controlify.driver.sdl.SDLNativesLoader.load(SDLNativesLoader.java:58)
at knot//dev.isxander.controlify.Controlify.initializeControlify(Controlify.java:274)
Mod Version
3.3.3+mc26.3-snapshot-7 (Patreon build) on Minecraft 26.3
Controller
Xinput Xbox layout, wired Flydigi Apex
Bluetooth
- Yes
Operating System
Windows
ARM
- Yes
Additional Information
Root cause: loadFromLwjgl() in dev.isxander.controlify.driver.sdl.SDLNativesLoader is an empty stub — confirmed both in the port/26.3 branch source and via javap on the shipped snapshot-7 jar (bytecode matches exactly):
private static Optional loadFromLwjgl(ServiceLoader serviceLoader) {
if (!LWJGL_SDL_AVAILABLE) {
return Optional.empty();
}
return Optional.empty(); // never actually implemented
}
Since this always returns empty, the loader chain falls through to loadFromSystem(), which only works if the user manually supplies a native on java.library.path — hence the workaround described above.
Proposed fix (tested working on Windows 11, controller detected immediately with zero manual configuration):
private static Optional loadFromLwjgl(ServiceLoader serviceLoader) {
if (!LWJGL_SDL_AVAILABLE) {
return Optional.empty();
}
SdlLoader sdlLoader = serviceLoader.stream()
.map(ServiceLoader.Provider::get)
.filter(loader -> loader.name().equals("ffm"))
.findAny().orElse(null);
if (sdlLoader == null) {
return Optional.empty();
}
String extractPath = System.getProperty("org.lwjgl.system.SharedLibraryExtractPath");
if (extractPath == null || extractPath.isBlank()) {
return Optional.empty();
}
// Naming convention seems to vary - checked both in testing.
Path prefixedName = Path.of(extractPath, "lwjgl" + NATIVE_SDL_NAME);
Path plainName = Path.of(extractPath, NATIVE_SDL_NAME);
Path lwjglNative = Files.isRegularFile(prefixedName) ? prefixedName
: Files.isRegularFile(plainName) ? plainName
: null;
if (lwjglNative == null) {
return Optional.empty();
}
Path finalPath = lwjglNative;
Optional<Sdl> sdl = loadFromInputStream(sdlLoader, () -> Files.newInputStream(finalPath));
sdl.ifPresent(ignored -> LOGGER.log("Loaded SDL from LWJGL's own extracted natives at {}", finalPath));
return sdl;
}
This is purely additive — if it finds nothing, it falls through safely to the existing loadFromNativesInJar/loadFromSystem paths exactly as before, so there's no regression risk for setups where this doesn't apply.
Disclosure: I'm not a developer by trade — I dug into this (decompiling the jar, comparing against the port/26.3 source, writing and testing this fix) with help from Claude AI, since I was curious whether the JVM-argument workaround could be replaced with something that "just worked." Happy to share the compiled class file, a full diff, or open a PR if that's useful — just let me know which you'd prefer.
Just to make sure...
- I have made sure I am using the latest version of Controlify for the latest version of Minecraft.
- I have made sure there are no other issues describing the same problem on the issue tracker.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.