From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=91.218.175.251 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793468546; b=yHuAAlNaSbKO5p7w3Gkf3rGqoRKlNmVd0gaDUUp5rTFkpbPNBNEaEDlKVWvYtqDTTer1 Rfib6TqfrS08CZ16+jeuHSrucw5FwrgZujIqmofLApLvvqRlUP3FRPa21c5sR68B0TkgI 5KZt/8mZXQvTScxJHVshgETb5Bkl5HB1q54FXXGu/IoEhl+mMSK4XzoqVKuEKmKvQXx2F d9fq8hREI/OPRAGzwq5CSFC8NtSSGML5D9/wMlPhXSBG0xbX6a1vloM5adJDewJIxHdW4 bgysqOD9ijXd2ynVn9P35vkXEjxl4PH2TCCtASh8JQS2bwmneNZnNlVQ/ofOO/UhDMA== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793468546; h=DMARC-Filter:X-Envelope-To:DKIM-Signature:X-Envelope-To:Received: X-Mizu-Trace-ID:X-Migadu-Flow:From:To:Cc:Subject:Date:Message-ID: X-Mailer:In-Reply-To:References:MIME-Version: Content-Transfer-Encoding; bh=xxmaITRQH8Q7deYKeJ3NFrKmDqyESI9iXE1osMNp+as=; b=yabq1+xs6jJvY3rWDCLqon0PFY2QWfJn8wrvIdOHMqMCVxvLNpQ9mzTuIic2bh1Xvx6J MU6sq738BZh6alKnWYfD37WnZNaaGO21eu0TYh+ADt13e0vVZquK1yit0anIPeS5QyZqc Mn7dcFUx0Z26hG/463EUBG80f99+SOeS1FloYA3JW0nR6X1eoU7S5mk1QcgfKs7ncbhqA xzC2IocfvIL9YZ+4fJV79/oqc7JASVRtaG3WYZAMz5JQE06wK5HPp23HAs03AWq4wjV52 IuW0wz2k72iOU2yP4B14ubcubVz74AEKbCT31iDThyPjiBPudGPz0DfTXrW8jMo0pEQ== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass header.d=linux.dev header.i=@linux.dev header.a=rsa-sha256 header.s=key1 header.b=Ox0G45Os; arc=none smtp.remote-ip=91.218.175.251 Received: from mta0.migadu.com (out-251.mta0.migadu.com [91.218.175.251]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 933D3606A31A for ; Sat, 31 Oct 2026 17:42:25 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org 933D3606A31A Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key, unprotected) header.d=linux.dev header.i=@linux.dev header.a=rsa-sha256 header.s=key1 header.b=Ox0G45Os X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=JCKHDaCESg53+cPASIvdw6p9ZmpDYOClpI0j4hdgpC4=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1793468544; v=1; x=1794073344; b=Ox0G45OsmNOCY+4zmsT4eOAhU+/wcih9ob26kRUxqlH5Jn03uDI2rsuNrPF4dA/1Jzp5cJ3I 6aH+LS2oDxGmmY50uHhspPLAKkTobVYngO8yabctaY0DyrsXFASv5KyxP5TmOV/UwC863flxWSO /414kfIuUl0/CiSPZfnldKhg= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id da194f08de04d5f6; Sat, 31 Oct 2026 17:42:23 +0000 X-Mizu-Trace-ID: da194f08de04d5f6 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: platform-driver-x86@vger.kernel.org, linux-input@vger.kernel.org, kernel@lists.opengamingcollective.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , "Joshua Leivenzon" , "Nathan Chancellor" , =?UTF-8?q?Rebecca=20Mara=20M=C3=BCller?= , "Ivan Levchenko" , "Corentin Chary" , "Hans de Goede" , =?UTF-8?q?Ilpo=20J=C3=A4rvinen?= , Denis Benato Subject: [PATCH v12 09/17] HID: asus: add support to force feedback Date: Sat, 31 Oct 2026 17:41:22 +0000 Message-ID: <20261031174130.208240-10-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261031174130.208240-1-denis.benato@linux.dev> References: <20261031174130.208240-1-denis.benato@linux.dev> X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Unlike ROG ally the X version and following ones uses DInput protocol and the force feedback needs to be implemented as its protocol is vendor-specific, therefore add support for FF_RUMBLE with magnitude scaling on a work-queue based approach to avoid using possibly sleeping calls in atomic context. Signed-off-by: Denis Benato Signed-off-by: Khamunetri Clark Co-developed-by: Luke Jones Signed-off-by: Luke Jones --- drivers/hid/Kconfig | 1 + drivers/hid/hid-asus.c | 143 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 2481bc01c435..dd411f6eff4f 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -189,6 +189,7 @@ config HID_ASUS depends on USB_HID depends on LEDS_CLASS depends on ASUS_WMI || ASUS_WMI=n + select INPUT_FF_MEMLESS select POWER_SUPPLY help Support for Asus notebook built-in keyboard and touchpad via i2c, and diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 783c78160277..3ade3dbda72e 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -210,6 +212,23 @@ struct asus_touchpad_info { int report_size; }; +/* Output report 0x0D on the gamepad interface. */ +struct ally_ff_data { + u8 enable; + u8 magnitude_left; + u8 magnitude_right; + u8 magnitude_strong; + u8 magnitude_weak; + u8 pulse_sustain_10ms; + u8 pulse_release_10ms; + u8 loop_count; +} __packed; + +struct ally_ff_report { + u8 report_id; + struct ally_ff_data ff; +} __packed; + struct ally_intf { struct hid_device *hdev; /* HID-core evdev of this interface. Reports for it land here. */ @@ -219,6 +238,14 @@ struct ally_intf { u8 cad_state; unsigned long cad_last_event_time; struct delayed_work resume_work; + struct { + /* Guards live, update, and packet. */ + spinlock_t lock; + bool live; + bool update; + struct ally_ff_report packet; + struct work_struct work; + } ff; }; struct asus_drvdata { @@ -325,10 +352,15 @@ enum ally_command_codes { CMD_SET_ANTI_DEADZONE = 0x18, }; +/* Rumble magnitudes use the hardware's 0..100 intensity range. */ +#define ALLY_FF_MAX_INTENSITY 100 + static const u8 ally_force_feedback_off[] = { 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB }; +static_assert(sizeof(struct ally_ff_report) == sizeof(ally_force_feedback_off)); + /* * Each Ally interface owns its own ally_intf. A report is delivered on the * evdev of the interface that received it. Nothing here points at another @@ -2184,6 +2216,84 @@ static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev, return input_dev; } +static int ally_ff_output(struct hid_device *hdev, const void *buf, size_t len) +{ + u8 *dmabuf __free(kfree) = kmemdup(buf, len, GFP_KERNEL); + + if (!dmabuf) + return -ENOMEM; + + return hid_hw_output_report(hdev, dmabuf, len); +} + +static int ally_ff_off(struct hid_device *hdev) +{ + return ally_ff_output(hdev, ally_force_feedback_off, + sizeof(ally_force_feedback_off)); +} + +static void ally_ff_work_fn(struct work_struct *work) +{ + struct ally_intf *intf = container_of(work, struct ally_intf, ff.work); + struct ally_ff_report report; + bool update = false; + int ret; + + scoped_guard(spinlock_irqsave, &intf->ff.lock) { + if (!intf->ff.update) + break; + + report = intf->ff.packet; + intf->ff.update = false; + update = true; + } + + if (!update) + return; + + ret = ally_ff_output(intf->hdev, &report, sizeof(report)); + if (ret < 0) + hid_err(intf->hdev, "Failed to send force-feedback: %d\n", ret); +} + +static int ally_ff_play(struct input_dev *idev, void *data, struct ff_effect *effect) +{ + struct ally_intf *intf = input_get_drvdata(idev); + + if (effect->type != FF_RUMBLE) + return 0; + + guard(spinlock_irqsave)(&intf->ff.lock); + + if (!intf->ff.live) + return 0; + + intf->ff.packet.ff.magnitude_strong = + effect->u.rumble.strong_magnitude * ALLY_FF_MAX_INTENSITY / U16_MAX; + intf->ff.packet.ff.magnitude_weak = + effect->u.rumble.weak_magnitude * ALLY_FF_MAX_INTENSITY / U16_MAX; + intf->ff.update = true; + schedule_work(&intf->ff.work); + return 0; +} + +static void ally_ff_quiesce(struct ally_intf *intf) +{ + int ret; + + if (!intf || !intf->gamepad) + return; + + scoped_guard(spinlock_irqsave, &intf->ff.lock) + intf->ff.live = false; + + cancel_work_sync(&intf->ff.work); + + ret = ally_ff_off(intf->hdev); + if (ret < 0) + hid_warn(intf->hdev, "Failed to stop force-feedback\n"); +} + static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf) { struct input_dev *input = ally_x_alloc_input_dev(hdev, intf); @@ -2212,8 +2322,24 @@ static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf) input_set_capability(input, EV_KEY, BTN_THUMBL); input_set_capability(input, EV_KEY, BTN_THUMBR); + spin_lock_init(&intf->ff.lock); + INIT_WORK(&intf->ff.work, ally_ff_work_fn); + memcpy(&intf->ff.packet, ally_force_feedback_off, sizeof(intf->ff.packet)); + intf->ff.update = false; + intf->ff.live = false; + + input_set_capability(input, EV_FF, FF_RUMBLE); + ret = input_ff_create_memless(input, NULL, ally_ff_play); + if (ret) { + hid_err(hdev, "Failed to create force-feedback: %d\n", ret); + input_free_device(input); + return ret; + } + + intf->ff.live = true; ret = input_register_device(input); if (ret) { + intf->ff.live = false; hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret); input_free_device(input); return ret; @@ -2404,6 +2530,14 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev) hid_err(hdev, "Failed to create Ally X gamepad device.\n"); return ERR_PTR(ret); } + + /* + * This is the interface that owns the rumble output report. + * Failure to silence it is not fatal. + */ + ret = ally_ff_off(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to disable force-feedback: %d\n", ret); break; case HID_ALLY_INTF_KEYBOARD_IN: break; @@ -2426,6 +2560,13 @@ static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf return 0; ep = ally_get_endpoint_address(hdev); + if (ep == HID_ALLY_X_INTF_IN) { + ret = ally_ff_off(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to disable force-feedback: %d\n", ret); + return 0; + } + if (ep != HID_ALLY_INTF_CFG_IN) return 0; @@ -3914,6 +4055,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { ally_quiesce(drvdata->ally); ally_sysfs_remove(hdev); + ally_ff_quiesce(drvdata->ally); } asus_worker_stop(drvdata->worker); @@ -3937,6 +4079,7 @@ static void asus_remove(struct hid_device *hdev) if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { ally_quiesce(drvdata->ally); ally_sysfs_remove(hdev); + ally_ff_quiesce(drvdata->ally); } asus_worker_stop(drvdata->worker); -- 2.47.3