[PATCH v2] HID: nintendo: relax subcommand limiter until cadence is proven

From: Mike Lothian

Date: Tue Sep 08 2026 - 19:43:47 EST


joycon_config_rumble() queues a zero-rumble packet during probe. The
rumble worker sends it once ctlr_state becomes READ, and
joycon_handle_rumble_report() retries it a few more times. This is the
first subcommand sent on every connection and it goes out before any
input report has been seen, so consecutive_valid_report_deltas is still
0. joycon_enforce_subcmd_rate_strict() needs 3 consecutive reports in
the 8-17ms window before it releases a subcommand, so the send exhausts
all 25 attempts and warns on every connection.

Add a subcmd_rate_unproven flag, set until the cadence has been
observed, and use the legacy flat-delay throttle while it is set.
joycon_parse_report() clears it after JC_SUBCMD_VALID_DELTA_REQ
consecutive reports at a valid cadence. USB is excluded, as its
consecutive_valid_report_deltas is forced to the requirement anyway.

The flag is separate from subcmd_rate_relaxed so that the exhaustion
fallback added by commit 781f8e020a78 ("HID: nintendo: fix rumble
starved by the input report cadence gate") keeps its semantics.
subcmd_rate_relaxed is written from the subcommand worker under
output_mutex, subcmd_rate_unproven only from joycon_parse_report().

Found with btmon over Bluetooth on a MediaTek mt7921e: the three
JC_SUBCMD_RATE_MAX_ATTEMPTS warnings on connect match three
JC_OUTPUT_RUMBLE_ONLY (0x10) frames from the rumble worker's
zero-countdown retries. Tested on a Pro Controller, which now connects
reliably, where before it dropped within the first ~70 seconds of a
fresh connection about half the time.

Fixes: d750d1480362 ("HID: nintendo: fix rumble rate limiter")
Signed-off-by: Mike Lothian <mike@xxxxxxxxxxxxxx>
Assisted-by: Claude:Opus-5 [Claude Code]
---
v2:
- Use a separate subcmd_rate_unproven flag rather than reusing
subcmd_rate_relaxed. In v1 joycon_parse_report() cleared
subcmd_rate_relaxed from softirq under ctlr->lock, while
joycon_enforce_subcmd_rate_strict() sets it from the subcommand
worker under output_mutex. Clearing it after the exhaustion counter
had passed JC_SUBCMD_RATE_MAX_FAILURES would stop the
++subcmd_rate_exhaustions == JC_SUBCMD_RATE_MAX_FAILURES test from
ever matching again, permanently disabling the fallback added by
781f8e020a78.
- Start USB controllers proven. consecutive_valid_report_deltas is
forced to JC_SUBCMD_VALID_DELTA_REQ for USB at the end of
joycon_parse_report(), so the v1 promotion could never run and USB
would have been left on the legacy throttle.
- v1 was assisted by Claude Sonnet 5, v2 by Claude Opus 5.

Both issues in v1 were spotted by the Sashiko AI review:
https://lore.kernel.org/linux-input/20260908225815.952CE1F00A3A@xxxxxxxxxxxxxxx/

That review also flagged a pre-existing issue: that the probe error
path does not set ctlr_state to JOYCON_CTLR_STATE_REMOVED, so a queued
rumble worker could run against stopped hardware during
destroy_workqueue(). As far as I can tell that is not reachable. The
worker can only be queued from joycon_handle_rumble_report(), which
runs via joycon_ctlr_read_handler() and is gated on ctlr_state ==
JOYCON_CTLR_STATE_READ, and from joycon_play_effect(), which needs the
input device registered. input_register_device() is the last call in
joycon_input_create() that can fail, and ctlr_state is set to READ
immediately after it returns, so there is no window in which the worker
is queued and probe can still fail.

v1: https://lore.kernel.org/linux-input/20260908224532.114357-1-mike@xxxxxxxxxxxxxx/

drivers/hid/hid-nintendo.c | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index 43e0f2aaea3b..5b3d97c0ad39 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -611,6 +611,7 @@ struct joycon_ctlr {
unsigned int consecutive_valid_report_deltas;
unsigned int subcmd_rate_exhaustions;
bool subcmd_rate_relaxed;
+ bool subcmd_rate_unproven;

/* factory calibration data */
struct joycon_stick_cal left_stick_cal_x;
@@ -917,7 +918,7 @@ static void joycon_enforce_subcmd_rate_legacy(struct joycon_ctlr *ctlr)

static void joycon_enforce_subcmd_rate(struct joycon_ctlr *ctlr)
{
- if (ctlr->subcmd_rate_relaxed)
+ if (ctlr->subcmd_rate_relaxed || READ_ONCE(ctlr->subcmd_rate_unproven))
joycon_enforce_subcmd_rate_legacy(ctlr);
else
joycon_enforce_subcmd_rate_strict(ctlr);
@@ -1797,8 +1798,11 @@ static void joycon_parse_report(struct joycon_ctlr *ctlr,
*/
if (report_delta_ms >= JC_INPUT_REPORT_MIN_DELTA &&
report_delta_ms <= JC_INPUT_REPORT_MAX_DELTA) {
- if (ctlr->consecutive_valid_report_deltas < JC_SUBCMD_VALID_DELTA_REQ)
+ if (ctlr->consecutive_valid_report_deltas < JC_SUBCMD_VALID_DELTA_REQ) {
ctlr->consecutive_valid_report_deltas++;
+ if (ctlr->consecutive_valid_report_deltas == JC_SUBCMD_VALID_DELTA_REQ)
+ WRITE_ONCE(ctlr->subcmd_rate_unproven, false);
+ }
} else {
ctlr->consecutive_valid_report_deltas = 0;
}
@@ -2730,6 +2734,7 @@ static int nintendo_hid_probe(struct hid_device *hdev,

ctlr->hdev = hdev;
ctlr->ctlr_state = JOYCON_CTLR_STATE_INIT;
+ ctlr->subcmd_rate_unproven = hdev->bus != BUS_USB;
ctlr->rumble_queue_head = 0;
ctlr->rumble_queue_tail = 0;
hid_set_drvdata(hdev, ctlr);
--
2.55.0