Pressing Up on an Android gamepad fired dpadDown. Pressing Down fired dpadUp. Left and Right worked fine. The bug lived in a single sign comparison, but finding which sign comparison took longer than writing the fix.
The report
Issue #123 in the Flame engine's gamepads package. A developer on a Samsung SM-G781B with a GameSir G8 Pro controller posted the raw event log:
GAMEPAD EVENT: button="GamepadButton.dpadUp" axis= null value=0.0
GAMEPAD EVENT: button="GamepadButton.dpadDown" axis= null value=1.0
That's what pressing Up produces. Up fires dpadDown with value 1.0. The reporter filed the issue, tagged it with their exact device, Android 13, Flutter 3.44.6, and gamepads: 0.1.10+5. Clean report.
Two inversions make a wrong
Android's AXIS_HAT_Y natively gives you -1.0 for up and +1.0 for down. The Kotlin side (EventListener.kt:27) registers the axis with invert = true, so by the time the value reaches Dart it's already flipped: +1.0 means up.
The Dart mapping in AndroidMapping.normalizeDpadAxis didn't know about that flip. It was still mapping per Android's native convention: value > 0 maps to dpadDown, value < 0 maps to dpadUp. So the Kotlin layer flips the sign, and the Dart layer interprets the flipped sign as if it hadn't been flipped. Two inversions. The result is backwards.
AXIS_HAT_X is not inverted natively, so left and right were fine. That's why only the Y axis was wrong.
The fix decision
There were two valid places to fix this: remove the invert = true in Kotlin, or correct the sign comparison in Dart. I went with Dart. The Kotlin inversion was intentional (the library's raw-value convention treats +1.0 as up), and undoing it would change the raw values downstream consumers might depend on. The Dart mapping is pure and directly unit-testable. One place, not two: fixing both would re-introduce the double inversion silently.
15 lines added, 4 deleted. The test covers both directions explicitly:
// raw AXIS_HAT_Y = +1.0 (physical up) -> dpadUp = 1.0, dpadDown = 0.0
// raw AXIS_HAT_Y = -1.0 (physical down) -> dpadDown = 1.0, dpadUp = 0.0
Spydon (Flame engine maintainer) approved and merged. I called out the fix-location trade-off in the PR description because this is the kind of bug that gets re-introduced by someone who sees the Kotlin inversion, removes it thinking it's wrong, and doesn't realize the Dart side is compensating.
What I couldn't verify
I don't own a GameSir G8 Pro. I traced the logic path and wrote the regression test, but end-to-end hardware confirmation on the reporter's exact setup was out of scope. I flagged that in the PR. The fix is correct by code analysis (the double inversion is unambiguous once you read both files), but I haven't personally held the controller and pressed Up.
Top comments (0)