Skip to content

fix: Linux gamepad hotplug support, disconnect handling, and downcall fix (#79) - #80

Merged
steffen-wilke merged 7 commits into
mainfrom
fix/issue-79-linux-gamepad-hotplug
Sep 5, 2026
Merged

fix: Linux gamepad hotplug support, disconnect handling, and downcall fix (#79)#80
steffen-wilke merged 7 commits into
mainfrom
fix/issue-79-linux-gamepad-hotplug

Conversation

@steffen-wilke

Copy link
Copy Markdown
Collaborator

Fixes #79

Overview

Resolves several interrelated issues on Linux where gamepad connections and disconnections were not detected, downcalls to write/setGain caused a JVM WrongMethodTypeException, laptop touchpads/mice were falsely registered as controllers, and polling caused unbounded off-heap memory allocations.


Problems & Solutions

1. Gamepad Connects and Disconnects Not Registering

  • Problem: LinuxEventDevicePlugin.refreshInputDevices() was an unpopulated stub. In addition, AbstractInputDevicePlugin.getAll() never triggered refreshDevices(), preventing discovery if an application started with 0 gamepads connected.
  • Solution:
    • Implemented LinuxEventDevicePlugin.refreshInputDevices() to scan /dev/input/event*, register newly connected devices, detect disconnected devices via EVIOCGVERSION, and prune stale entries.
    • Added periodic refreshDevices() checks in AbstractInputDevicePlugin.getAll().
    • Added refreshDevices(boolean force) to allow instant, out-of-band discovery/cleanup without waiting for hotPlugInterval to expire.
    • Updated AbstractInputDevicePlugin to commit internal device lists before dispatching connect/disconnect/change listeners so listener callbacks querying InputDevices.getDevices() or getAll() observe the updated state.

2. WrongMethodTypeException on Linux write / setGain

  • Problem: In Linux.java, HANDLE_WRITE was configured with a return type of sizeT (JAVA_LONG on 64-bit platforms), but downcall call sites cast the polymorphic invoke result to (int). This threw java.lang.invoke.WrongMethodTypeException: cannot adapt long to int whenever rumble effects or gain were updated.
  • Solution: Changed HANDLE_WRITE's function descriptor return type to JAVA_INT matching HANDLE_READ and invoke().

3. File Descriptor Exhaustion & Disconnect Log Spam

  • Problem: Scanning /dev/input/event* opened devices that were discarded (e.g. video buses, HDMI, or devices without required components) without closing them, leading to file descriptor leaks. Furthermore, when a device was unplugged, subsequent operations logged continuous Level.SEVERE errors.
  • Solution:
    • Added explicit device.close(this.memoryArena) on all ignored or discarded device nodes during discovery.
    • Added EBADF (9) and ENODEV (19) errno constants. When detected during readEvent(), devices are marked disconnected, refreshDevices(true) is triggered immediately, and logs are downgraded to Level.FINE.
    • Added a closed guard in LinuxEventDevice.close() to prevent double-closing and skip sending rumble stop commands to nodes already detached by the kernel.

4. Zero-Allocation Polling in Shared Arenas

  • Problem: Polling via Linux.read(Arena, fd) allocated an input_event layout and captured state layout on every single event into memoryArena (a shared arena that lives for the lifetime of the plugin), creating unbounded off-heap native memory growth.
  • Solution: Pre-allocated inputEventSegment, capturedStateSegment, and an errno buffer per LinuxEventDevice instance, eliminating native heap allocations inside the hot polling loop.

5. False-Positive Controller Detection (Touchpads, Mice, Keyboards)

  • Problem: Touchpads (e.g. SYNA30D2:00 Touchpad from issue logs) and mice expose ABS_X/ABS_Y and BTN_LEFT, satisfying the previous naive anyMatch(BUTTON || AXIS) check and registering as game controllers.
  • Solution:
    • Added LinuxEventDevicePlugin.isGamepadOrJoystick(keyBits, absBits) implementing kernel evdev heuristics:
      • Accepts standard gamepad/joystick buttons (BTN_GAMEPAD, BTN_JOYSTICK, BTN_TRIGGER_HAPPY, BTN_0..9, BTN_WHEEL, BTN_DPAD_*).
      • Accepts dedicated flight stick, rudder, throttle, wheel, and hat axes.
      • Explicitly filters out touchpads/touchscreens (BTN_TOUCH, BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP, etc.), mice (BTN_LEFT..BTN_TASK), and keyboards lacking gamepad buttons.
    • Added array bounds and null checking to LinuxEventDevice.isBitSet().

Changes by Commit

  1. f887c59 feat: improve hotplug lifecycle and event consistency in AbstractInputDevicePlugin
  2. 7dea2ae fix(linux): fix WrongMethodTypeException on write downcall and add disconnect errnos
  3. 61a2ac7 feat(linux): implement device hotplug, disconnect handling, and zero-allocation polling
  4. f52662c fix(linux): filter out touchpads, mice, and keyboards from controller detection

Verification & Testing

  • New Test Suites:
    • AbstractInputDevicePluginTests: Validates initialization checks, onDeviceConnected, onDeviceDisconnected, onDevicesChanged, hotPlugInterval expiry, and listener cleanup on close().
    • LinuxEventDevicePluginTests: Comprehensive unit tests for isGamepadOrJoystick covering Xbox controllers, flight sticks, arcade fight sticks, rudder pedals, hat switches, DualShock 4 with integrated touchpads, and rejecting laptop trackpads, mice, and keyboards.
    • LinuxPermissionTests: Validates EBADF and ENODEV errno mappings.
  • Build Status:
    • ./gradlew build: SUCCESSFUL (all unit tests and tasks passing).
    • ./gradlew spotlessCheck: SUCCESSFUL (enforces Google Java Format).

…tDevicePlugin

- Trigger periodic refreshDevices() within getAll() so hotplugging works when starting with 0 devices
- Add refreshDevices(boolean force) overload to allow plugins to immediately trigger discovery/cleanup
- Update internal device collection before dispatching listeners so queries in callbacks observe the updated state
- Ensure thread safety with synchronized refreshDevices() and CopyOnWriteArrayList
…sconnect errnos

- Fix HANDLE_WRITE return type to JAVA_INT matching HANDLE_READ and polymorphic invoke
- Add EBADF (9) and ENODEV (19) errno constants with unit test assertions
- Treat EBADF and ENODEV as expected disconnections in Linux.invoke instead of logging SEVERE
- Add overloaded read method supporting pre-allocated memory segments
…allocation polling

- Implement refreshInputDevices() to scan /dev/input/event*, register newly connected devices, and remove disconnected ones
- Automatically trigger refreshDevices() from pollLinuxEventDevice()
- Detect ENODEV and EBADF on event read, immediately marking devices disconnected and forcing device refresh
- Prevent file descriptor leaks by immediately closing discarded devices during scan
- Prevent double-close in LinuxEventDevice and skip rumble stop calls on already-disconnected devices
- Pre-allocate event structures and errno buffer per device to achieve zero-allocation polling in shared arena
… detection

- Add isGamepadOrJoystick() heuristic checking EV_KEY and EV_ABS bitmasks
- Accept standard gamepad/joystick buttons (BTN_GAMEPAD, BTN_JOYSTICK, BTN_TRIGGER_HAPPY, BTN_0..9, BTN_WHEEL, BTN_DPAD_*)
- Accept flight stick, rudder, throttle, wheel, gas, brake, and hat axes
- Reject touchpads (BTN_TOUCH, BTN_TOOL_FINGER), mice (BTN_MOUSE..BTN_TASK), and keyboards without gamepad buttons
- Add bounds checking to LinuxEventDevice.isBitSet() to guard against short bitmask arrays
- Add comprehensive unit test suite covering gamepads, flight sticks, rudder pedals, touchpads, and mice
…te downcalls

- Define ssizeT = IS_32_BIT ? JAVA_INT : JAVA_LONG to accurately match POSIX ssize_t across architectures
- Update FunctionDescriptor return types for read and write to ssizeT
- Handle long.class return types in invokeWithCapturedState without WrongMethodTypeException
- Add regression tests verifying downcall handle return types match architecture ssize_t
…and periodic refresh

- Probe candidate event device nodes inside try-with-resources Arena.ofConfined() so non-gamepad devices leave zero off-heap memory behind
- Allocate a dedicated Arena.ofShared() only when a candidate node is confirmed as a gamepad/joystick
- Close the per-device arena when the device is disconnected or closed, reclaiming 100% of native resources
- Pre-allocate versionSegment and versionCapturedState in LinuxEventDevice to make periodic liveness checks completely allocation-free
- Use Arena.ofConfined() for force-feedback rumble effect upload, playback, and removal to prevent off-heap growth during rumble
- Remove plugin-level shared arena from LinuxEventDevicePlugin
- Add regression tests for candidate probe arena lifecycle, device disconnection, and arena closing
@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

@steffen-wilke
steffen-wilke merged commit f3c2322 into main Sep 5, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not registering connects or disconnects on Linux

1 participant