NDOF: Rework input handling on macOS

This patch modifies the logic behind handling NDOF device events on
macOS so that it can benefit from the `Blender` profile available in
3DConnexion driver v10.8.7 and later.

A new device command was introduced: `kConnexionCmdAppEvent`, which is
sent by the driver upong getting an apprioriate NDOF device button
input. This allow the driver to consumes all NDOF device input and then
send appropriate app events based on its configuration instead of
forwarding raw data to the application directly.

When using 3DConnexion driver versions prior to v10.8.7, the behavior
is unchanged. This approach allows for supporting all of the SpaceMouse
Enterprise buttons, long presses included (solving issue #119206 on macOS)

Co-authored-by: Sergey Sharybin <sergey@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/126694
This commit is contained in:
kgalik
2025-02-16 23:39:24 +01:00
committed by Jonas Holzman
parent 665b4dfbad
commit c3d92f32dc
2 changed files with 49 additions and 42 deletions

View File

@@ -510,13 +510,6 @@ void GHOST_NDOFManager::updateButton(int button_number, bool press, uint64_t tim
{
GHOST_NDOF_ButtonT button = static_cast<GHOST_NDOF_ButtonT>(button_number);
/* For bit-mask devices button mapping isn't unified, therefore check the button map. */
if (std::find(bitmask_devices_.begin(), bitmask_devices_.end(), device_type_) !=
bitmask_devices_.end())
{
button = hid_map_[button_number];
}
if (button == GHOST_NDOF_BUTTON_INVALID) {
CLOG_INFO(
LOG, 2, "button=%d, press=%d (mapped to none, ignoring!)", button_number, int(press));
@@ -578,7 +571,11 @@ void GHOST_NDOFManager::updateButtonsBitmask(int button_bits, uint64_t time)
else {
button_depressed_ &= ~mask; /* Clear this button's bit. */
}
updateButton(button_number, press, time);
/* Bitmask devices don't have unified keymaps, so button numbers needs to be looked up in the
* map. */
int button = hid_map_[button_number];
updateButton(button, press, time);
}
}
}

View File

@@ -6,6 +6,7 @@
#include "GHOST_NDOFManagerCocoa.hh"
#include "GHOST_SystemCocoa.hh"
#import <Cocoa/Cocoa.h>
#include <dlfcn.h>
#include <stdint.h>
@@ -21,19 +22,23 @@ static GHOST_NDOFManager *ndof_manager = nullptr;
static uint16_t clientID = 0;
static bool driver_loaded = false;
static bool has_old_driver =
false; /* 3Dconnexion drivers before 10 beta 4 are "old", not all buttons will work. */
static bool has_new_driver =
false; /* drivers >= 10.2.2 are "new", and can process events on a separate thread. */
/* 3DxMacCore version >= minimal_version is considered "new".
* It was firstly introduced in 3DxWare v10.8.4 r3716 and can process
* (not yet documented in SDK manual) kConnexionCmdAppEvent events. */
static NSString *new_driver_minimal_version = @"1.3.4.473";
/* Replicate just enough of the 3Dx API for our uses, not everything the driver provides. */
#define kConnexionClientModeTakeOver 1
#define kConnexionMaskAll 0x3fff
#define kConnexionMaskAxis 0x3f00
#define kConnexionMaskNoButtons 0x0
#define kConnexionMaskAllButtons 0xffffffff
#define kConnexionCmdHandleButtons 2
#define kConnexionCmdHandleAxis 3
#define kConnexionCmdAppSpecific 10
#define kConnexionCmdAppEvent 11
#define kConnexionMsgDeviceState '3dSR'
#define kConnexionCtlGetDeviceID '3did'
@@ -46,8 +51,8 @@ struct ConnexionDeviceState {
int32_t value;
uint64_t time;
uint8_t report[8];
uint16_t buttons8; /* Obsolete! (pre-10.x drivers). */
int16_t axis[6]; /* TX, TY, TZ, RX, RY, RZ. */
uint16_t appEventPressed;
int16_t axis[6]; /* TX, TY, TZ, RX, RY, RZ. */
uint16_t address;
uint32_t buttons;
};
@@ -60,7 +65,6 @@ typedef void (*MessageHandler)(uint32_t, uint32_t msg_type, void *msg_arg);
/* Driver functions: */
typedef int16_t (*SetConnexionHandlers_ptr)(MessageHandler, AddedHandler, RemovedHandler, bool);
typedef int16_t (*InstallConnexionHandlers_ptr)(MessageHandler, AddedHandler, RemovedHandler);
typedef void (*CleanupConnexionHandlers_ptr)();
typedef uint16_t (*RegisterConnexionClient_ptr)(uint32_t signature,
const char *name,
@@ -76,7 +80,6 @@ typedef int16_t (*ConnexionClientControl_ptr)(uint16_t clientID,
#define DECLARE_FUNC(name) name##_ptr name = nullptr
DECLARE_FUNC(SetConnexionHandlers);
DECLARE_FUNC(InstallConnexionHandlers);
DECLARE_FUNC(CleanupConnexionHandlers);
DECLARE_FUNC(RegisterConnexionClient);
DECLARE_FUNC(SetConnexionClientButtonMask);
@@ -117,12 +120,6 @@ static bool load_driver_functions()
if (SetConnexionHandlers != nullptr) {
driver_loaded = true;
has_new_driver = true;
}
else {
LOAD_FUNC(InstallConnexionHandlers);
driver_loaded = (InstallConnexionHandlers != nullptr);
}
if (driver_loaded) {
@@ -131,8 +128,6 @@ static bool load_driver_functions()
LOAD_FUNC(SetConnexionClientButtonMask);
LOAD_FUNC(UnregisterConnexionClient);
LOAD_FUNC(ConnexionClientControl);
has_old_driver = (SetConnexionClientButtonMask == nullptr);
}
}
#if DEBUG_NDOF_DRIVER
@@ -141,8 +136,6 @@ static bool load_driver_functions()
}
printf("loaded: %s\n", driver_loaded ? "YES" : "NO");
printf("old: %s\n", has_old_driver ? "YES" : "NO");
printf("new: %s\n", has_new_driver ? "YES" : "NO");
#endif
return driver_loaded;
@@ -162,8 +155,8 @@ static void DeviceAdded(uint32_t /*unused*/)
/* Determine exactly which device is plugged in. */
int32_t result;
ConnexionClientControl(clientID, kConnexionCtlGetDeviceID, 0, &result);
int16_t vendorID = result >> 16;
int16_t productID = result & 0xffff;
const int16_t vendorID = result >> 16;
const int16_t productID = result & 0xffff;
ndof_manager->setDevice(vendorID, productID);
}
@@ -183,7 +176,7 @@ static void DeviceEvent(uint32_t /*unused*/, uint32_t msg_type, void *msg_arg)
/* Device state is broadcast to all clients; only react if sent to us. */
if (s->client == clientID) {
/* TODO: is s->time compatible with GHOST timestamps? if so use that instead. */
uint64_t now = ghost_system->getMilliSeconds();
const uint64_t now = ghost_system->getMilliSeconds();
switch (s->command) {
case kConnexionCmdHandleAxis: {
@@ -198,7 +191,7 @@ static void DeviceEvent(uint32_t /*unused*/, uint32_t msg_type, void *msg_arg)
break;
}
case kConnexionCmdHandleButtons: {
int button_bits = has_old_driver ? s->buttons8 : s->buttons;
const int button_bits = s->buttons;
#ifdef DEBUG_NDOF_BUTTONS
printf("button bits: 0x%08x\n", button_bits);
#endif
@@ -206,6 +199,16 @@ static void DeviceEvent(uint32_t /*unused*/, uint32_t msg_type, void *msg_arg)
ghost_system->notifyExternalEventProcessed();
break;
}
case kConnexionCmdAppEvent: {
const int button_number = s->value;
const bool pressed = s->appEventPressed;
#ifdef DEBUG_NDOF_BUTTONS
printf("button number: %d, pressed: %d\n", button_number, pressed);
#endif
ndof_manager->updateButton(button_number, pressed, now);
ghost_system->notifyExternalEventProcessed();
break;
}
#if DEBUG_NDOF_DRIVER
case kConnexionCmdAppSpecific:
printf("ndof: app-specific command, param = %hd, value = %d\n", s->param, s->value);
@@ -226,14 +229,7 @@ GHOST_NDOFManagerCocoa::GHOST_NDOFManagerCocoa(GHOST_System &sys) : GHOST_NDOFMa
ghost_system = dynamic_cast<GHOST_SystemCocoa *>(&sys);
ndof_manager = this;
uint16_t error;
if (has_new_driver) {
const bool separate_thread = false; /* TODO: rework Mac event handler to allow this. */
error = SetConnexionHandlers(DeviceEvent, DeviceAdded, DeviceRemoved, separate_thread);
}
else {
error = InstallConnexionHandlers(DeviceEvent, DeviceAdded, DeviceRemoved);
}
const uint16_t error = SetConnexionHandlers(DeviceEvent, DeviceAdded, DeviceRemoved, true);
if (error) {
#if DEBUG_NDOF_DRIVER
@@ -242,13 +238,27 @@ GHOST_NDOFManagerCocoa::GHOST_NDOFManagerCocoa(GHOST_System &sys) : GHOST_NDOFMa
return;
}
const NSDictionary *dictInfos =
[NSBundle bundleWithPath:@"/Library/Frameworks/3DconnexionClient.framework"]
.infoDictionary;
NSString *strVersion = [dictInfos objectForKey:(NSString *)kCFBundleVersionKey];
const auto compare = [strVersion compare:new_driver_minimal_version];
const bool has_new_driver = compare != NSOrderedAscending;
/* New driver makes use of kConnexionCmdAppEvent events, which require to have all buttons
* unmasked. Basically, this means that driver consumes all NDOF device input and then sends
* appropriate app events based on its configuration instead of forwarding raw data to the
* application. When using an older driver, the old solution with all buttons forwarded
* (masked) is preferred. */
const uint32_t client_mask = has_new_driver ? kConnexionMaskAxis : kConnexionMaskAll;
const uint32_t button_mask = has_new_driver ? kConnexionMaskNoButtons :
kConnexionMaskAllButtons;
/* Pascal string *and* a four-letter constant. How old-school. */
clientID = RegisterConnexionClient(
'blnd', "\007blender", kConnexionClientModeTakeOver, kConnexionMaskAll);
'blnd', "\007blender", kConnexionClientModeTakeOver, client_mask);
if (!has_old_driver) {
SetConnexionClientButtonMask(clientID, kConnexionMaskAllButtons);
}
SetConnexionClientButtonMask(clientID, button_mask);
}
}