Skip to content

feat: Switch-layout button remap for PDP wired Switch pads (Standard quirk + Direct button order) - #159

Merged
emir-hasanbegovic merged 2 commits into
mainfrom
feat/switch-layout-hid-remap
Aug 17, 2026
Merged

feat: Switch-layout button remap for PDP wired Switch pads (Standard quirk + Direct button order)#159
emir-hasanbegovic merged 2 commits into
mainfrom
feat/switch-layout-hid-remap

Conversation

@emir-hasanbegovic

Copy link
Copy Markdown
Contributor

Problem

A PDP Faceoff Wired Pro Controller for Nintendo Switch (USB 0e6f:0180, hardware in hand) streams with its A button dead in Standard mode. Tracing the pad (live USB descriptor read plus the Android input chain) showed the whole button row is shifted; A is just the loudest casualty.

These pads declare 14 buttons in the Switch usage order: Y B A X L R ZL ZR Minus Plus L3 R3 Home Capture.

Standard: the kernel maps button usages sequentially from BTN_SOUTH, and Generic.kl turns that into BUTTON_A..BUTTON_THUMBL. Physical A therefore arrives as KEYCODE_BUTTON_C and physical R as KEYCODE_BUTTON_Z; keycodeToXusb() had no case for either, so both key gates dropped the events. Everything else landed one label off: Y acted as A, L as Y, ZL/ZR as bumpers, Minus/Plus as full-press triggers, stick clicks as Back/Start, Capture as L3, and Home vanished.

Direct: the pad classifies as GENERIC_HID_GAMEPAD and its descriptor parses fine, but buttonBit() assumes the western A B X Y order, which scrambles the pad differently (A acted as X, ZL/ZR as Back/Start, R3/Home/Capture dropped). So Direct was not a workaround either.

Fix

One Switch-order layout applied on both paths, positional to match decodeSwitchProUsb: south to A, east to B, west to X, north to Y, L/R to the bumpers, ZL/ZR to the triggers, Minus/Plus to Back/Start, L3/R3 to the stick clicks, Home to Guide. Capture stays unmapped because XUSB has no equivalent bit.

Standard path

  • New QUIRK_SWITCH_LAYOUT bit (0x04) in gamepad_input.h and GamepadQuirks.kt; resolveGamepadQuirk() now takes VID and PID and the registry pushes both.
  • applyKey() branches on the quirk into switchLayoutKeycodeToXusb(); ZL/ZR (arriving as BUTTON_L1/R1) drive the triggers through the existing ltFromKey/rtFromKey latch so a zero axis sample cannot clear a held trigger.
  • Both key gates (gamepadKeyFilter and processGamepadKeyEvent) now look the device quirk up under g_devicesMtx before deciding whether a keycode is consumable, so BUTTON_C/BUTTON_Z/BUTTON_MODE are consumed only for Switch-layout devices. Non-quirked devices keep the exact previous gate behavior, including the pre-IME gate not special-casing BUTTON_7/8.

Direct path

  • KnownDevice and Classification carry a ButtonOrder (default WESTERN).
  • The attach path stamps HidLayout.switchOrderButtons after fetchHidLayout (the parse resets the struct, pinned by a test).
  • decodeFromLayout() uses switchOrderButtonBit() when flagged, with button indices 6/7 folding into bLT/bRT.

Model catalog

Five PDP models join kImported with Parser::GENERIC_HID_GAMEPAD and ButtonOrder::SWITCH: 0180 Faceoff Wired Pro, 0181 Faceoff Deluxe, 0184 Faceoff Deluxe+ Audio, 0185 Wired Fight Pad Pro, 0187 Rock Candy. They get card names and stay off the verified fast lane, so nothing auto-claims into Direct.

0e6f:0186 (Afterglow Wireless) is excluded on purpose: SDL classifies it as a Switch Pro protocol controller whose USB port is charge-only, so the wired Switch-order remap would be wrong for it. Source: SDL controller_list.h.

The model set intentionally lives in two places (Kotlin quirk table for the framework path, kImported for the direct path); each side pins its five PIDs in tests, and the exclusion of 0186 is tested on both sides too.

Tests

Current-state pins (documenting the bug and the assumptions the fix rests on):

  • keycodeToXusb drops BUTTON_C/BUTTON_Z/BUTTON_MODE; applyKey without the quirk ignores them.
  • A byte-accurate Faceoff descriptor fixture parses to the expected field map (buttons at bit 0, count 14, hat at bit 16, X/Y/Z/Rz bytes), and western decode of that shape reproduces the scramble (A lands on X, ZL on Back, upper row vanishes).
  • Classification fallbacks and PDP's Xbox pads stay WESTERN; PDP Switch pads are not verified fast lane.

New behavior:

  • Full truth tables for switchLayoutKeycodeToXusb and switchOrderButtonBit (face row, bumpers, Minus/Plus, stick clicks, Home, dpad, Capture unmapped).
  • Trigger latching: ZL/ZR set and clear bLT/bRT on both paths, survive zero axis samples while held, and Minus/Plus do not touch the triggers.
  • Gate predicate coverage (switchLayoutConsumesKey), quirk-bit cross-language pin (0x04 asserted in both gtest and JUnit), stray swap bits do not double-remap.
  • End-to-end decodeReport with a Switch-order layout through ParserState.
  • Kotlin: the five PIDs resolve to QUIRK_SWITCH_LAYOUT, 0186 and PDP Xbox PIDs resolve to none, Nintendo vendor behavior unchanged.

Verification

  • Native host tests: 288/288 pass (:app:nativeTest).
  • :app:ktlintCheck, :app:detekt, GamepadQuirksTest green; full unit-test source set compiles.
  • clang-format 22.1.4 (CI pin) clean over app/src/main/cpp.
  • :app:externalNativeBuildDebug green on arm64-v8a and x86_64.
  • Hardware: the 0180 unit is on hand. Draft until the on-device pass is done: Standard (A/B/X/Y positional, ZL/ZR as triggers, Minus/Plus as Back/Start, Home as Guide) and Direct (same table through the descriptor path), plus a regression check on an Xbox pad and a Switch Pro.

Notes for review

  • Positional mapping (not label) was chosen to match the repo's existing Nintendo convention (resolveGamepadQuirk comment, decodeSwitchProUsb). If label mapping is preferred for these pads, it is a two-table change.
  • The key gates now take g_devicesMtx slightly earlier (to read the quirk) and hold it across the same work as before; no new lock ordering.
  • Capture is consumed nowhere and mapped nowhere; if the wire ever grows a Share bit it slots into both tables.

@emir-hasanbegovic
emir-hasanbegovic marked this pull request as ready for review August 16, 2026 21:00
…mily)

PDP's wired Switch controllers (0e6f:0180/0181/0184/0185/0187) declare
their buttons in the Switch usage order (Y B A X L R ZL ZR Minus Plus
L3 R3 Home Capture). On Standard, Generic.kl hands that row over as
BUTTON_A..BUTTON_THUMBL, so physical A arrived as BUTTON_C and was
dropped, R arrived as BUTTON_Z and was dropped, and every other button
landed one label off. On Direct, the generic HID decoder assumed the
western A B X Y order and scrambled the pad differently.

Standard path: a QUIRK_SWITCH_LAYOUT bit, resolved per VID:PID and
pushed through the existing quirk plumbing, swaps in a positional
keycode table matching decodeSwitchProUsb. ZL/ZR drive the triggers
through the ltFromKey/rtFromKey latch, and both key gates consult the
device quirk before consuming an event.

Direct path: classifyDevice now carries a ButtonOrder from the model
catalog, the attach path stamps HidLayout.switchOrderButtons after the
descriptor fetch, and decodeFromLayout applies the Switch-order button
table with ZL/ZR folding into the trigger bytes.

The five models join kImported so they get names while staying off the
verified fast lane. 0e6f:0186 (Afterglow Wireless) is excluded on
purpose: SDL classifies it as a Switch Pro protocol device whose USB
port is charge-only. Hardware-verified on a Faceoff Wired Pro
(0e6f:0180); the siblings follow SDL's SwitchInputOnlyController list.
…o the 1.0.2 notes

CHANGELOG.md now reads for gamers instead of developers: the stale
Unreleased backlog folds into the 1.0.1 initial-release entry (tagged
2026-07-25), Unreleased carries only what actually landed since then
(Steam Controller support and the PDP wired Switch pad fix), and the
wire-coordination marker becomes a plain "update Satellite too" note.

The 10002 Play notes in all five locales gain the PDP fix and stay
under Play's 500-character limit; check_play_metadata.py reports no
errors.
@emir-hasanbegovic
emir-hasanbegovic force-pushed the feat/switch-layout-hid-remap branch from e5a1320 to 7986fc4 Compare August 16, 2026 21:04
@emir-hasanbegovic
emir-hasanbegovic merged commit 08cccd8 into main Aug 17, 2026
9 checks passed
@emir-hasanbegovic
emir-hasanbegovic deleted the feat/switch-layout-hid-remap branch August 17, 2026 02:06
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.

1 participant