Gui: Fix Linux SpaceMouse performance: event coalescing, camera batching, per-axis deadzone (#28110)

* 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<DeadzoneCache> 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 <Maik-0000FF@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Maik-0000FF
2026-03-07 22:37:59 +01:00
committed by GitHub
parent 36dd4b983c
commit 687c75e5bc
3 changed files with 83 additions and 3 deletions
+71 -1
View File
@@ -21,17 +21,70 @@
***************************************************************************/
#include <FCConfig.h>
#include <array>
#include <cmath>
#include <cstring>
#include <App/Application.h>
#include "GuiNativeEventLinux.h"
#include "GuiApplicationNativeEventAware.h"
#include <Base/Console.h>
#include <Base/Parameter.h>
#include <QMainWindow>
#include <QSocketNotifier>
#include <spnav.h>
// Cached per-axis deadzone values, auto-updated via Observer when user.cfg changes.
class Gui::DeadzoneCache: public ParameterGrp::ObserverType
{
public:
static constexpr std::array<const char*, 6> keys = {
"PanLRDeadzone",
"PanUDDeadzone",
"ZoomDeadzone",
"TiltDeadzone",
"RollDeadzone",
"SpinDeadzone",
};
std::array<int, 6> 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<int>(hGrp->GetInt(keys[i], 0));
return;
}
}
}
private:
void loadAll()
{
for (size_t i = 0; i < keys.size(); i++) {
values[i] = static_cast<int>(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<DeadzoneCache>(
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"
@@ -23,12 +23,14 @@
#pragma once
#include "GuiAbstractNativeEvent.h"
#include <memory>
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<DeadzoneCache> dzCache;
private Q_SLOTS:
void pollSpacenav();
};
+7 -2
View File
@@ -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;
}