From 8fa58bd6094fc96ccf93048085deebd1e197b086 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 19 Oct 2026 22:03:18 +1300 Subject: [PATCH] Fix the 3D mouse search asking every device on the PC for its name While no 3D mouse is plugged in, which for most users is always, the hidapi backend looks for one every 3 seconds for as long as QElectroTech runs. It asked hidapi for every HID device, hid_enumerate(0, 0), and on Windows hidapi reads the serial number, manufacturer and product strings of each device that matches what it is asked for from the device itself. So every mouse, keyboard, headset and webcam on the PC was asked for its names every 3 seconds, on the GUI thread. Issue #1553 reports the pointer moving by itself on Windows since the first package with this backend; nothing else in that package talks to devices. SDL 2.0.9 polled the same way and saw freezes and a keyboard that kept re-initialising (libsdl-org/SDL#3071). The scan now asks for 3Dconnexion's vendor id, and for each 3D mouse made under Logitech's id by its product id, since most other mice and keyboards carry Logitech's id too. That is twelve hidapi calls instead of one, but a device that does not match is no longer asked for its names. On Linux and macOS hidapi takes the names from the system, and one call is cheaper than twelve (on Linux, median of 200: 5 ms against 36 ms), so the scan there is unchanged. Where the scan is filtered, a Logitech-id device that is not one of the eleven 3D mice on the list is no longer found, even if it reports itself as a multi-axis controller. 3Dconnexion's own mice and keyboards, such as the CadMouse, are still asked: their vendor id is asked for as a whole, so that new 3D mice are found without a list. Co-Authored-By: Claude Opus 5.5 --- sources/spacemouse/hidbackend.cpp | 30 ++++++++++++++++------ sources/spacemouse/spacemousehid.cpp | 15 ++++++++++- sources/spacemouse/spacemousehid.h | 15 +++++++++++ tests/qttest/tst_spacemousehid.cpp | 37 ++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/sources/spacemouse/hidbackend.cpp b/sources/spacemouse/hidbackend.cpp index 63d9dbb05..6fc422712 100644 --- a/sources/spacemouse/hidbackend.cpp +++ b/sources/spacemouse/hidbackend.cpp @@ -82,16 +82,32 @@ void HidBackend::scan() return; } - hid_device_info *list = hid_enumerate(0, 0); - for (hid_device_info *info = list; info && !m_device; info = info->next) + //On Linux and macOS hidapi takes the name strings of the devices it + //lists from the system, and one call listing every device is the + //cheapest scan. Elsewhere, such as on Windows, it reads them from + //each device itself, so only 3D mice are asked for (see + //SpaceMouseHid::scanIds()). +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + const QList ids = {SpaceMouseHid::DeviceId()}; +#else + const QList ids = SpaceMouseHid::scanIds(); +#endif + for (const SpaceMouseHid::DeviceId &id : ids) { - if (!SpaceMouseHid::isSpaceMouse(info->vendor_id, info->product_id, - info->usage_page, info->usage)) { - continue; + hid_device_info *list = hid_enumerate(id.vendor, id.product); + for (hid_device_info *info = list; info && !m_device; info = info->next) + { + if (!SpaceMouseHid::isSpaceMouse(info->vendor_id, info->product_id, + info->usage_page, info->usage)) { + continue; + } + m_device = hid_open_path(info->path); + } + hid_free_enumeration(list); + if (m_device) { + break; } - m_device = hid_open_path(info->path); } - hid_free_enumeration(list); if (!m_device) { m_scan_timer->start(); diff --git a/sources/spacemouse/spacemousehid.cpp b/sources/spacemouse/spacemousehid.cpp index a235e338f..b420232ac 100644 --- a/sources/spacemouse/spacemousehid.cpp +++ b/sources/spacemouse/spacemousehid.cpp @@ -42,7 +42,7 @@ namespace { constexpr qint64 MAX_BITS = 1 << 20; //3D mice sold under Logitech's vendor id, for the ones whose - //interface does not report a usage. + //interface does not report a usage, and for scanIds(). const QSet LOGITECH_3D_MICE = { 0xc603, 0xc605, 0xc606, 0xc621, 0xc623, 0xc625, 0xc626, 0xc627, 0xc628, 0xc629, 0xc62b @@ -95,6 +95,19 @@ bool SpaceMouseHid::isSpaceMouse(unsigned short vendor, unsigned short product, return false; } +QList SpaceMouseHid::scanIds() +{ + QList ids; + ids.append({VENDOR_3DCONNEXION, 0}); + + QList logitech(LOGITECH_3D_MICE.cbegin(), LOGITECH_3D_MICE.cend()); + std::sort(logitech.begin(), logitech.end()); + for (const unsigned short product : std::as_const(logitech)) { + ids.append({VENDOR_LOGITECH, product}); + } + return ids; +} + bool SpaceMouseHid::Layout::hasAxes() const { for (const Field &f : axes) { diff --git a/sources/spacemouse/spacemousehid.h b/sources/spacemouse/spacemousehid.h index 3e23dcd8a..205bb6cea 100644 --- a/sources/spacemouse/spacemousehid.h +++ b/sources/spacemouse/spacemousehid.h @@ -45,6 +45,21 @@ namespace SpaceMouseHid bool isSpaceMouse(unsigned short vendor, unsigned short product, unsigned short usage_page, unsigned short usage); + /// A vendor and product id as hid_enumerate() takes them, where 0 + /// matches every vendor or every product. + struct DeviceId + { + unsigned short vendor = 0; + unsigned short product = 0; + }; + + /// @return what a scan asks hidapi for where hidapi reads each + /// matching device's name strings from the device itself, as on + /// Windows (issue #1553): every 3Dconnexion device, and Logitech's + /// 3D mice by product id, since most other mice and keyboards carry + /// Logitech's id too. + QList scanIds(); + /// Where one value sits in the reports, per the report descriptor. struct Field { diff --git a/tests/qttest/tst_spacemousehid.cpp b/tests/qttest/tst_spacemousehid.cpp index 5aa06242c..cd7d0e136 100644 --- a/tests/qttest/tst_spacemousehid.cpp +++ b/tests/qttest/tst_spacemousehid.cpp @@ -74,6 +74,8 @@ class TstSpaceMouseHid : public QObject private slots: void recognisesOnly3DMice_data(); void recognisesOnly3DMice(); + void scanAsksOnlyFor3DMice_data(); + void scanAsksOnlyFor3DMice(); void parsesTheClassicDescriptor(); void parsesADescriptorWithoutReportIds(); void survivesBrokenDescriptors_data(); @@ -115,6 +117,41 @@ void TstSpaceMouseHid::recognisesOnly3DMice() QCOMPARE(isSpaceMouse(vendor, product, page, usage), match); } + // Where hidapi reads the name strings of every device a scan asks for + // from the device itself, as on Windows, the other devices on the PC + // must never be asked for at all (issue #1553). +void TstSpaceMouseHid::scanAsksOnlyFor3DMice_data() +{ + QTest::addColumn("vendor"); + QTest::addColumn("product"); + QTest::addColumn("asked"); + QTest::newRow("3Dconnexion SpaceMouse Compact") << 0x256f << 0xc635 << true; + QTest::newRow("3Dconnexion receiver") << 0x256f << 0xc652 << true; + QTest::newRow("Logitech SpaceNavigator") << 0x046d << 0xc626 << true; + QTest::newRow("Logitech SpacePilot Pro") << 0x046d << 0xc629 << true; + QTest::newRow("Logitech receiver") << 0x046d << 0xc52b << false; + QTest::newRow("Logitech mouse") << 0x046d << 0xc077 << false; + QTest::newRow("Microsoft keyboard") << 0x045e << 0x07f8 << false; + QTest::newRow("HP mouse") << 0x03f0 << 0x094a << false; +} + +void TstSpaceMouseHid::scanAsksOnlyFor3DMice() +{ + QFETCH(int, vendor); + QFETCH(int, product); + QFETCH(bool, asked); + + // hid_enumerate()'s own rule: an id of 0 matches every id. + bool matched = false; + for (const DeviceId &id : scanIds()) { + if ((id.vendor == 0 || id.vendor == vendor) + && (id.product == 0 || id.product == product)) { + matched = true; + } + } + QCOMPARE(matched, asked); +} + void TstSpaceMouseHid::parsesTheClassicDescriptor() { const Layout layout = parseDescriptor(classicDescriptor(true));