Skip to content

fix: Correct inverted Android D-pad Up/Down (AXIS_HAT_Y double-inversion) - #128

Merged
spydon merged 1 commit into
flame-engine:mainfrom
GhagSagar23:fix/123-gamepadbutton-dpadup-dpaddown-invert
Aug 25, 2026
Merged

fix: Correct inverted Android D-pad Up/Down (AXIS_HAT_Y double-inversion)#128
spydon merged 1 commit into
flame-engine:mainfrom
GhagSagar23:fix/123-gamepadbutton-dpadup-dpaddown-invert

Conversation

@GhagSagar23

Copy link
Copy Markdown
Contributor

Summary

Fixes #123.

On Android, pressing the D-pad Up emitted GamepadButton.dpadDown (value 1.0), and Down emitted dpadUp. Root cause is a double inversion of the D-pad Y hat axis:

  • gamepads_android's EventListener registers AXIS_HAT_Y with invert = true (EventListener.kt:27), so the value delivered to Dart is already flipped to up = +1.0 / down = −1.0 — the opposite of Android's native AXIS_HAT_Y (up = −1.0 / down = +1.0).
  • AndroidMapping.normalizeDpadAxis still mapped per Android's native convention (value > 0 → dpadDown, value < 0 → dpadUp), so the two flips compounded.

AXIS_HAT_X is not inverted natively, so D-pad left/right were unaffected — matching the report.

Change

Corrected the sign comparison in a single canonical location — the AXIS_HAT_Y branch of AndroidMapping.normalizeDpadAxis — so dpadUp fires on value > 0 and dpadDown on value < 0, with a comment citing #123. EventListener.kt is intentionally left unchanged to avoid re-introducing a double-inversion.

  • packages/gamepads/lib/src/mappings/android_mapping.dart
  • packages/gamepads/test/mappings_test.dart — both-direction AXIS_HAT_Y regression

Fix-location decision (maintainer call welcome)

The reporter raised a genuine design choice for where to fix this:

  1. Kotlin (EventListener.kt): drop the AXIS_HAT_Y inversion → raw/unnormalized values would then follow Android's 1.0 == down.
  2. Dart (android_mapping.dart, this PR): keeps the library's existing 1.0 == up raw convention.

This PR takes option 2 to preserve the current raw-value convention (no breaking change) and because the mapping is pure Dart and directly unit-testable. Happy to switch to the Kotlin approach if you prefer — but the fix must live in exactly one place; applying it in both would double-invert and silently reintroduce the bug.

Tests

Added a both-direction regression to AndroidMapping > "normalizes hat d-pad axes":

  • 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

This fails on the pre-fix code (which returned dpadDown = 1.0 for +1.0) and passes after the fix.

Note: the containerized Flutter runner was gated in this non-interactive session, so the fail→pass transition was verified by tracing, not executed. Please run before merge:

flutter pub get && flutter test packages/gamepads/test/mappings_test.dart
flutter analyze packages/gamepads

End-to-end hardware confirmation (physical Android device + controller, e.g. the reporter's SM-G781B + GameSir G8 Pro) is out of CI scope.

Deferred

  • Analog stick Y-axis (leftStickY/rightStickY): the reporter separately observed up reading -1.0. Left untouched here — confirm it's a genuine inversion vs. Android's native joystick convention before changing; tracked as a follow-up reusing this PR's convention decision.
  • CHANGELOG / version bump: not hand-edited — this melos workspace generates them at release time from the fix: PR title (CONTRIBUTING.md, "Creating a release").

Coordination

Thanks @fescrb for the precise root-cause analysis. You'd offered to submit this — glad to defer to you or just help with review; sharing this as a ready-to-go option while the fix-location question is settled.

@GhagSagar23 GhagSagar23 changed the title fix: correct inverted Android D-pad Up/Down (AXIS_HAT_Y double-inversion) fix: Correct inverted Android D-pad Up/Down (AXIS_HAT_Y double-inversion) Aug 17, 2026
@GhagSagar23
GhagSagar23 marked this pull request as ready for review August 17, 2026 07:00
@spydon
spydon requested a balanced review from Copilot August 25, 2026 08:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@spydon spydon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution!

@spydon
spydon merged commit 8871119 into flame-engine:main Aug 25, 2026
7 of 8 checks passed
spydon added a commit that referenced this pull request Aug 25, 2026
…ick (#129)

Follow-up to #128, which fixed the same double inversion for the d-pad
hat axis but left the analog sticks inconsistent with it. Also closes
the second symptom reported in #123.

## Stick Y axis was double inverted

`gamepads_android`'s `EventListener` registers `AXIS_Y`, `AXIS_RZ` and
`AXIS_RY` with `invert = true`, so a physical stick-up already arrives
in Dart as `+1.0`. `AndroidMapping.normalizeAxis` negated it a second
time:

```dart
if (axis == GamepadAxis.leftStickY || axis == GamepadAxis.rightStickY) {
  return [NormalizedAxis(axis, -value)];
}
```

The result was `leftStickY = -1.0` for up on Android, while the same
gesture gives `+1.0` on iOS, macOS and Web. That contradicts the
convention documented on `GamepadAxis` and `PlatformMapping` ("Left/Down
= -1, Right/Up = +1"), so cross-platform game code moved the player in
opposite directions.

After #128 the file also argued both ways: "EventListener already
inverted it" for the hat axis, and the opposite three lines above for
the sticks.

## RX/RY right stick events were dropped

`EventListener` reports `AXIS_RX` and `AXIS_RY` (added in #111 for
non-Xbox layouts such as the DJI RC Pro), but they were missing from
`_axisMap`. Unmatched analog keys fall through to `normalizeDpadAxis`,
which returns `const []` for anything that is not a hat axis, so those
events were discarded. On a DJI RC Pro the right stick produced no
normalized events at all.

Both are mapped to `rightStickX`/`rightStickY`, the same aliasing
pattern already used for `AXIS_BRAKE`/`AXIS_GAS`.

## Guarding the cross-package invariant

`AndroidMapping`'s correctness depends on the `invert = true` flags
staying in the separately versioned Kotlin plugin, and until now the
only trace of that was a comment on the Dart side. `EventListener.kt` is
the more obvious place for a contributor to "fix" against Android's
documentation, and doing so would silently re-invert everything without
a single test failing, since the tests only exercise the Dart half.
Added a counterpart comment next to `supportedAxes` pointing back at the
Dart mapping.

`AXIS_WHEEL` is still reported by `EventListener` and still unmapped,
since there is no matching `GamepadAxis`. It stays available through the
raw event API.

## Changes

- `packages/gamepads/lib/src/mappings/android_mapping.dart`
- `packages/gamepads/lib/src/mappings/platform_mapping.dart`, the Y-axis
contract no longer lists Android as a platform that must negate
- `packages/gamepads_android/.../EventListener.kt`, comment only
- `packages/gamepads/test/mappings_test.dart`, stick assertions
corrected, new RX/RY test
- `packages/gamepads/test/gamepad_normalizer_test.dart`, the `AXIS_Y`
test asserted the buggy `-1.0`

## Verification

`melos analyze` is clean, `dart format` reports no changes, and all 80
tests in `packages/gamepads` pass. Hardware confirmation on a physical
Android device is still worth doing before release.

https://claude.ai/code/session_019aEP4Np5vqDw5BkgGYyHe7
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.

GamepadButton.dpadUp/.dpadDown inverted for Android

3 participants