Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package xpadneo for openSUSE:Factory checked in at 2026-04-01 19:51:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/xpadneo (Old) and /work/SRC/openSUSE:Factory/.xpadneo.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "xpadneo" Wed Apr 1 19:51:25 2026 rev:3 rq:1343916 version:0.10.1 Changes: -------- --- /work/SRC/openSUSE:Factory/xpadneo/xpadneo.changes 2026-03-08 17:43:03.326237387 +0100 +++ /work/SRC/openSUSE:Factory/.xpadneo.new.21863/xpadneo.changes 2026-04-01 19:52:19.785368051 +0200 @@ -1,0 +2,22 @@ +Tue Mar 31 13:11:58 UTC 2026 - Yunhe Guo <[email protected]> + +- Update to 0.10.1 + * xpadneo, licensing: Link the licenses for convenience + * xpadneo, rumble: Remove rumble accumulation + * xpadneo, rumble: Rename private function `xpadneo_rumble_welcome` + * xpadneo, rumble: Introduce helper to calculate throttling delay + * xpadneo, rumble: Migrate to more modern scoped locks + * xpadneo, docs: Replace back ticks syntax for shell substitution + * xpadneo, rumble: Tighten the rumble throttle timing + * xpadneo, docs: Fix style of `disable_shift_mode` + * xpadneo, power: Fix header export signature + * xpadneo, debug: Add new debug mode to improve logging for new devices + * xpadneo, debug: Move HID report debugger to new debug module + * xpadneo, quirks: Add OUI flag checks for new GameSir Nova + * xpadneo, debug: Add Xbox Wireless Controller modern descriptor + * xpadneo, debug: Add Xbox Elite 2 Controller descriptor + * xpadneo, mouse: Allow disabling the mouse device completely + * xpadneo, rumble: Do not send rumble commands unless ready + * xpadneo, rumble: Properly save and restore IRQ state during locking + +------------------------------------------------------------------- Old: ---- xpadneo-0.10.tar.gz New: ---- xpadneo-0.10.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ xpadneo.spec ++++++ --- /var/tmp/diff_new_pack.p5H6Vy/_old 2026-04-01 19:52:20.601402054 +0200 +++ /var/tmp/diff_new_pack.p5H6Vy/_new 2026-04-01 19:52:20.605402220 +0200 @@ -18,7 +18,7 @@ Name: xpadneo -Version: 0.10 +Version: 0.10.1 Release: 0 Summary: Driver for Xbox Wireless Controller License: GPL-3.0-only ++++++ xpadneo-0.10.tar.gz -> xpadneo-0.10.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/.github/ISSUE_TEMPLATE/bug_report.md new/xpadneo-0.10.1/.github/ISSUE_TEMPLATE/bug_report.md --- old/xpadneo-0.10/.github/ISSUE_TEMPLATE/bug_report.md 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/.github/ISSUE_TEMPLATE/bug_report.md 2026-03-25 02:59:22.000000000 +0100 @@ -110,7 +110,7 @@ <!-- Paste the output below the line prepended with # --> ```console -# xxd -c20 -g1 /sys/module/hid_xpadneo/drivers/hid:xpadneo/0005:045E:*/report_descriptor | tee >(cksum) +# dmesg -H | egrep -i 'bt|bluetooth|hci|l2cap|att|xbox|045e|hid|input|xpadneo' | tee xpadneo-dmesg.txt ``` @@ -128,8 +128,6 @@ - systemctl status bluetooth --no-pager --> -<!-- Run `dmesg -H | egrep -i 'bt|bluetooth|hci|l2cap|att|xbox|045e|hid|input|xpadneo' | tee xpadneo-dmesg.txt`. --> - <!-- Run `lsusb` and pick the device number of your dongle. --> <!-- Run `lsusb -v -s## | tee xpadneo-lsusb.txt` where `##` is the device diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/LICENSE.md new/xpadneo-0.10.1/LICENSE.md --- old/xpadneo-0.10/LICENSE.md 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/LICENSE.md 2026-03-25 02:59:22.000000000 +0100 @@ -21,8 +21,8 @@ The full license texts are provided in: -- `LICENSES/LICENSE.gpl-2.0.txt` -- `LICENSES/LICENSE.gpl-3.0.txt` +- [GPL-2.0-only](LICENSES/LICENSE.gpl-2.0.txt) +- [GPL-3.0-or-later](LICENSES/LICENSE.gpl-3.0.txt) ## File-level license identifiers (SPDX) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/NEWS.md new/xpadneo-0.10.1/NEWS.md --- old/xpadneo-0.10/NEWS.md 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/NEWS.md 2026-03-25 02:59:22.000000000 +0100 @@ -1,5 +1,55 @@ <!-- SPDX-License-Identifier: GPL-2.0-only --> +# Changes since v0.10 up to v0.10.1 + +This is a focused maintenance release for the v0.10 series with an emphasis on stability and compatibility. + +The main work in v0.10.1 targets rumble reliability. We now advertise rumble support early while deferring actual +rumble traffic until initialization is fully complete, reducing race conditions with userspace stacks that probe +capabilities immediately after hotplug. In addition, rumble locking was corrected to properly preserve IRQ state, +fixing a regression that could lead to hard lockups under force-feedback load. + +Beyond rumble, this update improves day-to-day usability and hardware coverage: the optional virtual mouse device can +now be disabled completely, quirk detection was extended for newer GameSir Nova variants, and several +documentation/debugging details were cleaned up. + +As a v0.10 maintenance release, this branch intentionally keeps the existing ff-memless-based architecture. The larger +native-rumble rework remains part of the v0.11 development line. + +Thanks to everyone who reported regressions, provided traces, and helped validate fixes on real hardware and different +userspace environments. + + +## Headlines: + + - xpadneo, docs: Replace back ticks syntax for shell substitution + - xpadneo, mouse: Allow disabling the mouse device completely + - xpadneo, quirks: Add OUI flag checks for new GameSir Nova + - xpadneo, rumble: Do not send rumble commands unless ready + - xpadneo, rumble: Properly save and restore IRQ state during locking + +``` +Kai Krakow (17): + xpadneo, licensing: Link the licenses for convenience + xpadneo, rumble: Remove rumble accumulation + xpadneo, rumble: Rename private function `xpadneo_rumble_welcome` + xpadneo, rumble: Introduce helper to calculate throttling delay + xpadneo, rumble: Migrate to more modern scoped locks + xpadneo, docs: Replace back ticks syntax for shell substitution + xpadneo, rumble: Tighten the rumble throttle timing + xpadneo, docs: Fix style of `disable_shift_mode` + xpadneo, power: Fix header export signature + xpadneo, debug: Add new debug mode to improve logging for new devices + xpadneo, debug: Move HID report debugger to new debug module + xpadneo, quirks: Add OUI flag checks for new GameSir Nova + xpadneo, debug: Add Xbox Wireless Controller modern descriptor + xpadneo, debug: Add Xbox Elite 2 Controller descriptor + xpadneo, mouse: Allow disabling the mouse device completely + xpadneo, rumble: Do not send rumble commands unless ready + xpadneo, rumble: Properly save and restore IRQ state during locking +``` + + # Changes since v0.9 up to v0.10 *Code name:* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/VERSION new/xpadneo-0.10.1/VERSION --- old/xpadneo-0.10/VERSION 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/VERSION 2026-03-25 02:59:22.000000000 +0100 @@ -1 +1 @@ -v0.10 +v0.10.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/azure-pipelines.yml new/xpadneo-0.10.1/azure-pipelines.yml --- old/xpadneo-0.10/azure-pipelines.yml 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/azure-pipelines.yml 2026-03-25 02:59:22.000000000 +0100 @@ -19,7 +19,7 @@ steps: - script: | sudo apt-get update - sudo apt-get install -y build-essential pkg-config linux-headers-`uname -r` + sudo apt-get install -y build-essential pkg-config linux-headers-$(uname -r) displayName: 'setup' - script: | sudo apt-get install -y dkms @@ -37,3 +37,4 @@ sudo apt-get install -y libncurses-dev make -C misc/examples/c_hidraw displayName: 'misc' +s \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/docs/CONFIGURATION.md new/xpadneo-0.10.1/docs/CONFIGURATION.md --- old/xpadneo-0.10/docs/CONFIGURATION.md 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/docs/CONFIGURATION.md 2026-03-25 02:59:22.000000000 +0100 @@ -38,10 +38,16 @@ - `128` if your controller uses motor-enable bits in reverse - `256` if your controller uses motor-enable bits with trigger and main motors swapped - `512` to avoid having your controller misdetected by heuristics (please report a bug) -- 'disable_shift_mode' (default 0) +- `disable_shift_mode` (default 0) - Let's you disable Xbox logo button shift behavior - '0' Xbox logo button will be used as shift - '1' will pass through the Xbox logo button as is +- `disable_mouse` (default 0) + - Let's you disable initialization of a mouse device through xpadneo, thus disabling mouse mode. + - '0' mouse device will be available + - '1' mouse device will be absent +- `debug_descriptor` (default 0) + - Enable debug logging for HID descriptor parsing Some settings may need to be changed at loading time of the module, take a look at the following example to see how that works: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/docs/README.md new/xpadneo-0.10.1/docs/README.md --- old/xpadneo-0.10/docs/README.md 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/docs/README.md 2026-03-25 02:59:22.000000000 +0100 @@ -302,7 +302,9 @@ - B for <key>Escape</key> **Important:** The mouse profile won't work if you disabled the shift-mode of the Xbox logo button (module parameter -`disable_shift_mode`). +`disable_shift_mode`). If you set the new `disable_mouse` module parameter to `1`, xpadneo will no longer register a +mouse device at all, so the Guide button combo will simply pass through to the desktop and no mouse emulation will be +started. ## Getting Started @@ -333,16 +335,16 @@ - On **Arch** and Arch-based distributions (like **EndeavourOS**), try `sudo pacman -S dkms linux-headers bluez bluez-utils` - On **Debian** based systems (like Ubuntu) you can install those packages by running - ``sudo apt-get install dkms linux-headers-`uname -r` `` + `sudo apt-get install dkms linux-headers-$(uname -r)` - On **Fedora**, it is - ``sudo dnf install dkms make bluez bluez-tools kernel-devel-`uname -r` kernel-headers `` + `sudo dnf install dkms make bluez bluez-tools kernel-devel-$(uname -r) kernel-headers` - On **Manjaro** try `sudo pacman -S dkms linux-latest-headers bluez bluez-utils` - On **openSUSE** (tested on Tumbleweed, should work for Leap), it is `sudo zypper install dkms make bluez kernel-devel kernel-source` - On **OSMC** you will have to run the following commands - ``sudo apt-get install dkms rbp2-headers-`uname -r`\`` - ``sudo ln -s "/usr/src/rbp2-headers-`uname -r`" "/lib/modules/`uname -r`/build"`` (as a [workaround](https://github.com/osmc/osmc/issues/471)) + `sudo apt-get install dkms rbp2-headers-$(uname -r)` + `sudo ln -s "/usr/src/rbp2-headers-$(uname -r)" "/lib/modules/$(uname -r)/build"` (as a [workaround](https://github.com/osmc/osmc/issues/471)) - On **Raspbian**, it is `sudo apt-get install dkms raspberrypi-kernel-headers` If you recently updated your firmware using `rpi-update` the above package may not yet include the header files for diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/docs/descriptors/gamesir_nova_2_lite.md new/xpadneo-0.10.1/docs/descriptors/gamesir_nova_2_lite.md --- old/xpadneo-0.10/docs/descriptors/gamesir_nova_2_lite.md 1970-01-01 01:00:00.000000000 +0100 +++ new/xpadneo-0.10.1/docs/descriptors/gamesir_nova_2_lite.md 2026-03-25 02:59:22.000000000 +0100 @@ -0,0 +1,30 @@ +<!-- SPDX-License-Identifier: GPL-3.0-or-later --> + +# HID Descriptor for GameSir Nova Lite 2 (Windows mode) + +Hex dump of the controller descriptor: +``` +xpadneo 0005:045E:02E0.000C: report descriptor: length=306 crc16=0x8BC5 +xpadneo 0005:045E:02E0.000C: report descriptor: OUI 1F:45:F3 (0x1F) - locally-administered (LAA), multicast +xpadneo 0005:045E:02E0.000C: report descriptor: dumping 306 unpatched bytes (crc16=0x8BC5) [LAA OUI] +xpadneo hid-desc: 00000000: 05 01 09 05 a1 01 85 01 09 01 a1 00 09 30 09 31 +xpadneo hid-desc: 00000010: 15 00 27 ff ff 00 00 95 02 75 10 81 02 c0 09 01 +xpadneo hid-desc: 00000020: a1 00 09 33 09 34 15 00 27 ff ff 00 00 95 02 75 +xpadneo hid-desc: 00000030: 10 81 02 c0 05 01 09 32 15 00 26 ff 03 95 01 75 +xpadneo hid-desc: 00000040: 0a 81 02 15 00 25 00 75 06 95 01 81 03 05 01 09 +xpadneo hid-desc: 00000050: 35 15 00 26 ff 03 95 01 75 0a 81 02 15 00 25 00 +xpadneo hid-desc: 00000060: 75 06 95 01 81 03 05 01 09 39 15 01 25 08 35 00 +xpadneo hid-desc: 00000070: 46 3b 01 66 14 00 75 04 95 01 81 42 75 04 95 01 +xpadneo hid-desc: 00000080: 15 00 25 00 35 00 45 00 65 00 81 03 05 09 19 01 +xpadneo hid-desc: 00000090: 29 0a 15 00 25 01 75 01 95 0a 81 02 15 00 25 00 +xpadneo hid-desc: 000000a0: 75 06 95 01 81 03 05 01 09 80 85 02 a1 00 09 85 +xpadneo hid-desc: 000000b0: 15 00 25 01 95 01 75 01 81 02 15 00 25 00 75 07 +xpadneo hid-desc: 000000c0: 95 01 81 03 c0 05 0f 09 21 85 03 a1 02 09 97 15 +xpadneo hid-desc: 000000d0: 00 25 01 75 04 95 01 91 02 15 00 25 00 75 04 95 +xpadneo hid-desc: 000000e0: 01 91 03 09 70 15 00 25 64 75 08 95 04 91 02 09 +xpadneo hid-desc: 000000f0: 50 66 01 10 55 0e 15 00 26 ff 00 75 08 95 01 91 +xpadneo hid-desc: 00000100: 02 09 a7 15 00 26 ff 00 75 08 95 01 91 02 65 00 +xpadneo hid-desc: 00000110: 55 00 09 7c 15 00 26 ff 00 75 08 95 01 91 02 c0 +xpadneo hid-desc: 00000120: 85 04 05 06 09 20 15 00 26 ff 00 75 08 95 01 81 +xpadneo hid-desc: 00000130: 02 c0 +``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/docs/heuristics/gamesir-nova.md new/xpadneo-0.10.1/docs/heuristics/gamesir-nova.md --- old/xpadneo-0.10/docs/heuristics/gamesir-nova.md 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/docs/heuristics/gamesir-nova.md 2026-03-25 02:59:22.000000000 +0100 @@ -8,14 +8,22 @@ We also collect OUIs here which should not match the heuristics because we are seeing conflicts with some official vendors. -Officially validated OUIs are marked with a "+". +The best indicators we currently have are the LLA or multicast bits in the MAC +address, one of which is always set. While the LLA bit is quite weak because +it is technically allowed, the multicast bit is much stronger because it should +never be used for controllers, and all other vendors follow that. -| OUI | Vendor | Bit representation | descriptor length +- Officially validated OUIs are flagged with a "+". +- LAA OUIs are flagged with an "L". +- Multicast OUIs are flagged with an "M". + +| OUI Flag | Vendor | Bit representation | descriptor length | ------------ | --------- | --------------------------------------- | ------------------- -| 3E:42:6C | GameSir | `0011 1110 : 0100 0010 : 0110 1100` | 283 -| ED:BC:9A | GameSir | `1110 1101 : 1011 1100 : 1001 1010` | 283 -| 6A:07:14 | GameSir | `0110 1010 : 0000 0111 : 0001 0100` | 283 -| **AND mask** | | **`0010 1000 : 0000 0000 : 0000 0000`** | **mask 0x28** +| 1F:45:F3 LM | GameSir | `0001 1111 : 0100 0101 : 1111 0011` | 306 +| 3E:42:6C L | GameSir | `0011 1110 : 0100 0010 : 0110 1100` | 283 +| ED:BC:9A M | GameSir | `1110 1101 : 1011 1100 : 1001 1010` | 283 +| 6A:07:14 L | GameSir | `0110 1010 : 0000 0111 : 0001 0100` | 283 +| **OR Mask** | | **`0000 0011 : 0000 0000 : 0000 0000` | **mask 0x03** | 3C:FA:06 + | Microsoft | `0011 1100 : 1111 1010 : 0000 0110` | 283 inval match | 44:16:22 + | Microsoft | `0100 0100 : 0001 0110 : 0010 0010` | | 68:6C:E6 + | Microsoft | `0110 1000 : 0110 1100 : 1110 0110` | 283 inval match diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/dkms.conf.in new/xpadneo-0.10.1/hid-xpadneo/dkms.conf.in --- old/xpadneo-0.10/hid-xpadneo/dkms.conf.in 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/dkms.conf.in 2026-03-25 02:59:22.000000000 +0100 @@ -11,7 +11,7 @@ MAKE[0]="make -C '${kernel_source_dir}' M='${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build/src' VERSION='${PACKAGE_VERSION}' modules" BUILD_EXCLUSIVE_KERNEL_MIN="5.12.0" -BUILD_EXCLUSIVE_CONFIG="CONFIG_HID CONFIG_INPUT_FF_MEMLESS CONFIG_POWER_SUPPLY" +BUILD_EXCLUSIVE_CONFIG="CONFIG_HID CONFIG_INPUT_FF_MEMLESS CONFIG_POWER_SUPPLY CONFIG_CRC16" AUTOINSTALL="yes" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/Makefile new/xpadneo-0.10.1/hid-xpadneo/src/Makefile --- old/xpadneo-0.10/hid-xpadneo/src/Makefile 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/Makefile 2026-03-25 02:59:22.000000000 +0100 @@ -7,6 +7,7 @@ hid-xpadneo-y += \ xpadneo/consumer.o \ xpadneo/core.o \ + xpadneo/debug.o \ xpadneo/device.o \ xpadneo/events.o \ xpadneo/keyboard.o \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/debug.c new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/debug.c --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/debug.c 1970-01-01 01:00:00.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/debug.c 2026-03-25 02:59:22.000000000 +0100 @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* + * xpadneo HID descriptor debug helpers + * + * Logs descriptor size and CRC-16 checksum unconditionally, and dumps the + * full unpatched HID descriptor (including OUI flags) automatically for + * locally-administered (LAA) addresses or when the debug_descriptor module + * parameter is set. + * + * Copyright (c) 2026 Kai Krakow <[email protected]> + */ + +#include <linux/crc16.h> +#include <linux/hid.h> +#include <linux/module.h> +#include <linux/printk.h> +#include <linux/string.h> +#include <linux/types.h> + +#include "xpadneo.h" + +static bool param_debug_descriptor; +module_param_named(debug_descriptor, param_debug_descriptor, bool, 0644); +MODULE_PARM_DESC(debug_descriptor, + "(bool) Dump unpatched HID descriptor to dmesg. 0: auto, 1: always."); + +static bool param_debug_hid; +module_param_named(debug_hid, param_debug_hid, bool, 0644); +MODULE_PARM_DESC(debug_hid, "(bool) Debug HID reports. 0: disable, 1: enable."); + +struct crc_name { + u16 crc16; + char *name; +}; + +const struct crc_name known_checksums[] = { + { .crc16 = 0x6500, .name = "Xbox One Elite Series 2" }, + { .crc16 = 0x8BC5, .name = "Xbox Wireless Controller (legacy)" }, + { .crc16 = 0x931D, .name = "Xbox Wireless Controller (modern)" }, +}; + +/* + * Parse the first three OUI bytes from a Bluetooth MAC address string of + * the form "aa:bb:cc:dd:ee:ff". Returns 0 on success, -EINVAL otherwise. + */ +static int parse_oui(const char *uniq, u8 *b0, u8 *b1, u8 *b2) +{ + char tmp[3] = { }; + + /* need at least "XX:XX:XX" (8 chars) with ':' separators */ + if (strnlen(uniq, 9) < 8 || uniq[2] != ':' || uniq[5] != ':') + return -EINVAL; + + memcpy(tmp, uniq + 0, 2); + if (kstrtou8(tmp, 16, b0)) + return -EINVAL; + + memcpy(tmp, uniq + 3, 2); + if (kstrtou8(tmp, 16, b1)) + return -EINVAL; + + memcpy(tmp, uniq + 6, 2); + if (kstrtou8(tmp, 16, b2)) + return -EINVAL; + + return 0; +} + +void xpadneo_debug_hid_report(struct hid_device *hdev, const struct xpadneo_rumble_report *r, + const size_t len) +{ + if (likely(!param_debug_hid)) + return; + + if (unlikely(len == 0)) { + hid_err(hdev, "HID debug: invalid length %zu\n", len); + return; + } + + switch (r->report_id) { + case 0x03: + if (unlikely(len != sizeof(*r))) { + hid_info(hdev, "HID debug: len %zu malformed cmd 0x%02x\n", len, + r->report_id); + } else { + hid_info(hdev, + "HID debug: rumble cmd 0x%02x " + "motors left %d right %d strong %d weak %d " + "magnitude left %d right %d strong %d weak %d " + "pulse sustain %dms release %dms loop %d\n", + r->report_id, + !!(r->data.enable & XBOX_RUMBLE_LEFT), + !!(r->data.enable & XBOX_RUMBLE_RIGHT), + !!(r->data.enable & XBOX_RUMBLE_STRONG), + !!(r->data.enable & XBOX_RUMBLE_WEAK), + r->data.magnitude_left, r->data.magnitude_right, + r->data.magnitude_strong, r->data.magnitude_weak, + r->data.pulse_sustain_10ms * 10, + r->data.pulse_release_10ms * 10, r->data.loop_count); + } + break; + default: + hid_info(hdev, "HID debug: unhandled cmd 0x%02x\n", r->report_id); + } +} + +/* + * xpadneo_debug_descriptor - log descriptor metadata and optionally hex-dump + * @hdev: HID device whose descriptor is being examined + * @rdesc: pointer to the *unpatched* report descriptor bytes + * @rsize: length of the descriptor in bytes + * + * Must be called at the very start of report_fixup, before any in-place + * modifications, so that the logged data reflects the device's original + * descriptor. + * + * Unconditionally emits one line containing the descriptor byte-length and + * CRC-16 checksum (CRC-16/IBM, poly 0x8005, init 0), plus one line + * describing the OUI flags. + * + * Additionally performs a full hex-dump when: + * - the module parameter debug_descriptor=1 is set, OR + * - the device's Bluetooth OUI is locally administered (LAA). + */ +void xpadneo_debug_descriptor(struct hid_device *hdev, const __u8 *rdesc, unsigned int rsize) +{ + u8 oui0 = 0, oui1 = 0, oui2 = 0; + u16 crc = crc16(0, rdesc, rsize); + bool oui_valid = (parse_oui(hdev->uniq, &oui0, &oui1, &oui2) == 0); + bool is_laa = !!(oui0 & XPADNEO_OUI_IS_LAA); + bool is_multicast = !!(oui0 & XPADNEO_OUI_IS_MULTICAST); + bool do_dump; + + hid_info(hdev, "report descriptor: length %u crc16 0x%04x\n", rsize, crc); + + if (oui_valid) { + hid_info(hdev, + "report descriptor: OUI %02X:%02X:%02X (0x%02X) - %s, %s\n", + oui0, oui1, oui2, oui0, + is_laa ? "locally-administered (LAA)" : "globally-assigned", + is_multicast ? "multicast" : "unicast"); + } else { + hid_info(hdev, "report descriptor: OUI unavailable (uniq='%.17s')\n", hdev->uniq); + } + + do_dump = param_debug_descriptor; + if (!do_dump) { + for (int i = 0; i < ARRAY_SIZE(known_checksums); i++) { + if (crc == known_checksums[i].crc16) { + hid_info(hdev, + "report descriptor: known checksum crc16 0x%04x name '%s'\n", + crc, known_checksums[i].name); + break; + } + } + } + + do_dump = param_debug_descriptor || (oui_valid && is_laa); + if (do_dump) { + hid_info(hdev, + "report descriptor: dumping %u unpatched bytes crc16 0x%04x%s\n", rsize, + crc, param_debug_descriptor ? " [forced]" : " [LAA OUI]"); + print_hex_dump(KERN_INFO, "xpadneo hid-desc: ", DUMP_PREFIX_OFFSET, 16, 1, rdesc, + rsize, false); + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/device.c new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/device.c --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/device.c 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/device.c 2026-03-25 02:59:22.000000000 +0100 @@ -10,41 +10,12 @@ #include "xpadneo.h" -static bool param_debug_hid; -module_param_named(debug_hid, param_debug_hid, bool, 0644); -MODULE_PARM_DESC(debug_hid, "(bool) Debug HID reports. 0: disable, 1: enable."); - int xpadneo_device_output_report(struct hid_device *hdev, __u8 *buf, size_t len) { struct xpadneo_rumble_report *r = (struct xpadneo_rumble_report *)buf; - if (unlikely(param_debug_hid && (len > 0))) { - switch (buf[0]) { - case 0x03: - if (len >= sizeof(*r)) { - hid_info(hdev, - "HID debug: len %zu rumble cmd 0x%02x " - "motors left %d right %d strong %d weak %d " - "magnitude left %d right %d strong %d weak %d " - "pulse sustain %dms release %dms loop %d\n", - len, r->report_id, - !!(r->data.enable & XBOX_RUMBLE_LEFT), - !!(r->data.enable & XBOX_RUMBLE_RIGHT), - !!(r->data.enable & XBOX_RUMBLE_STRONG), - !!(r->data.enable & XBOX_RUMBLE_WEAK), - r->data.magnitude_left, r->data.magnitude_right, - r->data.magnitude_strong, r->data.magnitude_weak, - r->data.pulse_sustain_10ms * 10, - r->data.pulse_release_10ms * 10, r->data.loop_count); - } else { - hid_info(hdev, "HID debug: len %zu malformed cmd 0x%02x\n", len, - buf[0]); - } - break; - default: - hid_info(hdev, "HID debug: len %zu unhandled cmd 0x%02x\n", len, buf[0]); - } - } + xpadneo_debug_hid_report(hdev, r, len); + return hid_hw_output_report(hdev, buf, len); } @@ -93,8 +64,11 @@ { struct xpadneo_devdata *xdata = hid_get_drvdata(hdev); + /* preserve the original descriptor size for post-parse quirk heuristics */ xdata->original_rsize = *rsize; - hid_info(hdev, "report descriptor size: %d bytes\n", *rsize); + + /* log size/CRC and optionally hex-dump before any in-place patches */ + xpadneo_debug_descriptor(hdev, rdesc, *rsize); /* fixup trailing NUL byte */ if (rdesc[*rsize - 2] == 0xC0 && rdesc[*rsize - 1] == 0x00) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/events.c new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/events.c --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/events.c 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/events.c 2026-03-25 02:59:22.000000000 +0100 @@ -253,8 +253,8 @@ } switch (usage->code) { case BTN_SELECT: - if (value == 1) - xpadneo_mouse_toggle(xdata); + if ((value == 1) && xpadneo_mouse_toggle(xdata)) + xdata->profile_switched = true; goto stop_processing; } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/mouse.c new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/mouse.c --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/mouse.c 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/mouse.c 2026-03-25 02:59:22.000000000 +0100 @@ -6,21 +6,29 @@ * Copyright (c) 2021 Kai Krakow <[email protected]> */ +#include <linux/module.h> + #include "xpadneo.h" /* always include last */ #include "compat.h" +static bool param_disable_mouse; +module_param_named(disable_mouse, param_disable_mouse, bool, 0444); +MODULE_PARM_DESC(disable_mouse, + "(bool) Disable mouse device permanently. 0: allow mouse, 1: disallow mouse."); + /* Mouse movement deadzone and trigger thresholds (raw values) */ #define XPADNEO_MOUSE_MOVEMENT_DEADZONE 3072 #define XPADNEO_TRIGGER_RELEASE_THRESHOLD 384 #define XPADNEO_TRIGGER_PRESS_THRESHOLD 640 -void xpadneo_mouse_toggle(struct xpadneo_devdata *xdata) +bool xpadneo_mouse_toggle(struct xpadneo_devdata *xdata) { if (!xdata->mouse.idev) { xdata->mouse_mode = false; hid_info(xdata->hdev, "mouse not available\n"); + return false; } else if (xdata->mouse_mode) { xdata->mouse_mode = false; hid_info(xdata->hdev, "mouse mode disabled\n"); @@ -30,7 +38,7 @@ } /* Indicate that a request was made */ - xdata->profile_switched = true; + return true; } #define mouse_report_rel(a,v) if((v)!=0)input_report_rel(mouse,(a),(v)) @@ -91,7 +99,7 @@ struct input_dev *consumer = xdata->consumer.idev; struct input_dev *keyboard = xdata->keyboard.idev; - if (!xdata->mouse_mode) + if (!xdata->mouse_mode || !xdata->mouse.idev) return 0; if (usage->type == EV_ABS) { @@ -203,6 +211,12 @@ { int ret; + if (param_disable_mouse) { + xdata->mouse.idev = NULL; + hid_info(xdata->hdev, "Mouse device disabled permanently.\n"); + return 0; + } + if (!xdata->mouse.idev) { ret = xpadneo_synthetic_init(xdata, "Mouse", &xdata->mouse); if (ret || !xdata->mouse.idev) @@ -240,16 +254,25 @@ void xpadneo_mouse_init_timer(struct xpadneo_devdata *xdata) { + if (param_disable_mouse) + return; + timer_setup(&xdata->mouse_timer, xpadneo_mouse_report, 0); mod_timer(&xdata->mouse_timer, jiffies); } void xpadneo_mouse_remove_timer(struct xpadneo_devdata *xdata) { + if (param_disable_mouse) + return; + timer_delete_sync(&xdata->mouse_timer); } void xpadneo_mouse_remove(struct xpadneo_devdata *xdata) { + if (param_disable_mouse) + return; + xpadneo_synthetic_remove(xdata, "mouse", &xdata->mouse); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/quirks.c new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/quirks.c --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/quirks.c 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/quirks.c 2026-03-25 02:59:22.000000000 +0100 @@ -41,24 +41,16 @@ }; /* MAC OUI masks */ -#define XPADNEO_OUI_MASK(oui, mask) (((oui)&(mask)) == (mask)) -#define XPADNEO_OUI_MASK_GAMESIR_NOVA 0x28 +#define XPADNEO_OUI_MASK_LAA_MULTICAST (XPADNEO_OUI_IS_MULTICAST | XPADNEO_OUI_IS_LAA) static const struct quirk quirks[] = { - DEVICE_OUI_QUIRK("28:EA:0B", XPADNEO_QUIRK_NO_HEURISTICS), - DEVICE_OUI_QUIRK("3C:FA:06", XPADNEO_QUIRK_NO_HEURISTICS), - DEVICE_OUI_QUIRK("68:6C:E6", XPADNEO_QUIRK_NO_HEURISTICS), - DEVICE_OUI_QUIRK("78:86:2E", XPADNEO_QUIRK_NO_HEURISTICS), DEVICE_OUI_QUIRK("98:B6:EA", XPADNEO_QUIRK_NO_PULSE | XPADNEO_QUIRK_NO_TRIGGER_RUMBLE | XPADNEO_QUIRK_REVERSE_MASK), DEVICE_OUI_QUIRK("98:B6:EC", XPADNEO_QUIRK_SIMPLE_CLONE | XPADNEO_QUIRK_SWAPPED_MASK), DEVICE_OUI_QUIRK("A0:5A:5D", XPADNEO_QUIRK_NO_HAPTICS), - DEVICE_OUI_QUIRK("A8:8C:3E", XPADNEO_QUIRK_NO_HEURISTICS), - DEVICE_OUI_QUIRK("AC:8E:BD", XPADNEO_QUIRK_NO_HEURISTICS), DEVICE_OUI_QUIRK("E4:17:D8", XPADNEO_QUIRK_SIMPLE_CLONE), - DEVICE_OUI_QUIRK("EC:83:50", XPADNEO_QUIRK_NO_HEURISTICS), }; int xpadneo_quirks_init(struct xpadneo_devdata *xdata) @@ -66,7 +58,7 @@ struct hid_device *hdev = xdata->hdev; struct input_dev *gamepad = xdata->gamepad.idev; u32 quirks_set = 0, quirks_unset = 0, quirks_override = U32_MAX; - u8 oui_byte; + u8 oui_byte = 0; char oui[3] = { }; for (int i = 0; i < ARRAY_SIZE(quirks); i++) { @@ -138,18 +130,25 @@ } /* + * Check whether we should enable heuristics checks at all, and then * copy the first two characters from the uniq ID (MAC address) and * expect it being too big to copy, then `kstrtou8()` converts the - * uniq ID "aa:bb:cc:dd:ee:ff" to u8, so we get the first OUI byte + * uniq ID "aa:bb:cc:dd:ee:ff" to u8, so we get the first OUI byte. */ - if ((xdata->original_rsize == 283) - && ((xdata->quirks & XPADNEO_QUIRK_NO_HEURISTICS) == 0) + if (((xdata->quirks & XPADNEO_QUIRK_NO_HEURISTICS) == 0) && ((xdata->quirks & XPADNEO_QUIRK_SIMPLE_CLONE) == 0) && (strscpy(oui, gamepad->uniq, sizeof(oui)) == -E2BIG) - && (kstrtou8(oui, 16, &oui_byte) == 0) - && XPADNEO_OUI_MASK(oui_byte, XPADNEO_OUI_MASK_GAMESIR_NOVA)) { - hid_info(hdev, "enabling heuristic GameSir Nova quirks\n"); - xdata->quirks |= XPADNEO_QUIRK_SIMPLE_CLONE; + && (kstrtou8(oui, 16, &oui_byte) == 0)) { + /* + * All known GameSir devices at least one of the LAA or + * multicast bits set, and a descriptor length of 283 or 306 + * bytes. + */ + if (((xdata->original_rsize == 283) || (xdata->original_rsize == 306)) + && ((oui_byte & XPADNEO_OUI_MASK_LAA_MULTICAST) > 0)) { + hid_info(hdev, "enabling heuristic GameSir quirks\n"); + xdata->quirks |= XPADNEO_QUIRK_SIMPLE_CLONE; + } } /* handle quirk flags which remove a behavior after heuristics */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/rumble.c new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/rumble.c --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/rumble.c 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/rumble.c 2026-03-25 02:59:22.000000000 +0100 @@ -9,12 +9,13 @@ #include <linux/delay.h> #include <linux/module.h> +#include <linux/smp.h> #include "xpadneo.h" #include "helpers.h" /* timing of rumble commands to work around firmware crashes */ -#define RUMBLE_THROTTLE_DELAY msecs_to_jiffies(50) +#define RUMBLE_THROTTLE_DELAY (msecs_to_jiffies(10)+1) #define RUMBLE_THROTTLE_JIFFIES (jiffies + RUMBLE_THROTTLE_DELAY) /* module parameter "trigger_rumble_mode" */ @@ -39,6 +40,21 @@ static struct workqueue_struct *rumble_wq; +static inline unsigned long calculate_throttling_delay(struct xpadneo_devdata *xdata) +{ + unsigned long rumble_run_at = jiffies, rumble_throttle_until = xdata->rumble.throttle_until; + + if (time_before(rumble_run_at, rumble_throttle_until)) { + /* last rumble was recently executed */ + long delay_work = (long)(rumble_throttle_until - rumble_run_at); + + return clamp(delay_work, 0L, (long)RUMBLE_THROTTLE_DELAY); + } + + /* the firmware is ready */ + return 0; +} + static void rumble_worker(struct work_struct *work) { struct xpadneo_devdata *xdata = @@ -46,7 +62,6 @@ struct hid_device *hdev = xdata->hdev; struct xpadneo_rumble_report *r = xdata->rumble.output_report_dmabuf; int ret; - unsigned long flags; memset(r, 0, sizeof(*r)); r->report_id = XPADNEO_XBOX_RUMBLE_REPORT; @@ -68,90 +83,94 @@ r->data.loop_count = 0xEB; } - spin_lock_irqsave(&xdata->rumble.lock, flags); + scoped_guard(spinlock_irqsave, &xdata->rumble.lock) { - /* let our scheduler know we've been called */ - xdata->rumble.scheduled = false; + /* let our scheduler know we've been called */ + xdata->rumble.scheduled = false; - if (unlikely(xdata->quirks & XPADNEO_QUIRK_NO_TRIGGER_RUMBLE)) { - /* do not send these bits if not supported */ - r->data.enable &= ~XBOX_RUMBLE_TRIGGERS; - } else { - /* trigger motors */ - r->data.magnitude_left = xdata->rumble.data.magnitude_left; - r->data.magnitude_right = xdata->rumble.data.magnitude_right; + /* only proceed once initialization data is globally visible */ + if (unlikely(!smp_load_acquire(&xdata->rumble.enabled))) + return; + + if (unlikely(xdata->quirks & XPADNEO_QUIRK_NO_TRIGGER_RUMBLE)) { + /* do not send these bits if not supported */ + r->data.enable &= ~XBOX_RUMBLE_TRIGGERS; + } else { + /* trigger motors */ + r->data.magnitude_left = xdata->rumble.data.magnitude_left; + r->data.magnitude_right = xdata->rumble.data.magnitude_right; + } + + /* main motors */ + r->data.magnitude_strong = xdata->rumble.data.magnitude_strong; + r->data.magnitude_weak = xdata->rumble.data.magnitude_weak; + + /* do not reprogram motors that have not changed */ + if (unlikely(xdata->rumble.shadow.magnitude_strong == r->data.magnitude_strong)) + r->data.enable &= ~XBOX_RUMBLE_STRONG; + if (unlikely(xdata->rumble.shadow.magnitude_weak == r->data.magnitude_weak)) + r->data.enable &= ~XBOX_RUMBLE_WEAK; + if (likely(xdata->rumble.shadow.magnitude_left == r->data.magnitude_left)) + r->data.enable &= ~XBOX_RUMBLE_LEFT; + if (likely(xdata->rumble.shadow.magnitude_right == r->data.magnitude_right)) + r->data.enable &= ~XBOX_RUMBLE_RIGHT; + + /* do not send a report if nothing changed */ + if (unlikely(r->data.enable == XBOX_RUMBLE_NONE)) + return; + + /* shadow our current rumble values for the next cycle */ + memcpy(&xdata->rumble.shadow, &xdata->rumble.data, sizeof(xdata->rumble.data)); + + /* set all bits if not supported (some clones require these set) */ + if (unlikely(xdata->quirks & XPADNEO_QUIRK_NO_MOTOR_MASK)) + r->data.enable = XBOX_RUMBLE_ALL; + + /* reverse the bits for trigger and main motors */ + if (unlikely(xdata->quirks & XPADNEO_QUIRK_REVERSE_MASK)) + r->data.enable = SWAP_BITS(SWAP_BITS(r->data.enable, 1, 2), 0, 3); + + /* swap the bits of trigger and main motors */ + if (unlikely(xdata->quirks & XPADNEO_QUIRK_SWAPPED_MASK)) + r->data.enable = SWAP_BITS(SWAP_BITS(r->data.enable, 0, 2), 1, 3); } - /* main motors */ - r->data.magnitude_strong = xdata->rumble.data.magnitude_strong; - r->data.magnitude_weak = xdata->rumble.data.magnitude_weak; - - /* do not reprogram motors that have not changed */ - if (unlikely(xdata->rumble.shadow.magnitude_strong == r->data.magnitude_strong)) - r->data.enable &= ~XBOX_RUMBLE_STRONG; - if (unlikely(xdata->rumble.shadow.magnitude_weak == r->data.magnitude_weak)) - r->data.enable &= ~XBOX_RUMBLE_WEAK; - if (likely(xdata->rumble.shadow.magnitude_left == r->data.magnitude_left)) - r->data.enable &= ~XBOX_RUMBLE_LEFT; - if (likely(xdata->rumble.shadow.magnitude_right == r->data.magnitude_right)) - r->data.enable &= ~XBOX_RUMBLE_RIGHT; - - /* do not send a report if nothing changed */ - if (unlikely(r->data.enable == XBOX_RUMBLE_NONE)) { - spin_unlock_irqrestore(&xdata->rumble.lock, flags); - return; - } - - /* shadow our current rumble values for the next cycle */ - memcpy(&xdata->rumble.shadow, &xdata->rumble.data, sizeof(xdata->rumble.data)); - - /* clear the magnitudes to properly accumulate the maximum values */ - xdata->rumble.data.magnitude_left = 0; - xdata->rumble.data.magnitude_right = 0; - xdata->rumble.data.magnitude_weak = 0; - xdata->rumble.data.magnitude_strong = 0; + ret = xpadneo_device_output_report(hdev, (__u8 *) r, sizeof(*r)); + if (ret < 0) + hid_warn(hdev, "failed to send FF report: %d\n", ret); /* * throttle next command submission, the firmware doesn't like us to * send rumble data any faster */ - xdata->rumble.throttle_until = RUMBLE_THROTTLE_JIFFIES; - - spin_unlock_irqrestore(&xdata->rumble.lock, flags); - - /* set all bits if not supported (some clones require these set) */ - if (unlikely(xdata->quirks & XPADNEO_QUIRK_NO_MOTOR_MASK)) - r->data.enable = XBOX_RUMBLE_ALL; - - /* reverse the bits for trigger and main motors */ - if (unlikely(xdata->quirks & XPADNEO_QUIRK_REVERSE_MASK)) - r->data.enable = SWAP_BITS(SWAP_BITS(r->data.enable, 1, 2), 0, 3); - - /* swap the bits of trigger and main motors */ - if (unlikely(xdata->quirks & XPADNEO_QUIRK_SWAPPED_MASK)) - r->data.enable = SWAP_BITS(SWAP_BITS(r->data.enable, 0, 2), 1, 3); + scoped_guard(spinlock_irqsave, &xdata->rumble.lock) { + xdata->rumble.throttle_until = RUMBLE_THROTTLE_JIFFIES; + if (xdata->rumble.scheduled) { + unsigned long delay_work = calculate_throttling_delay(xdata); - ret = xpadneo_device_output_report(hdev, (__u8 *) r, sizeof(*r)); - if (ret < 0) - hid_warn(hdev, "failed to send FF report: %d\n", ret); + mod_delayed_work(rumble_wq, &xdata->rumble.worker, delay_work); + } + } } -static inline void update_magnitude(u8 *m, u8 v) +static inline u8 calculate_magnitude(s32 magnitude, int fraction) { - *m = v > 0 ? max(*m, v) : 0; + return (u8)((magnitude * fraction + S16_MAX) / U16_MAX); } static int rumble_play(struct input_dev *dev, void *data, struct ff_effect *effect) { - unsigned long flags, rumble_run_at, rumble_throttle_until; - long delay_work; int fraction_TL, fraction_TR, fraction_MAIN, percent_TRIGGERS, percent_MAIN; s32 weak, strong, max_main; struct hid_device *hdev = input_get_drvdata(dev); struct xpadneo_devdata *xdata = hid_get_drvdata(hdev); - if (effect->type != FF_RUMBLE) + /* do not let FF clients run before rumble state is ready */ + if (unlikely(!smp_load_acquire(&xdata->rumble.enabled))) + return 0; + + if (unlikely(effect->type != FF_RUMBLE)) return 0; /* copy data from effect structure at the very beginning */ @@ -186,47 +205,35 @@ */ max_main = max(weak, strong); - spin_lock_irqsave(&xdata->rumble.lock, flags); - - /* calculate the physical magnitudes, scale from 16 bit to 0..100 */ - update_magnitude(&xdata->rumble.data.magnitude_strong, - (u8)((strong * fraction_MAIN + S16_MAX) / U16_MAX)); - update_magnitude(&xdata->rumble.data.magnitude_weak, - (u8)((weak * fraction_MAIN + S16_MAX) / U16_MAX)); - - /* calculate the physical magnitudes, scale from 16 bit to 0..100 */ - update_magnitude(&xdata->rumble.data.magnitude_left, - (u8)((max_main * fraction_TL + S16_MAX) / U16_MAX)); - update_magnitude(&xdata->rumble.data.magnitude_right, - (u8)((max_main * fraction_TR + S16_MAX) / U16_MAX)); - - /* synchronize: is our worker still scheduled? */ - if (xdata->rumble.scheduled) { - /* the worker is still guarding rumble programming */ - hid_notice_once(hdev, "throttling rumble reprogramming\n"); - goto unlock_and_return; - } + scoped_guard(spinlock_irqsave, &xdata->rumble.lock) { + unsigned long delay_work; - /* we want to run now but may be throttled */ - rumble_run_at = jiffies; - rumble_throttle_until = xdata->rumble.throttle_until; - if (time_before(rumble_run_at, rumble_throttle_until)) { - /* last rumble was recently executed */ - delay_work = (long)rumble_throttle_until - (long)rumble_run_at; - delay_work = clamp(delay_work, 0L, (long)HZ); - } else { - /* the firmware is ready */ - delay_work = 0; - } - - /* schedule writing a rumble report to the controller */ - if (queue_delayed_work(rumble_wq, &xdata->rumble.worker, delay_work)) + /* calculate the physical magnitudes, scale from 16 bit to 0..100 */ + xdata->rumble.data.magnitude_strong = calculate_magnitude(strong, fraction_MAIN); + xdata->rumble.data.magnitude_weak = calculate_magnitude(weak, fraction_MAIN); + + /* calculate the physical magnitudes, scale from 16 bit to 0..100 */ + xdata->rumble.data.magnitude_left = calculate_magnitude(max_main, fraction_TL); + xdata->rumble.data.magnitude_right = calculate_magnitude(max_main, fraction_TR); + + /* synchronize: is our worker still scheduled? */ + if (xdata->rumble.scheduled) { + /* the worker is still guarding rumble programming */ + hid_notice_once(hdev, "throttling rumble reprogramming\n"); + break; + } + + /* we want to run now but may be throttled */ + delay_work = calculate_throttling_delay(xdata); + + /* schedule writing a rumble report to the controller */ + if (mod_delayed_work(rumble_wq, &xdata->rumble.worker, delay_work)) { + /* this should never happen */ + hid_err(hdev, "rumble_playback: unexpected scheduling state\n"); + } xdata->rumble.scheduled = true; - else - hid_err(hdev, "lost rumble packet\n"); + } -unlock_and_return: - spin_unlock_irqrestore(&xdata->rumble.lock, flags); return 0; } @@ -302,7 +309,7 @@ mdelay(30); } -static void xpadneo_rumble_welcome(struct hid_device *hdev) +static void rumble_welcome(struct hid_device *hdev) { struct xpadneo_devdata *xdata = hid_get_drvdata(hdev); struct xpadneo_rumble_report pck = { }; @@ -345,6 +352,10 @@ { struct xpadneo_devdata *xdata = hid_get_drvdata(hdev); struct input_dev *gamepad = xdata->gamepad.idev; + int ret; + + /* publish that rumble is not ready until init finishes */ + smp_store_release(&xdata->rumble.enabled, false); spin_lock_init(&xdata->rumble.lock); INIT_DELAYED_WORK(&xdata->rumble.worker, rumble_worker); @@ -357,14 +368,22 @@ if (param_trigger_rumble_mode == PARAM_TRIGGER_RUMBLE_DISABLE) xdata->quirks |= XPADNEO_QUIRK_NO_TRIGGER_RUMBLE; - if (param_ff_connect_notify) - xpadneo_benchmark(xpadneo_rumble_welcome, hdev); - /* initialize our rumble command throttle */ xdata->rumble.throttle_until = RUMBLE_THROTTLE_JIFFIES; + /* set capabilities */ input_set_capability(gamepad, EV_FF, FF_RUMBLE); - return input_ff_create_memless(gamepad, NULL, rumble_play); + ret = input_ff_create_memless(gamepad, NULL, rumble_play); + if (ret) + return ret; + + if (param_ff_connect_notify) + xpadneo_benchmark(rumble_welcome, hdev); + + /* publish readiness once all rumble state is initialized */ + smp_store_release(&xdata->rumble.enabled, true); + + return 0; } int xpadneo_rumble_init_workqueue(void) @@ -387,5 +406,8 @@ void xpadneo_rumble_remove(struct xpadneo_devdata *xdata) { + /* disable rumble before removable to prevent queueing new data */ + smp_store_release(&xdata->rumble.enabled, false); + cancel_delayed_work_sync(&xdata->rumble.worker); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/xpadneo-0.10/hid-xpadneo/src/xpadneo/xpadneo.h new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/xpadneo.h --- old/xpadneo-0.10/hid-xpadneo/src/xpadneo/xpadneo.h 2026-03-08 07:11:35.000000000 +0100 +++ new/xpadneo-0.10.1/hid-xpadneo/src/xpadneo/xpadneo.h 2026-03-25 02:59:22.000000000 +0100 @@ -20,6 +20,10 @@ #include <linux/timer.h> #include <linux/workqueue.h> +/* detect locally administered unicast MAC addresses */ +#define XPADNEO_OUI_IS_MULTICAST (1 << 0) +#define XPADNEO_OUI_IS_LAA (1 << 1) + /* button aliases */ #define BTN_SHARE KEY_F12 #define BTN_XBOX BTN_MODE @@ -169,7 +173,7 @@ spinlock_t lock; struct delayed_work worker; unsigned long throttle_until; - bool scheduled; + bool scheduled, enabled; struct xpadneo_rumble_data data; struct xpadneo_rumble_data shadow; void *output_report_dmabuf; @@ -184,6 +188,11 @@ extern void xpadneo_synthetic_remove(struct xpadneo_devdata *, const char *, struct xpadneo_subdevice *); +/* xpadneo descriptor debug helpers */ +extern void xpadneo_debug_hid_report(struct hid_device *, const struct xpadneo_rumble_report *, + const size_t); +extern void xpadneo_debug_descriptor(struct hid_device *, const __u8 *, unsigned int); + /* xpadneo core device functions */ extern void xpadneo_device_report(struct hid_device *, struct hid_report *); extern void xpadneo_device_missing(struct xpadneo_devdata *, u32); @@ -209,7 +218,7 @@ extern int xpadneo_mouse_init(struct xpadneo_devdata *); extern void xpadneo_mouse_init_timer(struct xpadneo_devdata *); extern void xpadneo_mouse_report(struct timer_list *); -extern void xpadneo_mouse_toggle(struct xpadneo_devdata *); +extern bool xpadneo_mouse_toggle(struct xpadneo_devdata *); extern int xpadneo_mouse_event(struct xpadneo_devdata *, struct hid_usage *, __s32); extern int xpadneo_mouse_raw_event(struct xpadneo_devdata *, struct hid_report *, u8 *, int); extern void xpadneo_mouse_remove_timer(struct xpadneo_devdata *); @@ -218,7 +227,7 @@ /* battery and power functions */ extern int xpadneo_power_init(struct xpadneo_devdata *); extern void xpadneo_power_update(struct xpadneo_devdata *, u8); -extern void xpadneo_power_remove(struct xpadneo_devdata *xdata); +extern void xpadneo_power_remove(struct xpadneo_devdata *); /* driver quirks handling */ extern int xpadneo_quirks_init(struct xpadneo_devdata *);
