docs(gamepad/android): document the two-motor vibratorIds ordering assumption (G20)

The two-motor split assumes ids[0] = light/right and ids[1] = heavy/left, an
ordering `VibratorManager.getVibratorIds()` does not guarantee. Record the
assumption and its tactile-only failure mode (a heavy-first pad inverts the feel
but nothing silences or crashes) at the call site. No behavior change: a per-pad
fix needs on-glass verification, and a blanket count-based fallback is unsafe
(extra ids may be DualSense trigger actuators that must stay silent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:05:13 +02:00
parent aedffc69dd
commit 60af4de3ba
@@ -214,7 +214,13 @@ class GamepadFeedback(private val handle: Long, private val router: GamepadRoute
} }
val combo = CombinedVibration.startParallel() val combo = CombinedVibration.startParallel()
if (bind.amplitudeControlled && bind.ids.size >= 2) { if (bind.amplitudeControlled && bind.ids.size >= 2) {
// ids[0] = light/right, ids[1] = heavy/left (XInput/Moonlight convention). // Two-motor split — ASSUMPTION: ids[0] = light/right, ids[1] = heavy/left
// (XInput/Moonlight convention). Android does not guarantee the order of
// VibratorManager.getVibratorIds(), so a pad that enumerates heavy-first would
// invert the feel: the stronger amplitude drives the physically-lighter motor.
// Failure mode is tactile only — both motors still fire, nothing silences or
// crashes — so this stays the default pending per-pad on-glass verification (G20).
// ids beyond the first two (rare) are left alone here.
if (hi != 0) combo.addVibrator(bind.ids[0], oneShot(hi, durationMs)) if (hi != 0) combo.addVibrator(bind.ids[0], oneShot(hi, durationMs))
if (lo != 0) combo.addVibrator(bind.ids[1], oneShot(lo, durationMs)) if (lo != 0) combo.addVibrator(bind.ids[1], oneShot(lo, durationMs))
} else { } else {