Fixes the regression from #361-era commit 3b39710a reported by two users on 2026-08-21.
The reports
GameSir G8+ — "several inputs appear to be swapped or inverted".
Xbox Elite Series 2 ("Xbox Wireless Controller" over Bluetooth) — all correct except:
pressed
arrives as
X
Y
Y
LB
LB
a menu button
RB
the other menu button
Why
That table is not a stray mapping — it is precisely what PadButtons.GENERIC_XBOX does to scancodes 0x133 / 0x134 / 0x136 / 0x137. The correction added yesterday was firing on pads whose buttons already sit where Generic.kl says they do, and every scancode outside its table passed through untouched, which is the "all correct except" part.
It fired because it asked a question that cannot answer this. hasKeys(BUTTON_C, BUTTON_Z) reports what a device declares, not what it reports: hid-input allocates BTN_A + n straight through for every button in the descriptor, so BTN_C (0x132) and BTN_Z (0x135) are set on any pad declaring six or more — a standard-layout pad that never presses either included.
So the signal is identical on the pad that needs correcting and the pad that does not. It is literally the same model in both directions: an Elite Series 2 needed the correction on a Fire TV Stick, and another Elite Series 2 was broken by it here. No tightening of that check could have separated them.
The fix
The axes separate them. A HID gamepad describes its triggers either as the Accelerator/Brake usages — which become ABS_GAS / ABS_BRAKE, names Android has words for — or as two more generic axes on ABS_Z / ABS_RZ, which it does not. A descriptor well-formed enough to name its triggers puts its buttons at the standard positions too. It is also the firmware line on the exact pad in the report: an Xbox Wireless Controller over Bluetooth reports GAS/BRAKE after its firmware update and Z/Rz before it, and only the older one was ever wrong.
padButtons now takes namedTriggers and answers NATIVE whenever it is set — no correction of any kind, on buttons or on axes, for a pad Android already reads correctly. padMap was computing that fact one line below and spending it only on the axes; it now decides both. hasKeys stays for the narrower question it can answer — which straight-through order, once the axes have established there is one — where a false positive costs nothing.
Moonlight decides it on the same fact (ControllerHandler, gasRange == null beside the "Xbox Wireless Controller" name). 3b39710a cited Moonlight's tables and then replaced its discriminator with hasKeys, which is where this came in.
The original Fire TV pads are unaffected: neither names its triggers, so both still get their correction.
Verified
:kit:testDebugUnitTest and :app:testDebugUnitTest green (16 cases in PadButtonsTest, 3 new: the gate holds for every vendor/declaration combination, the four reported buttons stay themselves, and the report-order choice past the gate is unchanged). :app:compileDebugKotlin clean. Not yet tested on glass.
Not fixed here
The DualSense report filed alongside these — Triangle dead in the client UI and in the stream — is not explained by this change and is not addressed by it. A button that reaches neither is one buttonBit maps to nothing, and no branch of the correction produces that for Triangle. The Controllers screen prints the raw scancode and keycode of every press (<pad>: scan 0x… · RAW → CORRECTED); that one line, pressing Triangle on the reporter's pad, pins it.
Fixes the regression from #361-era commit `3b39710a` reported by two users on 2026-08-21.
## The reports
**GameSir G8+** — "several inputs appear to be swapped or inverted".
**Xbox Elite Series 2** ("Xbox Wireless Controller" over Bluetooth) — all correct except:
| pressed | arrives as |
|---|---|
| X | Y |
| Y | LB |
| LB | a menu button |
| RB | the other menu button |
## Why
That table is not a stray mapping — it is precisely what `PadButtons.GENERIC_XBOX` does to scancodes `0x133` / `0x134` / `0x136` / `0x137`. The correction added yesterday was firing on pads whose buttons already sit where `Generic.kl` says they do, and every scancode outside its table passed through untouched, which is the "all correct except" part.
It fired because it asked a question that cannot answer this. `hasKeys(BUTTON_C, BUTTON_Z)` reports what a device **declares**, not what it **reports**: `hid-input` allocates `BTN_A + n` straight through for every button in the descriptor, so BTN_C (`0x132`) and BTN_Z (`0x135`) are set on *any* pad declaring six or more — a standard-layout pad that never presses either included.
So the signal is identical on the pad that needs correcting and the pad that does not. It is literally the same model in both directions: an Elite Series 2 needed the correction on a Fire TV Stick, and another Elite Series 2 was broken by it here. No tightening of that check could have separated them.
## The fix
The axes separate them. A HID gamepad describes its triggers either as the Accelerator/Brake usages — which become `ABS_GAS` / `ABS_BRAKE`, names Android has words for — or as two more generic axes on `ABS_Z` / `ABS_RZ`, which it does not. A descriptor well-formed enough to name its triggers puts its buttons at the standard positions too. It is also the firmware line on the exact pad in the report: an Xbox Wireless Controller over Bluetooth reports GAS/BRAKE after its firmware update and Z/Rz before it, and only the older one was ever wrong.
`padButtons` now takes `namedTriggers` and answers `NATIVE` whenever it is set — no correction of any kind, on buttons or on axes, for a pad Android already reads correctly. `padMap` was computing that fact one line below and spending it only on the axes; it now decides both. `hasKeys` stays for the narrower question it *can* answer — **which** straight-through order, once the axes have established there is one — where a false positive costs nothing.
Moonlight decides it on the same fact (`ControllerHandler`, `gasRange == null` beside the `"Xbox Wireless Controller"` name). `3b39710a` cited Moonlight's tables and then replaced its discriminator with `hasKeys`, which is where this came in.
The original Fire TV pads are unaffected: neither names its triggers, so both still get their correction.
## Verified
`:kit:testDebugUnitTest` and `:app:testDebugUnitTest` green (16 cases in `PadButtonsTest`, 3 new: the gate holds for every vendor/declaration combination, the four reported buttons stay themselves, and the report-order choice past the gate is unchanged). `:app:compileDebugKotlin` clean. Not yet tested on glass.
## Not fixed here
The **DualSense** report filed alongside these — Triangle dead in the client UI *and* in the stream — is not explained by this change and is not addressed by it. A button that reaches neither is one `buttonBit` maps to nothing, and no branch of the correction produces that for Triangle. The Controllers screen prints the raw scancode and keycode of every press (`<pad>: scan 0x… · RAW → CORRECTED`); that one line, pressing Triangle on the reporter's pad, pins it.
Two field reports (2026-08-21), one shape: a GameSir G8+ and an Xbox Elite
Series 2 ("Xbox Wireless Controller" over Bluetooth) with X answering Y, Y
answering LB, and the two shoulders answering menu buttons — everything else
correct. That is not a stray mapping, it is exactly what `GENERIC_XBOX` does to
scancodes `0x133`/`0x134`/`0x136`/`0x137`, so the correction added yesterday was
firing on pads whose buttons were already where `Generic.kl` says they are.
It fired because it asked the wrong question. `hasKeys(BUTTON_C, BUTTON_Z)`
answers for what a device DECLARES, not what it reports: `hid-input` allocates
`BTN_A + n` straight through for every button in the descriptor, so BTN_C
(`0x132`) and BTN_Z (`0x135`) are set on ANY pad declaring six or more —
including a standard-layout pad that never presses either. The signal is
therefore identical on the pad that needs correcting and the pad that does not,
and no amount of tightening it could have separated them. It is the same pad
model in both reports: an Elite Series 2 needed the correction on a Fire TV and
another Elite Series 2 was broken by it here.
What does separate them is the axes. A HID gamepad describes its triggers either
as the Accelerator/Brake usages — which become `ABS_GAS`/`ABS_BRAKE`, names
Android has words for — or as two more generic axes on `ABS_Z`/`ABS_RZ`, which
it does not. A descriptor well-formed enough to name its triggers puts its
buttons at the standard positions too. It is also the firmware line on the pad in
the report: an Xbox Wireless Controller over Bluetooth reports GAS/BRAKE after
its firmware update and Z/Rz before it, and only the older one was ever wrong.
`padButtons` now takes `namedTriggers` and answers NATIVE whenever it is set —
no correction of any kind, on buttons or axes, for a pad Android already reads.
`padMap` computed that fact one line below and only ever spent it on the axes;
it now decides both. `hasKeys` stays for the narrower question it can answer —
WHICH straight-through order, once the axes have established there is one — where
a false positive costs nothing.
Moonlight decides it on the same fact (`ControllerHandler`, `gasRange == null`
beside the `"Xbox Wireless Controller"` name); yesterday's commit cited its
tables and then replaced its discriminator, which is where this came in.
Verified: `:kit:testDebugUnitTest` and `:app:testDebugUnitTest` green (16 cases
in PadButtonsTest, 3 new: the gate holds for every vendor/declaration
combination, the four reported buttons stay themselves, and the report-order
choice past the gate is unchanged), `:app:compileDebugKotlin` clean. The
DualSense report filed alongside these — Triangle dead in the client UI and in
the stream — is NOT explained by this and is not fixed here: a button that
reaches neither is one `buttonBit` maps to nothing, which no branch of the
correction produces for Triangle. The Controllers screen prints the raw scancode
and keycode of every press; that line off the reporter's pad will pin it.
Correcting the reasoning in the "Not fixed here" section above — the conclusion survives, the argument for it did not.
The argument was wrong. It read: a Triangle that reaches neither the client UI nor the stream must be one buttonBit maps to nothing, and no branch of the correction produces that. That treats "not working at all" as evidence of a dropped button, and it isn't. A button displaced onto something with no visible effect in that context reads identically to a reporter — the north face button landing on LB is exactly what "Y is mapped to LB" describes in the Xbox report, and on a DualSense that button is Triangle. Displacement had to be ruled out on its own, not assumed away.
Ruling it out properly. AOSP's Vendor_054c_Product_0ce6.kl:
Those two key rows disagree with Generic.kl (0x133→BUTTON_X, 0x134→BUTTON_Y), so PadButtons.correct's keyCode != genericKeyCode(scan) guard blocks SONY_MODERN on any device carrying the layout. And where the layout is absent, the C/Z signal is genuinely load-bearing for a Sony pad, in a way it never was for an Xbox one: hid-playstation's presence moves the bitmap and the report order together. Driver present → no BTN_C/BTN_Z → SONY_MODERN, which is the right table. Driver absent → BTN_C/BTN_Z declared → GENERIC_SONY, also the right table. There is no combination where a DualSense gets a table it should not have.
That is also why Moonlight asks hasKeys(BUTTON_C) for Sony and the axes for Xbox, rather than one question for both: no driver sits behind an Xbox pad in either firmware, so C/Z cannot separate them there. This PR's gate lands on the half that needed it.
One fact this turned up that the fix rests on. Current Generic.kl has no GAS/BRAKE rows at all:
So for a current-firmware Xbox pad it is namedTriggers' FIRST clause that fires (LTRIGGER && RTRIGGER), not the GAS/BRAKE one. Older AOSP spelled them GAS/BRAKE, which the second clause covers. The check is correct on both, but only because it carries all three spellings — worth knowing before anyone trims it.
What remains open, and it is the G8+ rather than the DualSense. That report says swapped "across both Punktfunk and Steam", which reads two ways: either this client sent the wrong button and Steam on the host merely displayed it — same root cause, fixed here — or the G8+ genuinely reports out of order and Android mis-maps it everywhere, in which case the correction was RIGHT for that pad and the new gate suppresses it. If that pad names its triggers and reports out of order, this PR makes it worse. The Controllers screen's scan 0x… · RAW → CORRECTED line off the reporter's pad decides it, and is the same line the DualSense needs.
Correcting the reasoning in the "Not fixed here" section above — the conclusion survives, the argument for it did not.
**The argument was wrong.** It read: a Triangle that reaches neither the client UI nor the stream must be one `buttonBit` maps to nothing, and no branch of the correction produces that. That treats "not working at all" as evidence of a *dropped* button, and it isn't. A button displaced onto something with no visible effect in that context reads identically to a reporter — the north face button landing on LB is exactly what "Y is mapped to LB" describes in the Xbox report, and on a DualSense that button is Triangle. Displacement had to be ruled out on its own, not assumed away.
**Ruling it out properly.** AOSP's `Vendor_054c_Product_0ce6.kl`:
```
key 0x133 BUTTON_Y # BTN_NORTH = Triangle
key 0x134 BUTTON_X # BTN_WEST = Square
axis 0x02 LTRIGGER
axis 0x05 RTRIGGER
```
Those two key rows *disagree* with `Generic.kl` (`0x133`→BUTTON_X, `0x134`→BUTTON_Y), so `PadButtons.correct`'s `keyCode != genericKeyCode(scan)` guard blocks `SONY_MODERN` on any device carrying the layout. And where the layout is absent, the C/Z signal is genuinely load-bearing for a Sony pad, in a way it never was for an Xbox one: `hid-playstation`'s presence moves the bitmap and the report order together. Driver present → no BTN_C/BTN_Z → `SONY_MODERN`, which is the right table. Driver absent → BTN_C/BTN_Z declared → `GENERIC_SONY`, also the right table. There is no combination where a DualSense gets a table it should not have.
That is also why Moonlight asks `hasKeys(BUTTON_C)` for Sony and the axes for Xbox, rather than one question for both: no driver sits behind an Xbox pad in either firmware, so C/Z cannot separate them there. This PR's gate lands on the half that needed it.
**One fact this turned up that the fix rests on.** Current `Generic.kl` has no GAS/BRAKE rows at all:
```
axis 0x09 RTRIGGER # ABS_GAS
axis 0x0a LTRIGGER # ABS_BRAKE
```
So for a current-firmware Xbox pad it is `namedTriggers`' FIRST clause that fires (`LTRIGGER && RTRIGGER`), not the GAS/BRAKE one. Older AOSP spelled them GAS/BRAKE, which the second clause covers. The check is correct on both, but only because it carries all three spellings — worth knowing before anyone trims it.
**What remains open, and it is the G8+ rather than the DualSense.** That report says swapped "across both Punktfunk and Steam", which reads two ways: either this client sent the wrong button and Steam on the host merely displayed it — same root cause, fixed here — or the G8+ genuinely reports out of order and Android mis-maps it everywhere, in which case the correction was RIGHT for that pad and the new gate suppresses it. If that pad names its triggers *and* reports out of order, this PR makes it worse. The Controllers screen's `scan 0x… · RAW → CORRECTED` line off the reporter's pad decides it, and is the same line the DualSense needs.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the regression from #361-era commit
3b39710areported by two users on 2026-08-21.The reports
GameSir G8+ — "several inputs appear to be swapped or inverted".
Xbox Elite Series 2 ("Xbox Wireless Controller" over Bluetooth) — all correct except:
Why
That table is not a stray mapping — it is precisely what
PadButtons.GENERIC_XBOXdoes to scancodes0x133/0x134/0x136/0x137. The correction added yesterday was firing on pads whose buttons already sit whereGeneric.klsays they do, and every scancode outside its table passed through untouched, which is the "all correct except" part.It fired because it asked a question that cannot answer this.
hasKeys(BUTTON_C, BUTTON_Z)reports what a device declares, not what it reports:hid-inputallocatesBTN_A + nstraight through for every button in the descriptor, so BTN_C (0x132) and BTN_Z (0x135) are set on any pad declaring six or more — a standard-layout pad that never presses either included.So the signal is identical on the pad that needs correcting and the pad that does not. It is literally the same model in both directions: an Elite Series 2 needed the correction on a Fire TV Stick, and another Elite Series 2 was broken by it here. No tightening of that check could have separated them.
The fix
The axes separate them. A HID gamepad describes its triggers either as the Accelerator/Brake usages — which become
ABS_GAS/ABS_BRAKE, names Android has words for — or as two more generic axes onABS_Z/ABS_RZ, which it does not. A descriptor well-formed enough to name its triggers puts its buttons at the standard positions too. It is also the firmware line on the exact pad in the report: an Xbox Wireless Controller over Bluetooth reports GAS/BRAKE after its firmware update and Z/Rz before it, and only the older one was ever wrong.padButtonsnow takesnamedTriggersand answersNATIVEwhenever it is set — no correction of any kind, on buttons or on axes, for a pad Android already reads correctly.padMapwas computing that fact one line below and spending it only on the axes; it now decides both.hasKeysstays for the narrower question it can answer — which straight-through order, once the axes have established there is one — where a false positive costs nothing.Moonlight decides it on the same fact (
ControllerHandler,gasRange == nullbeside the"Xbox Wireless Controller"name).3b39710acited Moonlight's tables and then replaced its discriminator withhasKeys, which is where this came in.The original Fire TV pads are unaffected: neither names its triggers, so both still get their correction.
Verified
:kit:testDebugUnitTestand:app:testDebugUnitTestgreen (16 cases inPadButtonsTest, 3 new: the gate holds for every vendor/declaration combination, the four reported buttons stay themselves, and the report-order choice past the gate is unchanged).:app:compileDebugKotlinclean. Not yet tested on glass.Not fixed here
The DualSense report filed alongside these — Triangle dead in the client UI and in the stream — is not explained by this change and is not addressed by it. A button that reaches neither is one
buttonBitmaps to nothing, and no branch of the correction produces that for Triangle. The Controllers screen prints the raw scancode and keycode of every press (<pad>: scan 0x… · RAW → CORRECTED); that one line, pressing Triangle on the reporter's pad, pins it.Two field reports (2026-08-21), one shape: a GameSir G8+ and an Xbox Elite Series 2 ("Xbox Wireless Controller" over Bluetooth) with X answering Y, Y answering LB, and the two shoulders answering menu buttons — everything else correct. That is not a stray mapping, it is exactly what `GENERIC_XBOX` does to scancodes `0x133`/`0x134`/`0x136`/`0x137`, so the correction added yesterday was firing on pads whose buttons were already where `Generic.kl` says they are. It fired because it asked the wrong question. `hasKeys(BUTTON_C, BUTTON_Z)` answers for what a device DECLARES, not what it reports: `hid-input` allocates `BTN_A + n` straight through for every button in the descriptor, so BTN_C (`0x132`) and BTN_Z (`0x135`) are set on ANY pad declaring six or more — including a standard-layout pad that never presses either. The signal is therefore identical on the pad that needs correcting and the pad that does not, and no amount of tightening it could have separated them. It is the same pad model in both reports: an Elite Series 2 needed the correction on a Fire TV and another Elite Series 2 was broken by it here. What does separate them is the axes. A HID gamepad describes its triggers either as the Accelerator/Brake usages — which become `ABS_GAS`/`ABS_BRAKE`, names Android has words for — or as two more generic axes on `ABS_Z`/`ABS_RZ`, which it does not. A descriptor well-formed enough to name its triggers puts its buttons at the standard positions too. It is also the firmware line on the pad in the report: an Xbox Wireless Controller over Bluetooth reports GAS/BRAKE after its firmware update and Z/Rz before it, and only the older one was ever wrong. `padButtons` now takes `namedTriggers` and answers NATIVE whenever it is set — no correction of any kind, on buttons or axes, for a pad Android already reads. `padMap` computed that fact one line below and only ever spent it on the axes; it now decides both. `hasKeys` stays for the narrower question it can answer — WHICH straight-through order, once the axes have established there is one — where a false positive costs nothing. Moonlight decides it on the same fact (`ControllerHandler`, `gasRange == null` beside the `"Xbox Wireless Controller"` name); yesterday's commit cited its tables and then replaced its discriminator, which is where this came in. Verified: `:kit:testDebugUnitTest` and `:app:testDebugUnitTest` green (16 cases in PadButtonsTest, 3 new: the gate holds for every vendor/declaration combination, the four reported buttons stay themselves, and the report-order choice past the gate is unchanged), `:app:compileDebugKotlin` clean. The DualSense report filed alongside these — Triangle dead in the client UI and in the stream — is NOT explained by this and is not fixed here: a button that reaches neither is one `buttonBit` maps to nothing, which no branch of the correction produces for Triangle. The Controllers screen prints the raw scancode and keycode of every press; that line off the reporter's pad will pin it.Correcting the reasoning in the "Not fixed here" section above — the conclusion survives, the argument for it did not.
The argument was wrong. It read: a Triangle that reaches neither the client UI nor the stream must be one
buttonBitmaps to nothing, and no branch of the correction produces that. That treats "not working at all" as evidence of a dropped button, and it isn't. A button displaced onto something with no visible effect in that context reads identically to a reporter — the north face button landing on LB is exactly what "Y is mapped to LB" describes in the Xbox report, and on a DualSense that button is Triangle. Displacement had to be ruled out on its own, not assumed away.Ruling it out properly. AOSP's
Vendor_054c_Product_0ce6.kl:Those two key rows disagree with
Generic.kl(0x133→BUTTON_X,0x134→BUTTON_Y), soPadButtons.correct'skeyCode != genericKeyCode(scan)guard blocksSONY_MODERNon any device carrying the layout. And where the layout is absent, the C/Z signal is genuinely load-bearing for a Sony pad, in a way it never was for an Xbox one:hid-playstation's presence moves the bitmap and the report order together. Driver present → no BTN_C/BTN_Z →SONY_MODERN, which is the right table. Driver absent → BTN_C/BTN_Z declared →GENERIC_SONY, also the right table. There is no combination where a DualSense gets a table it should not have.That is also why Moonlight asks
hasKeys(BUTTON_C)for Sony and the axes for Xbox, rather than one question for both: no driver sits behind an Xbox pad in either firmware, so C/Z cannot separate them there. This PR's gate lands on the half that needed it.One fact this turned up that the fix rests on. Current
Generic.klhas no GAS/BRAKE rows at all:So for a current-firmware Xbox pad it is
namedTriggers' FIRST clause that fires (LTRIGGER && RTRIGGER), not the GAS/BRAKE one. Older AOSP spelled them GAS/BRAKE, which the second clause covers. The check is correct on both, but only because it carries all three spellings — worth knowing before anyone trims it.What remains open, and it is the G8+ rather than the DualSense. That report says swapped "across both Punktfunk and Steam", which reads two ways: either this client sent the wrong button and Steam on the host merely displayed it — same root cause, fixed here — or the G8+ genuinely reports out of order and Android mis-maps it everywhere, in which case the correction was RIGHT for that pad and the new gate suppresses it. If that pad names its triggers and reports out of order, this PR makes it worse. The Controllers screen's
scan 0x… · RAW → CORRECTEDline off the reporter's pad decides it, and is the same line the DualSense needs.