576 lines
16 KiB
C++
576 lines
16 KiB
C++
/**
|
|
* PANDA 3D SOFTWARE
|
|
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
|
*
|
|
* All use of this software is subject to the terms of the revised BSD
|
|
* license. You should have received a copy of this license along
|
|
* with this source code in a file named "LICENSE."
|
|
*
|
|
* @file winInputDeviceManager.cxx
|
|
* @author rdb
|
|
* @date 2018-01-21
|
|
*/
|
|
|
|
#include "winInputDeviceManager.h"
|
|
#include "winRawInputDevice.h"
|
|
#include "throw_event.h"
|
|
#include "phidsdi.h"
|
|
|
|
#if defined(_WIN32) && !defined(CPPPARSER)
|
|
|
|
#ifdef HAVE_THREADS
|
|
/**
|
|
*
|
|
*/
|
|
class InputThread : public Thread {
|
|
public:
|
|
InputThread(WinInputDeviceManager *manager) :
|
|
Thread("input", "input"), _manager(manager) {}
|
|
|
|
private:
|
|
virtual void thread_main();
|
|
|
|
WinInputDeviceManager *_manager;
|
|
};
|
|
#endif
|
|
|
|
/**
|
|
* Initializes the input device manager by scanning which devices are currently
|
|
* connected and setting up any platform-dependent structures necessary for
|
|
* listening for future device connect events.
|
|
*/
|
|
WinInputDeviceManager::
|
|
WinInputDeviceManager() :
|
|
_xinput_device0(0),
|
|
_xinput_device1(1),
|
|
_xinput_device2(2),
|
|
_xinput_device3(3),
|
|
_message_hwnd(nullptr) {
|
|
|
|
// XInput provides four device slots, so we simply create four XInputDevice
|
|
// objects that are bound to the lifetime of the input manager.
|
|
_xinput_device0.local_object();
|
|
_xinput_device1.local_object();
|
|
_xinput_device2.local_object();
|
|
_xinput_device3.local_object();
|
|
|
|
// This function is not available in the 7.1A SDK, so we use a wrapper.
|
|
HMODULE module = LoadLibraryA("cfgmgr32.dll");
|
|
if (module) {
|
|
_CM_Get_DevNode_PropertyW = (pCM_Get_DevNode_Property)GetProcAddress(module, "CM_Get_DevNode_PropertyW");
|
|
} else {
|
|
_CM_Get_DevNode_PropertyW = nullptr;
|
|
}
|
|
|
|
// If we have threading enabled, start a thread with a message-only window
|
|
// loop to listen for input events. We can't actually just let this be
|
|
// handled by the main window loop, because the main window might actually
|
|
// have been created in a different thread.
|
|
#ifdef HAVE_THREADS
|
|
if (Thread::is_threading_supported()) {
|
|
PT(Thread) thread = new InputThread(this);
|
|
thread->start(TP_normal, false);
|
|
} else
|
|
#endif
|
|
{
|
|
setup_message_loop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes any resources that the device manager was using to listen for events.
|
|
*/
|
|
WinInputDeviceManager::
|
|
~WinInputDeviceManager() {
|
|
if (_message_hwnd != nullptr) {
|
|
#ifdef HAVE_THREADS
|
|
if (Thread::is_threading_supported()) {
|
|
HWND hwnd = _message_hwnd;
|
|
if (hwnd) {
|
|
SendMessage(hwnd, WM_QUIT, 0, 0);
|
|
}
|
|
} else
|
|
#endif
|
|
{
|
|
destroy_message_loop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the raw input device destructor.
|
|
*/
|
|
void WinInputDeviceManager::
|
|
device_destroyed(WinRawInputDevice *device) {
|
|
LightMutexHolder holder(_lock);
|
|
// It shouldn't be in here, but let's check to be sure.
|
|
if (device->_handle != nullptr) {
|
|
_raw_devices.erase(device->_handle);
|
|
}
|
|
|
|
_raw_devices_by_path.erase(device->_path);
|
|
}
|
|
|
|
/**
|
|
* Called upon receiving a WM_INPUT message.
|
|
*/
|
|
void WinInputDeviceManager::
|
|
on_input(HRAWINPUT handle) {
|
|
UINT size;
|
|
if (GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)) < 0) {
|
|
return;
|
|
}
|
|
|
|
PRAWINPUT data = (PRAWINPUT)alloca(size);
|
|
if (GetRawInputData(handle, RID_INPUT, data, &size, sizeof(RAWINPUTHEADER)) <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Look up the device in the map.
|
|
PT(WinRawInputDevice) device;
|
|
{
|
|
LightMutexHolder holder(_lock);
|
|
auto it = _raw_devices.find(data->header.hDevice);
|
|
if (it != _raw_devices.end()) {
|
|
device = it->second;
|
|
}
|
|
}
|
|
if (device != nullptr) {
|
|
device->on_input(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called upon receiving WM_INPUT_DEVICE_CHANGE with wparam GIDC_ARRIVAL.
|
|
*/
|
|
void WinInputDeviceManager::
|
|
on_input_device_arrival(HANDLE handle) {
|
|
// Get the device path.
|
|
UINT size;
|
|
if (GetRawInputDeviceInfoA(handle, RIDI_DEVICENAME, nullptr, &size) != 0) {
|
|
return;
|
|
}
|
|
|
|
char *path = (char *)alloca(size);
|
|
if (path == nullptr ||
|
|
GetRawInputDeviceInfoA(handle, RIDI_DEVICENAME, (void *)path, &size) < 0) {
|
|
return;
|
|
}
|
|
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug()
|
|
<< "GIDC_ARRIVAL: " << path << "\n";
|
|
}
|
|
|
|
// Get the device info.
|
|
RID_DEVICE_INFO info;
|
|
info.cbSize = sizeof(RID_DEVICE_INFO);
|
|
size = sizeof(RID_DEVICE_INFO);
|
|
if (GetRawInputDeviceInfoA(handle, RIDI_DEVICEINFO, &info, &size) <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Strip the \\?\ prefix from the path.
|
|
while (path[0] == '\\' || path[0] == '?' || path[0] == '.') {
|
|
++path;
|
|
}
|
|
|
|
// Now, replace # with \\ in the path, but only up to three components.
|
|
char *p = path;
|
|
int i = 0;
|
|
while (*p != '\0') {
|
|
if (*p == '#') {
|
|
if (i++ < 2) {
|
|
*p = '\\';
|
|
} else {
|
|
*p = '\0';
|
|
break;
|
|
}
|
|
}
|
|
++p;
|
|
}
|
|
|
|
// Find the device node, which will be something like "HID\VID_0123..."
|
|
// Then we walk the device tree upward to get the USB node, which which will
|
|
// be something like a "USB\VID..." node, from which we can fetch the real
|
|
// USB device information (such as the product name).
|
|
std::string name, manufacturer;
|
|
DEVINST inst;
|
|
CONFIGRET ret = CM_Locate_DevNodeA(&inst, (DEVINSTID_A)path, CM_LOCATE_DEVNODE_PHANTOM);
|
|
if (ret == CR_SUCCESS) {
|
|
char buffer[4096];
|
|
ULONG buflen = 4096;
|
|
if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_DEVICEDESC, 0, buffer, &buflen, 0) == CR_SUCCESS) {
|
|
name.assign(buffer);
|
|
}
|
|
buflen = 4096;
|
|
if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_MFG, 0, buffer, &buflen, 0) == CR_SUCCESS) {
|
|
if (strcmp(buffer, "(Standard system devices)") != 0) {
|
|
manufacturer.assign(buffer);
|
|
}
|
|
}
|
|
|
|
// Now walk the device tree upwards so fetch the bus-reported name of the
|
|
// parent USB device, which we prefer over the regular device description
|
|
// that is probably boring like "HID-compliant game controller".
|
|
DEVINST cur = inst;
|
|
DEVINST parent;
|
|
while (CM_Get_Parent(&parent, cur, 0) == CR_SUCCESS) {
|
|
buflen = 4096;
|
|
std::string dev_class;
|
|
if (CM_Get_DevNode_Registry_Property(parent, CM_DRP_CLASS, 0, buffer, &buflen, 0) == CR_SUCCESS) {
|
|
if (strcmp(buffer, "USB") == 0) {
|
|
// This is some generic USB device, like a hub. We've gone too far.
|
|
break;
|
|
}
|
|
dev_class.assign(buffer);
|
|
}
|
|
cur = parent;
|
|
|
|
// While we're at it, maybe this one defines a manufacturer?
|
|
buflen = 4096;
|
|
if (manufacturer.empty() &&
|
|
CM_Get_DevNode_Registry_Property(cur, CM_DRP_MFG, 0, buffer, &buflen, 0) == CR_SUCCESS) {
|
|
if (strcmp(buffer, "(Standard system devices)") != 0) {
|
|
manufacturer.assign(buffer);
|
|
}
|
|
}
|
|
|
|
// If it's a generic HID device, take the name from the USB bus.
|
|
// See devpkey.h for the available property keys.
|
|
static const DEVPROPKEY bus_reported_device_desc = {
|
|
{0x540b947e, 0x8b40, 0x45bc, {0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2}},
|
|
4,
|
|
};
|
|
DEVPROPTYPE type;
|
|
buflen = 4096;
|
|
if (dev_class == "HIDClass" && _CM_Get_DevNode_PropertyW != nullptr &&
|
|
_CM_Get_DevNode_PropertyW(cur, &bus_reported_device_desc, &type, (PBYTE)buffer, &buflen, 0) == CR_SUCCESS &&
|
|
type == DEVPROP_TYPE_STRING) {
|
|
|
|
// Some devices insert quite some trailing space here.
|
|
wchar_t *wbuffer = (wchar_t *)buffer;
|
|
size_t wlen = wcsnlen_s(wbuffer, sizeof(buffer) / sizeof(wchar_t));
|
|
while (wlen > 0 && iswspace(wbuffer[wlen - 1])) {
|
|
wbuffer[--wlen] = 0;
|
|
}
|
|
TextEncoder encoder;
|
|
name.assign(encoder.encode_wtext(std::wstring(wbuffer, wlen)));
|
|
break;
|
|
} else {
|
|
buflen = 4096;
|
|
if (CM_Get_DevNode_Registry_Property(cur, CM_DRP_DEVICEDESC, 0, buffer, &buflen, 0) == CR_SUCCESS) {
|
|
// We'll pass if it has this awfully boring name. Is there a
|
|
// language-independent way to check this?
|
|
if (strcmp(buffer, "USB Input Device") != 0) {
|
|
name.assign(buffer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (device_cat.is_debug()) {
|
|
// No big deal, we just won't be able to get the name.
|
|
device_cat.debug()
|
|
<< "Could not locate device node " << path << " (" << ret << ")\n";
|
|
}
|
|
|
|
// Is this an XInput device? If so, handle it via XInput, which allows us
|
|
// to handle independent left/right triggers as well as vibration output.
|
|
if (info.dwType == RIM_TYPEHID && strstr(path, "&IG_") != nullptr &&
|
|
XInputDevice::init_xinput()) {
|
|
// This is a device we should handle via the XInput API. Check which of
|
|
// the four players was the lucky one.
|
|
if (_xinput_device0.check_arrival(info, inst, name, manufacturer)) {
|
|
add_device(&_xinput_device0);
|
|
}
|
|
if (_xinput_device1.check_arrival(info, inst, name, manufacturer)) {
|
|
add_device(&_xinput_device1);
|
|
}
|
|
if (_xinput_device2.check_arrival(info, inst, name, manufacturer)) {
|
|
add_device(&_xinput_device2);
|
|
}
|
|
if (_xinput_device3.check_arrival(info, inst, name, manufacturer)) {
|
|
add_device(&_xinput_device3);
|
|
}
|
|
return;
|
|
}
|
|
|
|
LightMutexHolder holder(_lock);
|
|
|
|
// Do we have a device by this path already? This can happen if the
|
|
// user keeps around a pointer to a disconnected device in the hope that
|
|
// it will reconnect later.
|
|
PT(WinRawInputDevice) device;
|
|
auto it = _raw_devices_by_path.find(path);
|
|
if (it != _raw_devices_by_path.end()) {
|
|
device = it->second;
|
|
} else {
|
|
device = new WinRawInputDevice(this, path);
|
|
_raw_devices_by_path[path] = device;
|
|
}
|
|
|
|
if (device->on_arrival(handle, info, move(name))) {
|
|
_raw_devices[handle] = device;
|
|
_connected_devices.add_device(device);
|
|
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug()
|
|
<< "Discovered input device " << *device << "\n";
|
|
}
|
|
throw_event("connect-device", device.p());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called upon receiving WM_INPUT_DEVICE_CHANGE with wparam GIDC_REMOVAL.
|
|
*/
|
|
void WinInputDeviceManager::
|
|
on_input_device_removal(HANDLE handle) {
|
|
// The handle will probably no longer be valid after this, so find the
|
|
// device and remove it from _raw_devices. However, we keep it in
|
|
// _raw_devices_by_path in case there's still a pointer around to it.
|
|
|
|
// We keep the pointer outside the lock because the input device
|
|
// destructor calls back to InputDeviceManager.
|
|
PT(WinRawInputDevice) device;
|
|
{
|
|
LightMutexHolder holder(_lock);
|
|
auto it = _raw_devices.find(handle);
|
|
if (it != _raw_devices.end()) {
|
|
device = std::move(it->second);
|
|
_raw_devices.erase(it);
|
|
device->on_removal();
|
|
|
|
if (_connected_devices.remove_device(device)) {
|
|
throw_event("disconnect-device", device.p());
|
|
}
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug()
|
|
<< "Removed input device " << *device << "\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Polls the system to see if there are any new devices. In some
|
|
* implementations this is a no-op.
|
|
*/
|
|
void WinInputDeviceManager::
|
|
update() {
|
|
}
|
|
|
|
/**
|
|
* Sets up a Windows message loop. Should be called from the thread that will
|
|
* be handling the messages.
|
|
*/
|
|
HWND WinInputDeviceManager::
|
|
setup_message_loop() {
|
|
_message_hwnd = 0;
|
|
|
|
// Now create a message-only window for the raw input.
|
|
WNDCLASSEX wc = {};
|
|
wc.cbSize = sizeof(WNDCLASSEX);
|
|
wc.lpfnWndProc = window_proc;
|
|
wc.hInstance = GetModuleHandle(nullptr);
|
|
wc.lpszClassName = "InputDeviceManager";
|
|
if (!RegisterClassEx(&wc)) {
|
|
device_cat.warning()
|
|
<< "Failed to register message-only window class for input device detection.\n";
|
|
} else {
|
|
_message_hwnd = CreateWindowEx(0, wc.lpszClassName, "InputDeviceManager", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
|
|
if (!_message_hwnd) {
|
|
device_cat.warning()
|
|
<< "Failed to create message-only window for input device detection.\n";
|
|
}
|
|
}
|
|
|
|
// Now listen for raw input devices using the created message loop.
|
|
RAWINPUTDEVICE rid[4];
|
|
rid[0].usUsagePage = 1;
|
|
rid[0].usUsage = 4; // Joysticks
|
|
rid[0].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
|
rid[0].hwndTarget = _message_hwnd;
|
|
rid[1].usUsagePage = 1;
|
|
rid[1].usUsage = 5; // Gamepads
|
|
rid[1].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
|
rid[1].hwndTarget = _message_hwnd;
|
|
rid[2].usUsagePage = 1;
|
|
rid[2].usUsage = 8; // Multi-axis controllers (including 3D mice)
|
|
rid[2].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
|
rid[2].hwndTarget = _message_hwnd;
|
|
rid[3].usUsagePage = HID_USAGE_PAGE_DIGITIZER;
|
|
rid[3].usUsage = 1; // Digitizers
|
|
rid[3].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
|
rid[3].hwndTarget = _message_hwnd;
|
|
if (!RegisterRawInputDevices(rid, 4, sizeof(RAWINPUTDEVICE))) {
|
|
device_cat.warning()
|
|
<< "Failed to register raw input devices.\n";
|
|
}
|
|
|
|
// Do we have any XInput devices plugged in now?
|
|
int num_xinput = 0;
|
|
HANDLE xinput_handle;
|
|
RAWINPUTDEVICELIST devices[64];
|
|
UINT num_devices = 64;
|
|
num_devices = GetRawInputDeviceList(devices, &num_devices, sizeof(RAWINPUTDEVICELIST));
|
|
if (num_devices == (UINT)-1) {
|
|
num_devices = 0;
|
|
}
|
|
for (UINT i = 0; i < num_devices; ++i) {
|
|
if (devices[i].dwType != RIM_TYPEHID) {
|
|
continue;
|
|
}
|
|
HANDLE handle = devices[i].hDevice;
|
|
UINT size;
|
|
if (GetRawInputDeviceInfoA(handle, RIDI_DEVICENAME, nullptr, &size) != 0) {
|
|
continue;
|
|
}
|
|
|
|
char *path = (char *)alloca(size);
|
|
if (path == nullptr ||
|
|
GetRawInputDeviceInfoA(handle, RIDI_DEVICENAME, (void *)path, &size) < 0) {
|
|
continue;
|
|
}
|
|
|
|
if (strstr(path, "&IG_") != nullptr) {
|
|
xinput_handle = handle;
|
|
++num_xinput;
|
|
}
|
|
}
|
|
if (num_xinput == 1) {
|
|
// There's only one XInput device, so we know which one it is.
|
|
on_input_device_arrival(xinput_handle);
|
|
} else if (num_xinput > 0) {
|
|
// Just poll all the XInput devices.
|
|
_xinput_device0.detect(this);
|
|
_xinput_device1.detect(this);
|
|
_xinput_device2.detect(this);
|
|
_xinput_device3.detect(this);
|
|
}
|
|
|
|
return _message_hwnd;
|
|
}
|
|
|
|
/**
|
|
* Tears down the message loop. Should be called from the thread that called
|
|
* setup_message_loop().
|
|
*/
|
|
void WinInputDeviceManager::
|
|
destroy_message_loop() {
|
|
HWND hwnd = nullptr;
|
|
{
|
|
LightMutexHolder holder(_lock);
|
|
std::swap(_message_hwnd, hwnd);
|
|
}
|
|
|
|
if (hwnd) {
|
|
DestroyWindow(hwnd);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a signal to the thread input thread, asking it to shut itself down.
|
|
*/
|
|
void WinInputDeviceManager::
|
|
stop_thread() {
|
|
#ifdef HAVE_THREADS
|
|
WinInputDeviceManager *mgr = (WinInputDeviceManager *)_global_ptr;
|
|
if (mgr != nullptr) {
|
|
LightMutexHolder holder(mgr->_lock);
|
|
HWND hwnd = mgr->_message_hwnd;
|
|
if (hwnd) {
|
|
PostMessage(hwnd, WM_QUIT, 0, 0);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Implementation of the message loop.
|
|
*/
|
|
LRESULT WINAPI WinInputDeviceManager::
|
|
window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
|
|
WinInputDeviceManager *mgr;
|
|
switch (msg) {
|
|
case WM_INPUT:
|
|
mgr = (WinInputDeviceManager *)InputDeviceManager::get_global_ptr();
|
|
if (mgr != nullptr) {
|
|
mgr->on_input((HRAWINPUT)lparam);
|
|
}
|
|
break;
|
|
|
|
case WM_INPUT_DEVICE_CHANGE:
|
|
switch (LOWORD(wparam)) {
|
|
case GIDC_ARRIVAL:
|
|
mgr = (WinInputDeviceManager *)InputDeviceManager::get_global_ptr();
|
|
if (mgr != nullptr) {
|
|
mgr->on_input_device_arrival((HANDLE)lparam);
|
|
}
|
|
break;
|
|
|
|
case GIDC_REMOVAL:
|
|
mgr = (WinInputDeviceManager *)InputDeviceManager::get_global_ptr();
|
|
if (mgr != nullptr) {
|
|
mgr->on_input_device_removal((HANDLE)lparam);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
|
}
|
|
|
|
#ifdef HAVE_THREADS
|
|
/**
|
|
* Thread entry point for the input listener thread.
|
|
*/
|
|
void InputThread::
|
|
thread_main() {
|
|
WinInputDeviceManager *manager = _manager;
|
|
HWND hwnd = manager->setup_message_loop();
|
|
if (!hwnd) {
|
|
return;
|
|
}
|
|
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug()
|
|
<< "Started input device listener thread.\n";
|
|
}
|
|
|
|
MSG msg;
|
|
#ifdef SIMPLE_THREADS
|
|
// In the simple threading case, we can't block the thread waiting for a
|
|
// message; we yield control back if there are no more messages.
|
|
while (true) {
|
|
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
|
if (msg.message == WM_QUIT) {
|
|
break;
|
|
}
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
} else {
|
|
Thread::force_yield();
|
|
}
|
|
}
|
|
#else
|
|
while (GetMessage(&msg, nullptr, 0, 0) > 0) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
}
|
|
#endif
|
|
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug()
|
|
<< "Stopping input device listener thread.\n";
|
|
}
|
|
|
|
manager->destroy_message_loop();
|
|
}
|
|
#endif // HAVE_THREADS
|
|
|
|
#endif
|