open_toontown_panda3d/panda/src/device/winInputDeviceManager.cxx

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