Platform Support
Control Flex reads controllers through libgamepadjni + SDL3. On desktop, both the native library and SDL3 ship inside the JAR, so it works out of the box; on Android, SDL3 must come from the launcher APK, which is why behaviour varies a little between launchers.
At a Glance
| Platform | Architectures | SDL3 source | Status |
|---|---|---|---|
| Windows | x86_64 | Bundled in the JAR (SDL3.dll) |
Supported |
| macOS | x86_64 · Apple Silicon (aarch64) | Bundled in the JAR (libSDL3.0.dylib) |
Supported |
| Linux | x86_64 · aarch64 | Bundled in the JAR (libSDL3.so) |
Supported |
| Android game JVM inside a launcher |
arm64-v8a · armeabi-v7a · x86_64 | Provided by the launcher APK; never bundled | Supported |
| iOS | — | — | Not yet |
Detection reads os.version only: a value starting with Android- means Android, everything else is treated as desktop. os.name (Android launchers also report Linux) is written to diagnostic logs and never used for detection.
Desktop
- No SDL3, driver, or extra runtime to install — the mod always uses the SDL3 packaged in the JAR, so it neither conflicts with a system SDL nor searches system paths.
- Windows ships x86_64 natives only (
gamepadjni.dll+SDL3.dll); there is no ARM64 build in the JAR yet, so on Windows on ARM run an x64 JVM — an arm64 JVM fails withUnsatisfiedLinkError. - macOS ships both Intel and Apple Silicon
libgamepadjni.dylibbuilds, so it works with or without Rosetta. - Linux ships x86_64 and aarch64
libgamepadjni.sobuilds, covering aarch64 devices such as Steam Deck. - Hotplug, multi-controller detection, and per-controller identity (name / type / serial) behave the same across the three desktop platforms.
Android (inside a Launcher)
On Android, Minecraft: Java Edition runs on a JVM provided by a launcher, so the controller path is split in two: SDL3 comes from the launcher APK (the launcher already installed the SDL glue on the ART side), while libgamepadjni.so ships with the mod and is extracted per os.arch. The order is fixed — SDL3 first, then the JNI bridge — or the dynamic linker binds to a different copy.
Two Launcher Settings to Change First
Both live in the launcher, not in the mod, and both have to be redone whenever you switch launchers or the instance directory gets reset. Skipping them does not stop the game from starting — it just looks broken: a stray pointer floating over the screen, or a pad that never appears in settings.
1 · Hide the on-screen virtual mouse pointer
The pointer the launcher draws is its own overlay and has nothing to do with the mod's virtual cursor — the two do not track each other, so you end up with two pointers on screen, or with one that sits wherever you last touched while the actual click lands somewhere else.
Three equivalent ways to get rid of it; pick whichever your launcher exposes:
- turn the pointer display off (hide the virtual mouse cursor)
- set the pointer size to
0 - set the pointer opacity to
0, i.e. fully transparent
After that only the mod's own cursor is on screen and its position matches where clicks land. Style and scale are set in game under Cursor Style: System / Bedrock / Hand, scale 0.1–1.0.
2 · Set Gamepad Input Mode to SDL Direct (ZL2 / FCL)
In Zalith Launcher 2 and Fold Craft Launcher, Gamepad Input Mode defaults to Mapped (Virtual Keys): buttons and sticks are first translated into virtual keyboard/mouse events, and only then handed to the game. ControlFlex never sees the raw device — no pad in settings, and deadzone, trigger travel, and rumble are all decided by the launcher.
Pick SDL Direct instead. In ZL2 the path is Settings → Gamepad → Gamepad Input Mode, and the option describes itself: raw gamepad input is delivered to the game through the SDL/GLFW gamepad API with button remapping bypassed — exactly what is needed here. SDL3 opens the device directly and the mod reads raw axes and buttons through libgamepadjni, which is the only reason multiple pads, hotplug, and rumble work at all.
- First make sure the Gamepad Control master switch at the top of the same page is on — turn it off only when some other device is being mistaken for a gamepad.
- Check: in game, Settings → Gamepad Selection lists the device by name and reacts to any button.
- Trade-off: once SDL Direct is on, Mapped (Virtual Keys) no longer applies to that pad. That is expected — bind everything in the mod's key mapping panel instead.
- Quit the game and relaunch after changing it, so the launcher re-enumerates devices.
Launchers Verified
| Launcher | How the launcher's libSDL3.so is found |
|---|---|
| Fold Craft Launcher (FCL) | Sets POJAV_NATIVEDIR to the APK native dir; loaded by absolute path |
| Zalith Launcher 2 (ZL2) | Same as above |
| Mojo Launcher v3_openjdk |
No POJAV_NATIVEDIR; the APK native dir sits on LD_LIBRARY_PATH, so System.loadLibrary("SDL3") resolves it |
| Amethyst Android | Sets POJAV_NATIVEDIR and loads by absolute path |
This is a compatibility note, not a runtime allowlist: any launcher that sets os.version to Android-* takes the Android path, listed or not.
ABIs and Natives
os.arch |
Directory in the JAR |
|---|---|
aarch64 / arm64 | native/android-arm64-v8a/ |
arm / armv7l / armv8l | native/android-armeabi-v7a/ |
x86_64 / amd64 | native/android-x86_64/ |
i386 and others |
Unsupported — initialization fails and the game still launches, just without a controller |
Android Caveats
Do not override os.version. Some launchers' custom JVM arguments or version JSON replace it with a plain kernel number (e.g. 5.0) or drop the Android- prefix. The mod then treats the JVM as desktop Linux, loads a desktop SDL3, and the controller never shows up in settings. Make sure the launcher's -Dos.version=Android-<release> is not overridden by your own arguments.
- The JAR ships no Android
libSDL3.so; do not drop another SDL3 next to your mods. Loading an SDL without the launcher glue makes controllers permanently invisible. - To supply your own
libgamepadjni.so, pointCFX_LIB_PATHat the directory holding it (system property wins over environment variable). - On Android, SDL is shared with the launcher inside one process, so the mod never
SDL_Quits the whole library on shutdown — that would tear down the launcher's video / glue. - The config directory is still
config/controlflex/, relative to the game directory the launcher assigned to that instance; switching launchers (or instance dirs) does not carry configs over. - Rumble, cursor, and split-screen availability depends on the launcher's window model and the device: split-screen targets multi-instance desktop setups, and Android devices normally cannot run two instances side by side.
Platform & Dependency Matrix
| Platform | Extra installs | Config location |
|---|---|---|
| Windows / macOS / Linux | None (SDL3 bundled) | config/controlflex/ |
| Android | None (SDL3 comes from the launcher; the launcher must be covered by one of the mechanisms above) — plus the two launcher settings above | config/controlflex/ under the launcher's instance dir |
Troubleshooting
| Symptom | Check first |
|---|---|
| No device at all in Settings → Gamepad Selection (desktop) | Does the JAR match your MC version and loader (see Minecraft Versions)? Is another program (Steam Input, remote play) holding the pad? |
| On Android the game runs but no controller is ever visible | Is os.version overridden? Is the launcher one of the verified ones? Do the logs show POJAV_NATIVEDIR and the SDL3 path? |
| Two pointers on screen, or the pointer sits away from where clicks land | The launcher's virtual mouse pointer is still visible — hide it, or set its size / opacity to 0 |
| The pad works through the launcher's own key mapping, but never appears in Settings → Gamepad Selection | Gamepad Input Mode is set to Mapped (Virtual Keys) instead of SDL Direct — see the two settings above |
UnsatisfiedLinkError in the log |
On Android, either an unsupported ABI (e.g. 32-bit x86) or the launcher APK lacks SDL3; on desktop, the native does not match the OS architecture — note that Windows only carries x86_64 natives, so ARM64 machines need an x64 JVM. |
| Nothing available on iOS | iOS is not wired up yet (the platform layer reserves a flag, but no natives exist) |