From 687c75e5bc640207283aa01cbcbeb019e46c7a91 Mon Sep 17 00:00:00 2001 From: Maik-0000FF Date: Sat, 7 Mar 2026 22:37:59 +0100 Subject: [PATCH] Gui: Fix Linux SpaceMouse performance: event coalescing, camera batching, per-axis deadzone (#28110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Linux SpaceMouse jerkiness: coalesce spnav events and batch camera updates On Linux, pollSpacenav() posts every spnav motion event individually (125-250Hz from spacenavd), each triggering a full scene redraw. Combined with camera->orientation and camera->position each triggering separate Coin3D notifications, this results in up to 500 redraws/sec. Fix 1 - Event coalescing (GuiNativeEventLinux.cpp): The drain loop now keeps only the latest motion state and posts once after all pending events are consumed. SpaceMouse events are absolute deflection (current puck position), not deltas, so the latest event always contains the complete state. Fix 2 - Batched camera updates (NavigationStyle.cpp): Wrap camera property changes in enableNotify(false/true) + touch() so Coin3D fires a single redraw instead of two per event. Together this reduces redraws from ~500/sec to ~60/sec (display refresh), eliminating the jerky navigation that Linux users experience with complex models. Fixes #25926 * Add per-axis deadzone filtering for Linux SpaceMouse Each SpaceMouse axis has different sensitivity and noise characteristics. A single global deadzone is too coarse — rotation axes may need different thresholds than translation axes. Read per-axis deadzone values from user.cfg (BaseApp/Spaceball/Motion) using keys PanLRDeadzone, PanUDDeadzone, ZoomDeadzone, TiltDeadzone, RollDeadzone, SpinDeadzone. Values default to 0 (no filtering). Axis values below their individual threshold are zeroed before posting the motion event, preventing unintended drift from sensor noise while preserving intentional small movements. This is the Linux equivalent of deadzone handling that NavLib provides on Windows/macOS. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use std::array for axisDeadzoneKeys to avoid magic number Replace C-style array with std::array and use .size() instead of hardcoded loop bound, as suggested in code review. * Cache deadzone values with ParameterGrp Observer instead of polling Replace per-event GetParameterGroupByPath + GetInt calls with a static DeadzoneCache that loads values once and updates them via ParameterGrp::ObserverType when user.cfg changes. Also use static constexpr for the keys array and std::abs for readability. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Move DeadzoneCache from static local to member variable Replace the static local DeadzoneCache in pollSpacenav() with a std::unique_ptr member of GuiNativeEvent, initialized in initSpaceball() after the spnav connection is established. Forward-declare in the header to keep includes minimal. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Maik-0000FF Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/Gui/3Dconnexion/GuiNativeEventLinux.cpp | 72 ++++++++++++++++++++- src/Gui/3Dconnexion/GuiNativeEventLinux.h | 5 ++ src/Gui/Navigation/NavigationStyle.cpp | 9 ++- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/Gui/3Dconnexion/GuiNativeEventLinux.cpp b/src/Gui/3Dconnexion/GuiNativeEventLinux.cpp index c8e1eef835..e88db5f8e6 100644 --- a/src/Gui/3Dconnexion/GuiNativeEventLinux.cpp +++ b/src/Gui/3Dconnexion/GuiNativeEventLinux.cpp @@ -21,17 +21,70 @@ ***************************************************************************/ #include +#include +#include +#include +#include #include "GuiNativeEventLinux.h" #include "GuiApplicationNativeEventAware.h" #include +#include #include #include #include +// Cached per-axis deadzone values, auto-updated via Observer when user.cfg changes. +class Gui::DeadzoneCache: public ParameterGrp::ObserverType +{ +public: + static constexpr std::array keys = { + "PanLRDeadzone", + "PanUDDeadzone", + "ZoomDeadzone", + "TiltDeadzone", + "RollDeadzone", + "SpinDeadzone", + }; + + std::array values {}; + + explicit DeadzoneCache(ParameterGrp::handle hGrp) + : hGrp(std::move(hGrp)) + { + loadAll(); + this->hGrp->Attach(this); + } + + ~DeadzoneCache() override + { + hGrp->Detach(this); + } + + void OnChange(ParameterGrp::SubjectType& /*rCaller*/, ParameterGrp::MessageType reason) override + { + for (size_t i = 0; i < keys.size(); i++) { + if (std::strcmp(reason, keys[i]) == 0) { + values[i] = static_cast(hGrp->GetInt(keys[i], 0)); + return; + } + } + } + +private: + void loadAll() + { + for (size_t i = 0; i < keys.size(); i++) { + values[i] = static_cast(hGrp->GetInt(keys[i], 0)); + } + } + + ParameterGrp::handle hGrp; +}; + Gui::GuiNativeEvent::GuiNativeEvent(Gui::GUIApplicationNativeEventAware* app) : GuiAbstractNativeEvent(app) {} @@ -60,6 +113,9 @@ void Gui::GuiNativeEvent::initSpaceball(QMainWindow* window) QSocketNotifier* SpacenavNotifier = new QSocketNotifier(spnav_fd(), QSocketNotifier::Read, this); connect(SpacenavNotifier, SIGNAL(activated(int)), this, SLOT(pollSpacenav())); + dzCache = std::make_unique( + App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Spaceball/Motion") + ); mainApp->setSpaceballPresent(true); } } @@ -67,6 +123,7 @@ void Gui::GuiNativeEvent::initSpaceball(QMainWindow* window) void Gui::GuiNativeEvent::pollSpacenav() { spnav_event ev; + bool hasMotion = false; while (spnav_poll_event(&ev)) { switch (ev.type) { case SPNAV_EVENT_MOTION: { @@ -76,7 +133,7 @@ void Gui::GuiNativeEvent::pollSpacenav() motionDataArray[3] = -ev.motion.rx; motionDataArray[4] = -ev.motion.rz; motionDataArray[5] = -ev.motion.ry; - mainApp->postMotionEvent(motionDataArray); + hasMotion = true; break; } case SPNAV_EVENT_BUTTON: { @@ -85,6 +142,19 @@ void Gui::GuiNativeEvent::pollSpacenav() } } } + if (hasMotion) { + // Per-axis deadzone: zero out axes below their individual threshold. + // Values cached and auto-updated via Observer when user.cfg changes. + if (dzCache) { + for (size_t i = 0; i < dzCache->values.size(); i++) { + int dz = dzCache->values[i]; + if (dz > 0 && std::abs(motionDataArray[i]) < dz) { + motionDataArray[i] = 0; + } + } + } + mainApp->postMotionEvent(motionDataArray); + } } #include "3Dconnexion/moc_GuiNativeEventLinux.cpp" diff --git a/src/Gui/3Dconnexion/GuiNativeEventLinux.h b/src/Gui/3Dconnexion/GuiNativeEventLinux.h index adcb4a4929..27ced02165 100644 --- a/src/Gui/3Dconnexion/GuiNativeEventLinux.h +++ b/src/Gui/3Dconnexion/GuiNativeEventLinux.h @@ -23,12 +23,14 @@ #pragma once #include "GuiAbstractNativeEvent.h" +#include class QMainWindow; namespace Gui { class GUIApplicationNativeEventAware; +class DeadzoneCache; class GuiNativeEvent: public GuiAbstractNativeEvent { @@ -42,6 +44,9 @@ private: GuiNativeEvent(); GuiNativeEvent(const GuiNativeEvent&); GuiNativeEvent& operator=(const GuiNativeEvent&); + + std::unique_ptr dzCache; + private Q_SLOTS: void pollSpacenav(); }; diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 22bdec6be8..a3a6e10156 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -1934,9 +1934,14 @@ SbBool NavigationStyle::processMotionEvent(const SoMotion3Event* const ev) newRotation.multVec(SbVec3f(0.0, 0.0, -1.0), newDirection); newPosition = center - (newDirection * camera->focalDistance.getValue()); + newRotation.multVec(dir, dir); + SbVec3f finalPosition = newPosition + (dir * translationFactor); + + camera->enableNotify(false); camera->orientation.setValue(newRotation); - camera->orientation.getValue().multVec(dir, dir); - camera->position = newPosition + (dir * translationFactor); + camera->position = finalPosition; + camera->enableNotify(true); + camera->touch(); return true; }