FEAT(client): Implement raw input, get rid of hooks and DirectInput (Windows)

Fixes #4039.

Several users complained about the global shortcuts engine on Windows, mainly because it affects the keyboard and mouse behavior for all applications:

1. It reportedly causes the polling rate to drop significantly.
2. Causes accessibility programs such as NVDA to read Mumble's UI slow. Workaround implemented in a310c7f.
3. Using the debugger's "pause" function is nearly impossible: the OS has to wait for the callback to timeout before registering the input, causing a consistent and heavy slowdown.

Disabling hooks fixes all issues described above, however it only leaves DirectInput for standard devices (i.e. not relying on XInput and G-keys).

This commit completely rewrites GlobalShortcutWin, implementing raw input and getting rid of hooks and DirectInput.

Controllers are supported, but only basic buttons are handled for now. Support for analog input (including d-pad) will be added in the future.
XInput can still be enabled, in order for input from triggers and d-pad on Xbox controllers to be registered.

As a bonus, the main thread doesn't wait for the input processing function(s) to return anymore.
The reason why this wasn't the case before is because we needed to know whether to block the message (returning true) from reaching other applications ("suppress" feature).
That cannot work with raw input due to its nature. Returning true simply tells Windows that we didn't process the message.
This commit is contained in:
Davide Beatrici 2021-04-28 00:56:14 +02:00
parent 822185e997
commit 3dc7b00d09
9 changed files with 618 additions and 1021 deletions

View File

@ -533,8 +533,8 @@ if(WIN32)
target_link_libraries(mumble
PRIVATE
dbghelp.lib
dinput8.lib
dxguid.lib
hid.lib
wintrust.lib
)
else()

View File

@ -686,7 +686,6 @@ void GlobalShortcutConfig::load(const Settings &r) {
}
qcbEnableUIAccess->setChecked(r.bEnableUIAccess);
qcbEnableWinHooks->setChecked(r.bEnableWinHooks);
qcbEnableGKey->setChecked(r.bEnableGKey);
qcbEnableXboxInput->setChecked(r.bEnableXboxInput);
@ -702,17 +701,13 @@ void GlobalShortcutConfig::save() const {
bool oldUIAccess = s.bEnableUIAccess;
s.bEnableUIAccess = qcbEnableUIAccess->checkState() == Qt::Checked;
bool oldWinHooks = s.bEnableWinHooks;
s.bEnableWinHooks = qcbEnableWinHooks->checkState() == Qt::Checked;
bool oldGKey = s.bEnableGKey;
s.bEnableGKey = qcbEnableGKey->checkState() == Qt::Checked;
bool oldXboxInput = s.bEnableXboxInput;
s.bEnableXboxInput = qcbEnableXboxInput->checkState() == Qt::Checked;
if (s.bEnableUIAccess != oldUIAccess || s.bEnableWinHooks != oldWinHooks || s.bEnableGKey != oldGKey
|| s.bEnableXboxInput != oldXboxInput) {
if (s.bEnableUIAccess != oldUIAccess || s.bEnableGKey != oldGKey || s.bEnableXboxInput != oldXboxInput) {
s.requireRestartToApply = true;
}
}

View File

@ -127,12 +127,12 @@
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerDefaultSectionSize">
<number>100</number>
</attribute>
<attribute name="headerMinimumSectionSize">
<number>50</number>
</attribute>
<attribute name="headerDefaultSectionSize">
<number>100</number>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
@ -227,16 +227,6 @@ Without this option enabled, using Mumble's global shortcuts in privileged appli
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="qcbEnableWinHooks">
<property name="whatsThis">
<string>&lt;b&gt;Enable Windows hooks&lt;/b&gt;.&lt;br /&gt;This enables the Windows hooks shortcut engine. Using this engine allows Mumble to suppress keypresses and mouse clicks.</string>
</property>
<property name="text">
<string>Enable Windows hooks</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="qcbEnableGKey">
<property name="whatsThis">

File diff suppressed because it is too large Load Diff

View File

@ -6,135 +6,79 @@
#ifndef MUMBLE_MUMBLE_GLOBALSHORTCUT_WIN_H_
#define MUMBLE_MUMBLE_GLOBALSHORTCUT_WIN_H_
#include "Timer.h"
#include "GlobalShortcut.h"
#ifdef USE_GKEY
# include "GKey.h"
#endif
#ifdef USE_XBOXINPUT
# include "XboxInput.h"
#endif
#define DIRECTINPUT_VERSION 0x0800
#include <dinput.h>
#include <memory>
#include <unordered_map>
typedef QPair< GUID, DWORD > qpButton;
struct InputDevice {
LPDIRECTINPUTDEVICE8 pDID;
QString name;
GUID guid;
QVariant vguid;
GUID guidproduct;
QVariant vguidproduct;
uint16_t vendor_id;
uint16_t product_id;
// dwType to name
QHash< DWORD, QString > qhNames;
// Map dwType to dwOfs in our structure
QHash< DWORD, DWORD > qhTypeToOfs;
// Map dwOfs in our structure to dwType
QHash< DWORD, DWORD > qhOfsToType;
// Buttons active since last reset
QSet< DWORD > activeMap;
};
#ifdef USE_GKEY
class GKeyLibrary;
#endif
class GlobalShortcutWin : public GlobalShortcutEngine {
private:
Q_OBJECT
Q_DISABLE_COPY(GlobalShortcutWin)
public:
LPDIRECTINPUT8 pDI;
QHash< GUID, InputDevice * > qhInputDevices;
HHOOK hhMouse, hhKeyboard;
unsigned int uiHardwareDevices;
Timer tDoubleClick;
bool bHook;
#ifdef USE_GKEY
GKeyLibrary *gkey;
#endif
#ifdef USE_XBOXINPUT
/// xboxinputLastPacket holds the last packet number
/// that was processed. Any new data queried for a
/// device is only valid if the packet number is
/// different than last time we queried it.
uint32_t xboxinputLastPacket[XBOXINPUT_MAX_DEVICES];
XboxInput *xboxinput;
/// nxboxinput holds the number of XInput devices
/// available on the system. It is filled out by
/// our EnumDevices callback.
int nxboxinput;
#endif
static BOOL CALLBACK EnumSuitableDevicesCB(LPCDIDEVICEINSTANCE, LPDIRECTINPUTDEVICE8, DWORD, DWORD, LPVOID);
static BOOL CALLBACK EnumDevicesCB(LPCDIDEVICEINSTANCE, LPVOID);
static BOOL CALLBACK EnumDeviceObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef);
static LRESULT CALLBACK HookKeyboard(int, WPARAM, LPARAM);
static LRESULT CALLBACK HookMouse(int, WPARAM, LPARAM);
/// Handle an incoming Windows keyboard message.
///
/// Returns true if the GlobalShortcut engine signals that the
/// button should be suppressed. Returns false otherwise.
static bool handleKeyboardMessage(DWORD scancode, DWORD vkcode, bool extended, bool down);
/// Handle an incoming Windows mouse message.
///
/// Returns true if the GlobalShortcut engine signals that the
/// button should be suppressed. Returns false otherwise.
static bool handleMouseMessage(unsigned int btn, bool down);
virtual bool canSuppress() Q_DECL_OVERRIDE;
void run() Q_DECL_OVERRIDE;
bool event(QEvent *e) Q_DECL_OVERRIDE;
public slots:
void timeTicked();
public:
GlobalShortcutWin();
~GlobalShortcutWin() Q_DECL_OVERRIDE;
void unacquire();
ButtonInfo buttonInfo(const QVariant &) Q_DECL_OVERRIDE;
/// Inject a native Windows keyboard message into GlobalShortcutWin's
/// event stream. This method is meant to be called from the main thread
/// Inject a native Windows raw input message into GlobalShortcutWin's
/// thread. This method is meant to be called from the main thread
/// to pass native Windows keyboard messages to GlobalShortcutWin.
///
/// @param msg The keyboard message to inject into GlobalShortcutWin.
/// Must be WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN or WM_SYSKEYUP.
/// Otherwise the message will be ignored.
///
/// @return Returns true if the GlobalShortcut engine signalled that
/// the button should be suppressed. Returns false otherwise.
bool injectKeyboardMessage(MSG *msg);
/// @param msg The raw input handle to inject into GlobalShortcutWin.
void injectRawInputMessage(HRAWINPUT handle);
/// Inject a native Windows mouse message into GlobalShortcutWin's
/// event stream. This method is meant to be called from the main thread
/// to pass native Windows mouse messages to GlobalShortcutWin.
///
/// @param msg The keyboard message to inject into GlobalShortcutWin.
/// Must be WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONDOWN,
/// WM_RBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_XBUTTONDOWN
/// or WM_XBUTTONUP. Otherwise the message will be ignored.
///
/// @return Returns true if the GlobalShortcut engine signalled that
/// the button should be suppressed. Returns false otherwise.
bool injectMouseMessage(MSG *msg);
ButtonInfo buttonInfo(const QVariant &button) override;
void run() override;
private:
bool areScreenReadersActive();
GlobalShortcutWin();
~GlobalShortcutWin() override;
public slots:
void deviceRemoved(const HANDLE deviceHandle);
void timeTicked();
void on_hidMessage(const HANDLE deviceHandle, std::vector< char > reports, const uint32_t reportSize);
void on_keyboardMessage(const uint16_t flags, uint16_t scanCode, const uint16_t virtualKey);
void on_mouseMessage(const uint16_t flags, const uint16_t data);
signals:
void hidMessage(const HANDLE deviceHandle, std::vector< char > reports, const uint32_t reportSize);
void keyboardMessage(const uint16_t flags, uint16_t scanCode, const uint16_t virtualKey);
void mouseMessage(const uint16_t flags, const uint16_t data);
protected:
struct Device {
std::string name;
std::string prefix;
std::vector< uint8_t > data;
std::vector< bool > buttons;
std::pair< uint16_t, uint16_t > usageRange;
#ifdef USE_XBOXINPUT
bool xinput;
Device() : xinput(false){};
#endif
};
typedef std::unordered_map< HANDLE, Device > DeviceMap;
DeviceMap m_devices;
#ifdef USE_XBOXINPUT
std::unique_ptr< XboxInput > m_xinput;
/// Holds the number of XInput devices available on the system.
/// It is filled out by addDevice().
uint8_t m_xinputDevices;
/// Holds the last packet number that was processed.
/// Any new data queried for a device is only valid
/// if the packet number is different than last time we queried it.
uint32_t m_xinputLastPacket[XBOXINPUT_MAX_DEVICES];
static bool xinputIsPressed(const uint8_t bit, const XboxInputState &state);
#endif
#ifdef USE_GKEY
std::unique_ptr< GKeyLibrary > m_gkey;
#endif
DeviceMap::iterator addDevice(const HANDLE deviceHandle);
};
uint qHash(const GUID &);
#endif

View File

@ -56,43 +56,25 @@ bool MumbleApplication::event(QEvent *e) {
}
#ifdef Q_OS_WIN
/// gswForward forwards a native Windows keyboard/mouse message
/// into GlobalShortcutWin's event stream.
///
/// @return Returns true if the forwarded event was suppressed
/// by GlobalShortcutWin. Otherwise, returns false.
static bool gswForward(MSG *msg) {
GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(GlobalShortcutEngine::engine);
bool MumbleApplication::nativeEventFilter(const QByteArray &, void *message, long *) {
auto gsw = static_cast< GlobalShortcutWin * >(GlobalShortcutEngine::engine);
if (!gsw) {
return false;
}
switch (msg->message) {
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
return gsw->injectMouseMessage(msg);
case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
return gsw->injectKeyboardMessage(msg);
}
return false;
}
bool MumbleApplication::nativeEventFilter(const QByteArray &, void *message, long *) {
MSG *msg = reinterpret_cast< MSG * >(message);
if (QThread::currentThread() == thread()) {
bool suppress = gswForward(msg);
if (suppress) {
return true;
}
auto msg = reinterpret_cast< const MSG * >(message);
switch (msg->message) {
case WM_INPUT:
gsw->injectRawInputMessage(reinterpret_cast< HRAWINPUT >(msg->lParam));
break;
case WM_INPUT_DEVICE_CHANGE:
// We don't care about GIDC_ARRIVAL because we add a device only when we receive input from it.
if (msg->wParam == GIDC_REMOVAL) {
// The device is not available anymore, free resources allocated for it.
gsw->deviceRemoved(reinterpret_cast< const HANDLE >(msg->lParam));
}
}
return false;
}
#endif

View File

@ -516,8 +516,6 @@ Settings::Settings() {
bEnableXInput2 = true;
bEnableGKey = false;
bEnableXboxInput = true;
bEnableWinHooks = true;
bDirectInputVerboseLogging = false;
bEnableUIAccess = true;
for (int i = Log::firstMsgType; i <= Log::lastMsgType; ++i) {
@ -975,8 +973,6 @@ void Settings::load(QSettings *settings_ptr) {
LOAD(bEnableXInput2, "shortcut/x11/xinput2/enable");
LOAD(bEnableGKey, "shortcut/gkey");
LOAD(bEnableXboxInput, "shortcut/windows/xbox/enable");
LOAD(bEnableWinHooks, "winhooks");
LOAD(bDirectInputVerboseLogging, "shortcut/windows/directinput/verboselogging");
LOAD(bEnableUIAccess, "shortcut/windows/uiaccess/enable");
int nshorts = settings_ptr->beginReadArray(QLatin1String("shortcuts"));
@ -1371,8 +1367,6 @@ void Settings::save() {
SAVE(bEnableXInput2, "shortcut/x11/xinput2/enable");
SAVE(bEnableGKey, "shortcut/gkey");
SAVE(bEnableXboxInput, "shortcut/windows/xbox/enable");
SAVE(bEnableWinHooks, "winhooks");
SAVE(bDirectInputVerboseLogging, "shortcut/windows/directinput/verboselogging");
SAVE(bEnableUIAccess, "shortcut/windows/uiaccess/enable");
settings_ptr->beginWriteArray(QLatin1String("shortcuts"));

View File

@ -284,11 +284,8 @@ struct Settings {
bool bEnableXInput2;
bool bEnableGKey;
bool bEnableXboxInput;
bool bEnableWinHooks;
/// Enable verbose logging in GlobalShortcutWin's DirectInput backend.
bool bDirectInputVerboseLogging;
/// Enable use of UIAccess (Windows's UI automation feature). This allows
/// Mumble greater access to global shortcuts.
/// Enable use of UIAccess (Windows's UI automation feature). This allows Mumble to receive WM_INPUT messages when
/// an application with elevated privileges is in foreground.
bool bEnableUIAccess;
QList< Shortcut > qlShortcuts;

View File

@ -680,8 +680,8 @@ int main(int argc, char **argv) {
#ifdef Q_OS_WIN
// Set mumble_mw_hwnd in os_win.cpp.
// Used by APIs in ASIOInput and GlobalShortcut_win that require a HWND.
mumble_mw_hwnd = GetForegroundWindow();
// Used in ASIOInput and GlobalShortcut_win by APIs that require a HWND.
mumble_mw_hwnd = reinterpret_cast< HWND >(Global::get().mw->winId());
#endif
#ifdef USE_DBUS