diff --git a/dtool/src/parser-inc/XInput.h b/dtool/src/parser-inc/XInput.h index 21a2802287..0674961fc0 100644 --- a/dtool/src/parser-inc/XInput.h +++ b/dtool/src/parser-inc/XInput.h @@ -1,3 +1,5 @@ -struct XINPUT_STATE; -struct XINPUT_CAPABILITIES; -struct XINPUT_VIBRATION; +typedef struct _XINPUT_STATE XINPUT_STATE, *PXINPUT_STATE; +typedef struct _XINPUT_CAPABILITIES XINPUT_CAPABILITIES, *PXINPUT_CAPABILITIES; +typedef struct _XINPUT_VIBRATION XINPUT_VIBRATION, *PXINPUT_VIBRATION; +typedef struct _XINPUT_GAMEPAD XINPUT_GAMEPAD, *PXINPUT_GAMEPAD; +typedef struct _XINPUT_KEYSTROKE XINPUT_KEYSTROKE, *PXINPUT_KEYSTROKE; diff --git a/panda/src/device/inputDevice.cxx b/panda/src/device/inputDevice.cxx index 76aa79dc58..1aa2a03ca4 100644 --- a/panda/src/device/inputDevice.cxx +++ b/panda/src/device/inputDevice.cxx @@ -13,6 +13,10 @@ #include "inputDevice.h" +#if defined(_MSC_VER) && _MSC_VER < 1700 +#define fma(a, b, c) ((a) * (b) + (c)) +#endif + TypeHandle InputDevice::_type_handle; /** @@ -29,7 +33,8 @@ InputDevice(const string &name, DeviceClass dev_class, int flags) : _event_sequence(0), _enable_pointer_events(false), _battery_level(-1), - _max_battery_level(-1) + _max_battery_level(-1), + _lock("InputDevice") { _button_events = new ButtonEventList; } diff --git a/panda/src/device/inputDeviceManager.I b/panda/src/device/inputDeviceManager.I index b38dc6d2d5..731051aa3c 100644 --- a/panda/src/device/inputDeviceManager.I +++ b/panda/src/device/inputDeviceManager.I @@ -16,8 +16,10 @@ */ INLINE InputDeviceManager *InputDeviceManager:: get_global_ptr() { - if (_global_ptr == (InputDeviceManager *)NULL) { - _global_ptr = new InputDeviceManager; + if (_global_ptr != nullptr) { + return _global_ptr; + } else { + make_global_ptr(); + return _global_ptr; } - return _global_ptr; } diff --git a/panda/src/device/inputDeviceManager.cxx b/panda/src/device/inputDeviceManager.cxx index d7f6a04955..31f7f93934 100644 --- a/panda/src/device/inputDeviceManager.cxx +++ b/panda/src/device/inputDeviceManager.cxx @@ -14,6 +14,7 @@ #include "inputDeviceManager.h" #include "ioKitInputDevice.h" #include "linuxJoystickDevice.h" +#include "winInputDeviceManager.h" #include "throw_event.h" #ifdef PHAVE_LINUX_INPUT_H @@ -23,12 +24,6 @@ #include #endif -static ConfigVariableDouble xinput_detection_delay -("xinput-detection-delay", 0.5, - PRC_DESC("How many seconds to wait between each check to see whether " - "an XInput has been connected. This check is not done every " - "frame in order to prevent slowdown.")); - InputDeviceManager *InputDeviceManager::_global_ptr = NULL; /** @@ -80,35 +75,6 @@ InputDeviceManager() : _inotify_fd(-1) { return; } } -#elif defined(_WIN32) -InputDeviceManager:: -InputDeviceManager() : - _xinput_device0(0), - _xinput_device1(1), - _xinput_device2(2), - _xinput_device3(3), - _last_detection(0.0) { - - // 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(); - - if (_xinput_device0.is_connected()) { - _connected_devices.add_device(&_xinput_device0); - } - if (_xinput_device1.is_connected()) { - _connected_devices.add_device(&_xinput_device1); - } - if (_xinput_device2.is_connected()) { - _connected_devices.add_device(&_xinput_device2); - } - if (_xinput_device3.is_connected()) { - _connected_devices.add_device(&_xinput_device3); - } -} #elif defined(__APPLE__) InputDeviceManager:: InputDeviceManager() { @@ -153,7 +119,7 @@ InputDeviceManager() { } #else InputDeviceManager:: -InputDeviceManager() { +InputDeviceManager() : _lock("InputDeviceManager") { } #endif @@ -174,6 +140,18 @@ InputDeviceManager:: #endif } +/** + * Creates the global input manager. + */ +void InputDeviceManager:: +make_global_ptr() { +#ifdef _WIN32 + _global_ptr = new WinInputDeviceManager; +#else + _global_ptr = new InputDeviceManager; +#endif +} + #ifdef PHAVE_LINUX_INPUT_H /** * Checks whether the event device with the given index is accessible, and if @@ -455,24 +433,6 @@ update() { ptr += sizeof(inotify_event) + event->len; } #endif - -#ifdef _WIN32 - // XInput doesn't provide a very good hot-plugging interface. We just check - // whether it's connected every so often. Perhaps we can switch to using - // RegisterDeviceNotification in the future. - double time_now = ClockObject::get_global_clock()->get_real_time(); - LightMutexHolder holder(_update_lock); - - if (time_now - _last_detection > xinput_detection_delay.get_value()) { - // I've heard this can be quite slow if no device is detected. We - // should probably move it to a thread. - _xinput_device0.detect(this); - _xinput_device1.detect(this); - _xinput_device2.detect(this); - _xinput_device3.detect(this); - _last_detection = time_now; - } -#endif } #if defined(__APPLE__) && !defined(CPPPARSER) diff --git a/panda/src/device/inputDeviceManager.h b/panda/src/device/inputDeviceManager.h index a262b60d28..c6c50a33d9 100644 --- a/panda/src/device/inputDeviceManager.h +++ b/panda/src/device/inputDeviceManager.h @@ -21,6 +21,7 @@ #ifdef _WIN32 #include "xinputDevice.h" +class WinRawInputDevice; #endif #ifdef __APPLE__ @@ -32,10 +33,12 @@ * when a device has been hot-plugged. */ class EXPCL_PANDA_DEVICE InputDeviceManager { -private: +protected: InputDeviceManager(); ~InputDeviceManager(); + static void make_global_ptr(); + #ifdef PHAVE_LINUX_INPUT_H InputDevice *consider_add_evdev_device(int index); InputDevice *consider_add_js_device(int index); @@ -48,14 +51,14 @@ PUBLISHED: void add_device(InputDevice *device); void remove_device(InputDevice *device); - void update(); + virtual void update(); INLINE static InputDeviceManager *get_global_ptr(); // The set of all currently connected devices. MAKE_PROPERTY(devices, get_devices); -private: +protected: LightMutex _lock; #ifdef PHAVE_LINUX_INPUT_H @@ -65,16 +68,6 @@ private: InputDeviceSet _inactive_devices; #endif -#ifdef _WIN32 - // There are always exactly four of these in existence. - LightMutex _update_lock; - XInputDevice _xinput_device0; - XInputDevice _xinput_device1; - XInputDevice _xinput_device2; - XInputDevice _xinput_device3; - double _last_detection; -#endif - #if defined(__APPLE__) && !defined(CPPPARSER) IOHIDManagerRef _hid_manager; diff --git a/panda/src/device/inputDeviceNode.cxx b/panda/src/device/inputDeviceNode.cxx index 662b2dbac8..e68fd8652b 100644 --- a/panda/src/device/inputDeviceNode.cxx +++ b/panda/src/device/inputDeviceNode.cxx @@ -61,11 +61,11 @@ do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &, } // calculate the battery level in percent and set a warning if the level is to low - if (_device->has_battery()) { + /*if (_device->has_battery()) { short bl = _device->get_battery_level(); short bl_percent = bl / (_device->get_max_battery_level() / (short)100); if (bl_percent <= low_battery_level) { output.set_data(_low_battery_event_output, EventParameter(1)); } - } + }*/ } diff --git a/panda/src/device/p3device_composite1.cxx b/panda/src/device/p3device_composite1.cxx index b9895f7880..65a03ccc48 100644 --- a/panda/src/device/p3device_composite1.cxx +++ b/panda/src/device/p3device_composite1.cxx @@ -12,3 +12,5 @@ #include "inputDeviceSet.cxx" #include "ioKitInputDevice.cxx" #include "linuxJoystickDevice.cxx" +#include "winInputDeviceManager.cxx" +#include "winRawInputDevice.cxx" diff --git a/panda/src/device/winInputDeviceManager.cxx b/panda/src/device/winInputDeviceManager.cxx new file mode 100644 index 0000000000..124229b5ea --- /dev/null +++ b/panda/src/device/winInputDeviceManager.cxx @@ -0,0 +1,384 @@ +/** + * 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" + +#if defined(_WIN32) && !defined(CPPPARSER) + +/** + * 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 only available in Vista and later, 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; + } + + // 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.\n"; + return; + } + + _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.\n"; + return; + } + + // Now listen for raw input devices using the created message loop. + RAWINPUTDEVICE rid[3]; + 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; + if (!RegisterRawInputDevices(rid, 3, sizeof(RAWINPUTDEVICE))) { + device_cat.warning() + << "Failed to register raw input devices.\n"; + } +} + +/** + * Closes any resources that the device manager was using to listen for events. + */ +WinInputDeviceManager:: +~WinInputDeviceManager() { + if (_message_hwnd != nullptr) { + DestroyWindow(_message_hwnd); + _message_hwnd = nullptr; + } +} + +/** + * 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). + 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; + 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) { + TextEncoder encoder; + name.assign(encoder.encode_wtext((wchar_t *)buffer)); + 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) { + // This is a device we should handle via the XInput API. Check which of + // the four players was the lucky one. + WinRawInputDevice idev(this, path); + 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 = 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"; + } + } + } +} + +/** + * 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); +} + +#endif diff --git a/panda/src/device/winInputDeviceManager.h b/panda/src/device/winInputDeviceManager.h new file mode 100644 index 0000000000..de2b0dc04e --- /dev/null +++ b/panda/src/device/winInputDeviceManager.h @@ -0,0 +1,64 @@ +/** + * 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.h + * @author rdb + * @date 2018-01-21 + */ + +#ifndef WININPUTDEVICEMANAGER_H +#define WININPUTDEVICEMANAGER_H + +#include "inputDeviceManager.h" + +#if defined(_WIN32) && !defined(CPPPARSER) + +#include "xinputDevice.h" + +#include +#include + +class WinRawInputDevice; + +/** + * This is the Windows implementation of InputDeviceManager, managing both + * XInput controllers and raw input devices. + */ +class EXPCL_PANDA_DEVICE WinInputDeviceManager FINAL : public InputDeviceManager { +private: + WinInputDeviceManager(); + ~WinInputDeviceManager(); + +public: + void device_destroyed(WinRawInputDevice *device); + + void on_input(HRAWINPUT handle); + void on_input_device_arrival(HANDLE handle); + void on_input_device_removal(HANDLE handle); + +private: + // There are always exactly four of these in existence. + XInputDevice _xinput_device0; + XInputDevice _xinput_device1; + XInputDevice _xinput_device2; + XInputDevice _xinput_device3; + + HWND _message_hwnd; + pmap _raw_devices; + pmap _raw_devices_by_path; + + static LRESULT WINAPI window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + + typedef CONFIGRET (*pCM_Get_DevNode_Property)(DEVINST, const DEVPROPKEY *, DEVPROPTYPE *, PBYTE, PULONG, ULONG); + pCM_Get_DevNode_Property _CM_Get_DevNode_PropertyW; + + friend class InputDeviceManager; +}; + +#endif +#endif diff --git a/panda/src/device/winRawInputDevice.cxx b/panda/src/device/winRawInputDevice.cxx new file mode 100644 index 0000000000..68e8e89bf3 --- /dev/null +++ b/panda/src/device/winRawInputDevice.cxx @@ -0,0 +1,655 @@ +/** + * 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 winRawInputDevice.cxx + * @author rdb + * @date 2018-01-19 + */ + +#include "winRawInputDevice.h" +#include "gamepadButton.h" +#include "mouseButton.h" + +#if defined(_WIN32) && !defined(CPPPARSER) + +#include +#include + +// Copy definitions from hidusage.h, until we can drop support for the 7.1 SDK +typedef USHORT USAGE, *PUSAGE; + +#define HID_USAGE_PAGE_UNDEFINED ((USAGE) 0x00) +#define HID_USAGE_PAGE_GENERIC ((USAGE) 0x01) +#define HID_USAGE_PAGE_SIMULATION ((USAGE) 0x02) +#define HID_USAGE_PAGE_VR ((USAGE) 0x03) +#define HID_USAGE_PAGE_SPORT ((USAGE) 0x04) +#define HID_USAGE_PAGE_GAME ((USAGE) 0x05) +#define HID_USAGE_PAGE_KEYBOARD ((USAGE) 0x07) +#define HID_USAGE_PAGE_LED ((USAGE) 0x08) +#define HID_USAGE_PAGE_BUTTON ((USAGE) 0x09) + +#define HID_USAGE_GENERIC_POINTER ((USAGE) 0x01) +#define HID_USAGE_GENERIC_MOUSE ((USAGE) 0x02) +#define HID_USAGE_GENERIC_JOYSTICK ((USAGE) 0x04) +#define HID_USAGE_GENERIC_GAMEPAD ((USAGE) 0x05) +#define HID_USAGE_GENERIC_KEYBOARD ((USAGE) 0x06) +#define HID_USAGE_GENERIC_KEYPAD ((USAGE) 0x07) +#define HID_USAGE_GENERIC_SYSTEM_CTL ((USAGE) 0x80) + +#define HID_USAGE_GENERIC_X ((USAGE) 0x30) +#define HID_USAGE_GENERIC_Y ((USAGE) 0x31) +#define HID_USAGE_GENERIC_Z ((USAGE) 0x32) +#define HID_USAGE_GENERIC_RX ((USAGE) 0x33) +#define HID_USAGE_GENERIC_RY ((USAGE) 0x34) +#define HID_USAGE_GENERIC_RZ ((USAGE) 0x35) +#define HID_USAGE_GENERIC_SLIDER ((USAGE) 0x36) +#define HID_USAGE_GENERIC_DIAL ((USAGE) 0x37) +#define HID_USAGE_GENERIC_WHEEL ((USAGE) 0x38) +#define HID_USAGE_GENERIC_HATSWITCH ((USAGE) 0x39) + +// Copy definitions from hidpi.h, until we can drop support for the 7.1 SDK +#define HIDP_STATUS_SUCCESS ((NTSTATUS)(0x11 << 16)) + +typedef enum _HIDP_REPORT_TYPE { + HidP_Input, + HidP_Output, + HidP_Feature +} HIDP_REPORT_TYPE; + +typedef struct _HIDP_BUTTON_CAPS { + USAGE UsagePage; + UCHAR ReportID; + BOOLEAN IsAlias; + USHORT BitField; + USHORT LinkCollection; + USAGE LinkUsage; + USAGE LinkUsagePage; + BOOLEAN IsRange; + BOOLEAN IsStringRange; + BOOLEAN IsDesignatorRange; + BOOLEAN IsAbsolute; + ULONG Reserved[10]; + union { + struct { + USAGE UsageMin, UsageMax; + USHORT StringMin, StringMax; + USHORT DesignatorMin, DesignatorMax; + USHORT DataIndexMin, DataIndexMax; + } Range; + struct { + USAGE Usage, Reserved1; + USHORT StringIndex, Reserved2; + USHORT DesignatorIndex, Reserved3; + USHORT DataIndex, Reserved4; + } NotRange; + }; +} HIDP_BUTTON_CAPS, *PHIDP_BUTTON_CAPS; + +typedef struct _HIDP_VALUE_CAPS { + USAGE UsagePage; + UCHAR ReportID; + BOOLEAN IsAlias; + USHORT BitField; + USHORT LinkCollection; + USAGE LinkUsage; + USAGE LinkUsagePage; + BOOLEAN IsRange; + BOOLEAN IsStringRange; + BOOLEAN IsDesignatorRange; + BOOLEAN IsAbsolute; + BOOLEAN HasNull; + UCHAR Reserved; + USHORT BitSize; + USHORT ReportCount; + USHORT Reserved2[5]; + ULONG UnitsExp; + ULONG Units; + LONG LogicalMin, LogicalMax; + LONG PhysicalMin, PhysicalMax; + union { + struct { + USAGE UsageMin, UsageMax; + USHORT StringMin, StringMax; + USHORT DesignatorMin, DesignatorMax; + USHORT DataIndexMin, DataIndexMax; + } Range; + struct { + USAGE Usage, Reserved1; + USHORT StringIndex, Reserved2; + USHORT DesignatorIndex, Reserved3; + USHORT DataIndex, Reserved4; + } NotRange; + }; +} HIDP_VALUE_CAPS, *PHIDP_VALUE_CAPS; + +typedef PUCHAR PHIDP_REPORT_DESCRIPTOR; +typedef struct _HIDP_PREPARSED_DATA *PHIDP_PREPARSED_DATA; + +typedef struct _HIDP_CAPS { + USAGE Usage; + USAGE UsagePage; + USHORT InputReportByteLength; + USHORT OutputReportByteLength; + USHORT FeatureReportByteLength; + USHORT Reserved[17]; + USHORT NumberLinkCollectionNodes; + USHORT NumberInputButtonCaps; + USHORT NumberInputValueCaps; + USHORT NumberInputDataIndices; + USHORT NumberOutputButtonCaps; + USHORT NumberOutputValueCaps; + USHORT NumberOutputDataIndices; + USHORT NumberFeatureButtonCaps; + USHORT NumberFeatureValueCaps; + USHORT NumberFeatureDataIndices; +} HIDP_CAPS, *PHIDP_CAPS; + +typedef struct _HIDP_DATA { + USHORT DataIndex; + USHORT Reserved; + union { + ULONG RawValue; + BOOLEAN On; + }; +} HIDP_DATA, *PHIDP_DATA; + +typedef LONG NTSTATUS; +typedef NTSTATUS (*pHidP_GetCaps)(PHIDP_PREPARSED_DATA, PHIDP_CAPS); +typedef NTSTATUS (*pHidP_GetButtonCaps)(HIDP_REPORT_TYPE, PHIDP_BUTTON_CAPS, PUSHORT, PHIDP_PREPARSED_DATA); +typedef NTSTATUS (*pHidP_GetValueCaps)(HIDP_REPORT_TYPE, PHIDP_VALUE_CAPS, PUSHORT, PHIDP_PREPARSED_DATA); +typedef NTSTATUS (*pHidP_GetData)(HIDP_REPORT_TYPE, PHIDP_DATA, PULONG, PHIDP_PREPARSED_DATA, PCHAR, ULONG); +typedef ULONG (*pHidP_MaxDataListLength)(HIDP_REPORT_TYPE, PHIDP_PREPARSED_DATA); + +static pHidP_GetCaps _HidP_GetCaps = nullptr; +static pHidP_GetButtonCaps _HidP_GetButtonCaps = nullptr; +static pHidP_GetValueCaps _HidP_GetValueCaps = nullptr; +static pHidP_GetData _HidP_GetData = nullptr; +static pHidP_MaxDataListLength _HidP_MaxDataListLength = nullptr; + +/** + * Static method to initialize the HID parser library. We load it dynamically + * because the Windows 7.1 SDK doesn't ship hid.lib. + */ +static bool init_hidp() { + HMODULE module = LoadLibraryA("hid.dll"); + if (module) { + if (device_cat.is_debug()) { + device_cat.debug() + << "Successfully loaded hid.dll\n"; + } + + _HidP_GetCaps = (pHidP_GetCaps)GetProcAddress(module, "HidP_GetCaps"); + _HidP_GetButtonCaps = (pHidP_GetButtonCaps)GetProcAddress(module, "HidP_GetButtonCaps"); + _HidP_GetValueCaps = (pHidP_GetValueCaps)GetProcAddress(module, "HidP_GetValueCaps"); + _HidP_GetData = (pHidP_GetData)GetProcAddress(module, "HidP_GetData"); + _HidP_MaxDataListLength = (pHidP_MaxDataListLength)GetProcAddress(module, "HidP_MaxDataListLength"); + + if (_HidP_GetCaps == nullptr || _HidP_GetButtonCaps == nullptr || + _HidP_GetValueCaps == nullptr || _HidP_GetData == nullptr || + _HidP_MaxDataListLength == nullptr) { + device_cat.error() + << "Failed to locate function pointers in hid.dll\n"; + return false; + } + + return true; + } + + device_cat.error() + << "Failed to load hid.dll.\n"; + return false; +} + +/** + * Protected constructor. Given a raw device handle. + */ +WinRawInputDevice:: +WinRawInputDevice(WinInputDeviceManager *manager, const char *path) : + _manager(manager), + _path(path), + _max_data_count(0), + _preparsed(nullptr) { +} + +/** + * + */ +WinRawInputDevice:: +~WinRawInputDevice() { + // Unregister the device from the manager. + LightMutexHolder holder(_lock); + if (_manager != nullptr) { + _manager->device_destroyed(this); + } + if (_preparsed != nullptr) { + free(_preparsed); + _preparsed = nullptr; + } +} + +/** + * Called by InputDeviceManager when this device is connected. Returns true + * if the device was connected successfully. + */ +bool WinRawInputDevice:: +on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, string name) { + LightMutexHolder holder(_lock); + + _name = move(name); + + switch (info.dwType) { + case RIM_TYPEMOUSE: + _device_class = DC_mouse; + break; + + case RIM_TYPEKEYBOARD: + _device_class = DC_keyboard; + break; + + case RIM_TYPEHID: + _vendor_id = info.hid.dwVendorId; + _product_id = info.hid.dwProductId; + + // Gamepads + if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC && + info.hid.usUsage == HID_USAGE_GENERIC_GAMEPAD) { + _device_class = DC_gamepad; + + // Flight sticks + } else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC && + info.hid.usUsage == HID_USAGE_GENERIC_JOYSTICK) { + _device_class = DC_flight_stick; + + // Mice + } else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC && + info.hid.usUsage == HID_USAGE_GENERIC_MOUSE) { + _device_class = DC_mouse; + + // Keyboards + } else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC && + info.hid.usUsage == HID_USAGE_GENERIC_KEYBOARD) { + _device_class = DC_keyboard; + + // 3Dconnexion SpaceNavigator and friends. + } else if (_vendor_id == 0x046d && + (_product_id == 0xc623 || + _product_id == 0xc625 || + _product_id == 0xc626 || + _product_id == 0xc627 || + _product_id == 0xc628 || + _product_id == 0xc629 || + _product_id == 0xc62b)) { + _device_class = DC_3d_mouse; + } + break; + + default: + return false; + } + + // Initialize hid.dll, which provides the HID parser functions. + static bool hid_initialized = false; + if (!hid_initialized) { + if (!init_hidp()) { + return false; + } + hid_initialized = true; + } + + // Get the "preparsed data", which we can parse with the HID parser API. + UINT size = 0; + if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, nullptr, &size) < 0) { + return false; + } + + PHIDP_PREPARSED_DATA buffer = (PHIDP_PREPARSED_DATA)malloc(size); + if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, buffer, &size) <= 0) { + return false; + } + _preparsed = buffer; + + HIDP_CAPS caps; + if (_HidP_GetCaps(buffer, &caps) != HIDP_STATUS_SUCCESS) { + device_cat.warning() + << "Failed to get capabilities from HID preparsed data.\n"; + return false; + } + + // Prepare a mapping of data indices to button/control indices. + _indices.resize(caps.NumberInputDataIndices); + + _buttons.clear(); + _controls.clear(); + + USHORT num_button_caps = caps.NumberInputButtonCaps; + PHIDP_BUTTON_CAPS button_caps = (PHIDP_BUTTON_CAPS)alloca(num_button_caps * sizeof(HIDP_BUTTON_CAPS)); + _HidP_GetButtonCaps(HidP_Input, button_caps, &num_button_caps, buffer); + + for (USHORT i = 0; i < num_button_caps; ++i) { + HIDP_BUTTON_CAPS &cap = button_caps[i]; + int upper = 0; + if (cap.IsRange) { + upper = (cap.Range.UsageMax - cap.Range.UsageMin); + + if (device_cat.is_debug()) { + device_cat.debug() + << "Found button range: DataIndex=" << dec + << cap.Range.DataIndexMin << ".." << cap.Range.DataIndexMax + << ", ReportID=" << (int)cap.ReportID + << ", UsagePage=0x" << hex << cap.UsagePage + << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax + << dec << "\n"; + } + } else { + if (device_cat.is_debug()) { + device_cat.debug() + << "Found button: DataIndex=" << dec << cap.NotRange.DataIndex + << ", ReportID=" << dec << (int)cap.ReportID + << ", UsagePage=0x" << cap.UsagePage + << ", Usage=0x" << cap.NotRange.Usage + << dec << "\n"; + } + } + + // Windows will only tell us which buttons in a report are "on", so we + // need to keep track of which buttons exist in which report so that we + // can figure out which ones are off. + if (cap.ReportID >= _report_buttons.size()) { + _report_buttons.resize(cap.ReportID + 1); + } + for (int j = 0; j <= upper; ++j) { + USAGE usage = j + cap.Range.UsageMin; + USHORT data_index = j + cap.Range.DataIndexMin; + ButtonHandle handle = ButtonHandle::none(); + switch (cap.UsagePage) { + case HID_USAGE_PAGE_BUTTON: + if (_device_class == DC_gamepad) { + static const ButtonHandle gamepad_buttons[] = { + ButtonHandle::none(), + GamepadButton::action_a(), + GamepadButton::action_b(), + GamepadButton::action_x(), + GamepadButton::action_y(), + GamepadButton::lshoulder(), + GamepadButton::rshoulder(), + GamepadButton::start(), + GamepadButton::back(), + GamepadButton::lstick(), + GamepadButton::rstick(), + }; + if (usage < sizeof(gamepad_buttons) / sizeof(ButtonHandle)) { + handle = gamepad_buttons[usage]; + } + } else if (_device_class == DC_flight_stick) { + if (usage > 0) { + handle = GamepadButton::joystick(usage - 1); + } + } else if (_device_class == DC_mouse) { + // In Panda, wheel and right button are flipped around... + int button = (usage == 2 || usage == 3) ? (4 - usage) : (usage - 1); + handle = MouseButton::button(button); + } + break; + } + + int button_index = _buttons.size(); + _report_buttons[cap.ReportID].set_bit(button_index); + _indices[data_index] = Index::button(button_index); + _buttons.push_back(ButtonState(handle)); + } + } + + USHORT num_value_caps = caps.NumberInputValueCaps; + PHIDP_VALUE_CAPS value_caps = (PHIDP_VALUE_CAPS)alloca(num_value_caps * sizeof(HIDP_VALUE_CAPS)); + _HidP_GetValueCaps(HidP_Input, value_caps, &num_value_caps, buffer); + + _hat_data_index = -1; + + for (USHORT i = 0; i < num_value_caps; ++i) { + HIDP_VALUE_CAPS &cap = value_caps[i]; + int upper = 0; + if (cap.IsRange) { + upper = (cap.Range.UsageMax - cap.Range.UsageMin); + + if (device_cat.is_debug()) { + device_cat.debug() + << "Found value range: DataIndex=" << dec + << cap.Range.DataIndexMin << ".." << cap.Range.DataIndexMax + << ", ReportID=" << (int)cap.ReportID + << ", UsagePage=0x" << hex << cap.UsagePage + << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax + << dec << ", LogicalMin=" << cap.LogicalMin + << ", LogicalMax=" << cap.LogicalMax << "\n"; + } + } else { + if (device_cat.is_debug()) { + device_cat.debug() + << "Found value: DataIndex=" << dec << cap.NotRange.DataIndex + << ", ReportID=" << dec << (int)cap.ReportID + << ", UsagePage=0x" << hex << cap.UsagePage + << ", Usage=0x" << cap.NotRange.Usage + << dec << ", LogicalMin=" << cap.LogicalMin + << ", LogicalMax=" << cap.LogicalMax << "\n"; + } + } + + for (int j = 0; j <= upper; ++j) { + USAGE usage = j + cap.Range.UsageMin; + USHORT data_index = j + cap.Range.DataIndexMin; + bool is_signed = true; + + // My gamepads give this odd invalid range. + if (cap.LogicalMin == 0 && cap.LogicalMax == -1) { + cap.LogicalMax = 65535; + is_signed = false; + } + + ControlAxis axis = C_none; + switch (cap.UsagePage) { + case HID_USAGE_PAGE_GENERIC: + switch (usage) { + case HID_USAGE_GENERIC_X: + if (_device_class == DC_gamepad) { + axis = C_left_x; + } else if (_device_class == DC_flight_stick) { + axis = C_roll; + } else { + axis = C_x; + } + break; + case HID_USAGE_GENERIC_Y: + if (_device_class == DC_gamepad) { + axis = C_left_y; + swap(cap.LogicalMin, cap.LogicalMax); + } else if (_device_class == DC_flight_stick) { + axis = C_pitch; + } else { + axis = C_y; + swap(cap.LogicalMin, cap.LogicalMax); + } + break; + case HID_USAGE_GENERIC_Z: + if (_device_class == DC_gamepad) { + axis = C_left_trigger; + } else if (_device_class == DC_flight_stick) { + axis = C_throttle; + } else { + axis = C_z; + swap(cap.LogicalMin, cap.LogicalMax); + } + break; + case HID_USAGE_GENERIC_RX: + if (_device_class == DC_gamepad) { + axis = C_right_x; + } else { + axis = C_pitch; + } + break; + case HID_USAGE_GENERIC_RY: + if (_device_class == DC_gamepad) { + axis = C_right_y; + } else { + axis = C_roll; + } + swap(cap.LogicalMin, cap.LogicalMax); + break; + case HID_USAGE_GENERIC_RZ: + if (_device_class == DC_gamepad) { + axis = C_right_trigger; + } else { + // Flip to match Panda's convention for heading. + axis = C_yaw; + swap(cap.LogicalMin, cap.LogicalMax); + } + break; + case HID_USAGE_GENERIC_SLIDER: + // Flip to match Panda's convention for heading. + axis = C_rudder; + swap(cap.LogicalMin, cap.LogicalMax); + break; + case HID_USAGE_GENERIC_WHEEL: + axis = C_wheel; + break; + case HID_USAGE_GENERIC_HATSWITCH: + // This is handled specially. + _hat_data_index = data_index; + _hat_data_minimum = cap.LogicalMin; + continue; + } + break; + } + + int control_index; + if (_vendor_id == 0x044f && _product_id == 0xb108 && axis == C_throttle) { + // T.Flight Hotas X throttle is reversed and can go backwards. + control_index = add_control(axis, cap.LogicalMax, cap.LogicalMin, true); + } else if (!is_signed) { + // All axes on the weird XInput-style mappings go from -1 to 1 + control_index = add_control(axis, cap.LogicalMin, cap.LogicalMax, true); + } else { + control_index = add_control(axis, cap.LogicalMin, cap.LogicalMax); + } + _indices[data_index] = Index::control(control_index, is_signed); + } + } + + // Do we need to emulate a hat switch or directional pad? + if (_hat_data_index != -1) { + _hat_left_button = (int)_buttons.size(); + if (_device_class == DC_gamepad) { + _buttons.push_back(ButtonState(GamepadButton::dpad_left())); + _buttons.push_back(ButtonState(GamepadButton::dpad_right())); + _buttons.push_back(ButtonState(GamepadButton::dpad_down())); + _buttons.push_back(ButtonState(GamepadButton::dpad_up())); + } else { + _buttons.push_back(ButtonState(GamepadButton::hat_left())); + _buttons.push_back(ButtonState(GamepadButton::hat_right())); + _buttons.push_back(ButtonState(GamepadButton::hat_down())); + _buttons.push_back(ButtonState(GamepadButton::hat_up())); + } + } + + _max_data_count = _HidP_MaxDataListLength(HidP_Input, buffer); + + _handle = handle; + _is_connected = true; + return true; +} + +/** + * Called by InputDeviceManager when this device is removed. + */ +void WinRawInputDevice:: +on_removal() { + LightMutexHolder holder(_lock); + _is_connected = false; + _handle = nullptr; + if (_preparsed != nullptr) { + delete _preparsed; + _preparsed = nullptr; + } + _indices.clear(); + _report_buttons.clear(); +} + +void WinRawInputDevice:: +on_input(PRAWINPUT input) { + nassertv(input != nullptr); + nassertv(_preparsed != nullptr); + + BYTE *ptr = input->data.hid.bRawData; + if (input->data.hid.dwSizeHid == 0) { + return; + } + + PHIDP_DATA data = (PHIDP_DATA)alloca(sizeof(HIDP_DATA) * _max_data_count); + nassertv(data != nullptr); + ULONG count; + + LightMutexHolder holder(_lock); + + for (DWORD i = 0; i < input->data.hid.dwCount; ++i) { + // The first byte is the report identifier. We need it to figure out + // which buttons are off, since each report only contains the buttons that + // are "on". + UCHAR report_id = ptr[0]; + BitArray unset_buttons = _report_buttons[report_id]; + + count = _max_data_count; + NTSTATUS status = _HidP_GetData(HidP_Input, data, &count, (PHIDP_PREPARSED_DATA)_preparsed, (PCHAR)ptr, input->data.hid.dwSizeHid); + if (status == HIDP_STATUS_SUCCESS) { + for (ULONG di = 0; di < count; ++di) { + if (data[di].DataIndex != _hat_data_index) { + const Index &idx = _indices[data[di].DataIndex]; + if (idx._control >= 0) { + if (idx._signed) { + control_changed(idx._control, (SHORT)data[di].RawValue); + } else { + control_changed(idx._control, data[di].RawValue); + } + } + if (idx._button >= 0) { + unset_buttons.clear_bit(idx._button); + button_changed(idx._button, (data[di].On != FALSE)); + } + } else { + int value = (int)data[di].RawValue - _hat_data_minimum; + button_changed(_hat_left_button + 0, value >= 5 && value <= 7); // left + button_changed(_hat_left_button + 1, value >= 1 && value <= 3); // right + button_changed(_hat_left_button + 2, value >= 3 && value <= 5); // down + button_changed(_hat_left_button + 3, value == 7 || value == 0 || value == 1); // up + } + } + + // Now unset the buttons in this report that aren't pressed. + int button_index = unset_buttons.get_lowest_on_bit(); + while (button_index >= 0) { + button_changed(button_index, false); + unset_buttons.clear_bit(button_index); + button_index = unset_buttons.get_lowest_on_bit(); + } + } else if (device_cat.is_spam()) { + device_cat.spam() + << "Failed to get data from raw device " << _path + << " (error 0x" << hex << (status & 0xffffffffu) << dec << ")\n"; + } + + ptr += input->data.hid.dwSizeHid; + } +} + +/** + * Polls the input device for new activity, to ensure it contains the latest + * events. This will only have any effect for some types of input devices; + * others may be updated automatically, and this method will be a no-op. + */ +void WinRawInputDevice:: +do_poll() { +} + +#endif // _WIN32 diff --git a/panda/src/device/winRawInputDevice.h b/panda/src/device/winRawInputDevice.h new file mode 100644 index 0000000000..58090ed07f --- /dev/null +++ b/panda/src/device/winRawInputDevice.h @@ -0,0 +1,85 @@ +/** + * 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 winRawInputDevice.h + * @author rdb + * @date 2018-01-19 + */ + +#ifndef WINRAWINPUTDEVICE_H +#define WINRAWINPUTDEVICE_H + +#include "pandabase.h" +#include "inputDevice.h" +#include "bitArray.h" + +#if defined(_WIN32) && !defined(CPPPARSER) + +class WinInputDeviceManager; + +/** + * This implementation of InputDevice uses the Win32 raw input API and the HID + * parser library to support a wide range of devices. + */ +class EXPCL_PANDA_DEVICE WinRawInputDevice FINAL : public InputDevice { +public: + WinRawInputDevice(WinInputDeviceManager *manager, const char *path); + ~WinRawInputDevice(); + + bool on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, string name); + void on_removal(); + void on_input(PRAWINPUT input); + +private: + virtual void do_poll(); + +private: + const string _path; + HANDLE _handle; + DWORD _size; + void *_preparsed; + ULONG _max_data_count; + ULONG _max_usage_count; + + // Indexed by report ID + pvector _report_buttons; + + // Either a button index or a control index. + struct Index { + Index() : _button(-1), _control(-1) {} + + static Index button(int index) { + Index idx; + idx._button = index; + return idx; + } + static Index control(int index, bool is_signed=true) { + Index idx; + idx._control = index; + idx._signed = is_signed; + return idx; + } + + int _button; + int _control; + bool _signed; + }; + + // Maps a "data index" to either button index or control index. + pvector _indices; + int _hat_data_index; + int _hat_data_minimum; + int _hat_left_button; + + WinInputDeviceManager *_manager; + friend class WinInputDeviceManager; +}; + +#endif // _WIN32 + +#endif diff --git a/panda/src/device/xInputDevice.cxx b/panda/src/device/xInputDevice.cxx index 57fa73a416..5c8e9e8b2f 100644 --- a/panda/src/device/xInputDevice.cxx +++ b/panda/src/device/xInputDevice.cxx @@ -13,7 +13,8 @@ #include "xInputDevice.h" -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CPPPARSER) + #include "gamepadButton.h" #include "inputDeviceManager.h" #include "string_utils.h" @@ -73,27 +74,44 @@ typedef struct _XINPUT_BATTERY_INFORMATION { } XINPUT_BATTERY_INFORMATION; // Undocumented, I figured out how this looks by trial and error. -struct XINPUT_BUSINFO { +typedef struct _XINPUT_BUSINFO { WORD VendorID; WORD ProductID; WORD RevisionID; WORD Unknown1; // Unknown - padding? DWORD InstanceID; DWORD Unknown2; - //WORD Unknown3; -}; + WORD Unknown3; +} XINPUT_BUSINFO; + +typedef struct _XINPUT_CAPABILITIES_EX { + BYTE Type; + BYTE SubType; + WORD Flags; + XINPUT_GAMEPAD Gamepad; + XINPUT_VIBRATION Vibration; + + // The following fields are undocumented. + WORD VendorID; + WORD ProductID; + WORD RevisionID; + WORD Unknown1; + WORD Unknown2; +} XINPUT_CAPABILITIES_EX; typedef DWORD (*pXInputGetState)(DWORD, XINPUT_STATE *); typedef DWORD (*pXInputSetState)(DWORD, XINPUT_VIBRATION *); typedef DWORD (*pXInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES *); +typedef DWORD (*pXInputGetCapabilitiesEx)(DWORD, DWORD, DWORD, XINPUT_CAPABILITIES_EX *); typedef DWORD (*pXInputGetBatteryInformation)(DWORD, BYTE, XINPUT_BATTERY_INFORMATION *); typedef DWORD (*pXInputGetBaseBusInformation)(DWORD, XINPUT_BUSINFO *); -static pXInputGetState get_state = NULL; -static pXInputSetState set_state = NULL; -static pXInputGetCapabilities get_capabilities = NULL; -static pXInputGetBatteryInformation get_battery_information = NULL; -static pXInputGetBaseBusInformation get_base_bus_information = NULL; +static pXInputGetState get_state = nullptr; +static pXInputSetState set_state = nullptr; +static pXInputGetCapabilities get_capabilities = nullptr; +static pXInputGetCapabilitiesEx get_capabilities_ex = nullptr; +static pXInputGetBatteryInformation get_battery_information = nullptr; +static pXInputGetBaseBusInformation get_base_bus_information = nullptr; bool XInputDevice::_initialized = false; @@ -108,26 +126,8 @@ XInputDevice(DWORD user_index) : nassertv(user_index >= 0 && user_index < XUSER_MAX_COUNT); - if (!_initialized) { - nassertv(init_xinput()); - } - - _name = "XInput Device #"; - _name += format_string(user_index); - _controls.resize(6); _buttons.resize(16); - - // Check if the device is connected. If so, initialize it. - XINPUT_CAPABILITIES caps; - XINPUT_STATE state; - if (get_capabilities(_index, 0, &caps) == ERROR_SUCCESS && - get_state(_index, &state) == ERROR_SUCCESS) { - _is_connected = true; - init_device(caps, state); - } else { - _is_connected = false; - } } /** @@ -138,17 +138,79 @@ XInputDevice:: do_set_vibration(0, 0); } +/** + * Called when a new input device arrives in the InputDeviceManager. This + * method checks whether it matches this XInput device. + */ +bool XInputDevice:: +check_arrival(const RID_DEVICE_INFO &info, DEVINST inst, + const string &name, const string &manufacturer) { + LightMutexHolder holder(_lock); + if (_is_connected) { + return false; + } + + if (!_initialized) { + nassertr_always(init_xinput(), false); + } + + XINPUT_CAPABILITIES_EX caps = {0}; + XINPUT_STATE state; + if ((get_capabilities_ex && get_capabilities_ex(1, _index, 0, &caps) != ERROR_SUCCESS) && + get_capabilities(_index, 0, (XINPUT_CAPABILITIES *)&caps) != ERROR_SUCCESS) { + return false; + } + + // Extra check for VID/PID if we have it, just to be sure. + if ((caps.VendorID != 0 && caps.VendorID != info.hid.dwVendorId) || + (caps.ProductID != 0 && caps.ProductID != info.hid.dwProductId)) { + return false; + } + + // Yes, take the name and manufacturer. + if (!name.empty()) { + _name = name; + } else { + _name = "XInput Device #"; + _name += format_string(_index + 1); + } + _manufacturer = manufacturer; + + if (inst && caps.ProductID == 0 && caps.RevisionID != 0) { + // XInput does not report a product ID for the Xbox 360 wireless adapter. + // Instead, we check that the RevisionID matches. + char buffer[4096]; + ULONG buflen = sizeof(buffer); + if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_HARDWAREID, 0, buffer, &buflen, 0) == CR_SUCCESS) { + string ids(buffer, buflen); + char revstr[16]; + sprintf(revstr, "REV_%04x", caps.RevisionID); + if (ids.find(revstr) == string::npos) { + return false; + } + } + } + + _is_connected = true; + init_device(caps, state); + _vendor_id = info.hid.dwVendorId; + _product_id = info.hid.dwProductId; + return true; +} + /** * Called periodically by the InputDeviceManager to detect whether the device * is currently connected. + * Returns true if the device wasn't connected, but now is. */ void XInputDevice:: detect(InputDeviceManager *mgr) { bool connected = false; - XINPUT_CAPABILITIES caps; + XINPUT_CAPABILITIES_EX caps = {0}; XINPUT_STATE state; - if (get_capabilities(_index, 0, &caps) == ERROR_SUCCESS && + if (((get_capabilities_ex && get_capabilities_ex(1, _index, 0, &caps) == ERROR_SUCCESS) || + get_capabilities(_index, 0, (XINPUT_CAPABILITIES *)&caps) == ERROR_SUCCESS) && get_state(_index, &state) == ERROR_SUCCESS) { connected = true; } else { @@ -203,9 +265,9 @@ init_xinput() { // Undocumented version (XInputGetStateEx) that includes a // state bit for the guide button. get_state = (pXInputGetState)GetProcAddress(module, MAKEINTRESOURCE(100)); - if (get_state == NULL) { + if (get_state == nullptr) { get_state = (pXInputGetState)GetProcAddress(module, "XInputGetState"); - if (get_state == NULL) { + if (get_state == nullptr) { device_cat.error() << "Failed to find function XInputGetState in " << dll_name << ".\n"; return false; @@ -213,14 +275,14 @@ init_xinput() { } set_state = (pXInputSetState)GetProcAddress(module, "XInputSetState"); - if (set_state == NULL) { + if (set_state == nullptr) { device_cat.error() << "Failed to find function XInputSetState in " << dll_name << ".\n"; return false; } get_capabilities = (pXInputGetCapabilities)GetProcAddress(module, "XInputGetCapabilities"); - if (get_capabilities == NULL) { + if (get_capabilities == nullptr) { device_cat.error() << "Failed to find function XInputGetCapabilities in " << dll_name << ".\n"; return false; @@ -228,6 +290,7 @@ init_xinput() { get_battery_information = (pXInputGetBatteryInformation)GetProcAddress(module, "XInputGetBatteryInformation"); get_base_bus_information = (pXInputGetBaseBusInformation)GetProcAddress(module, MAKEINTRESOURCE(104)); + get_capabilities_ex = (pXInputGetCapabilitiesEx)GetProcAddress(module, MAKEINTRESOURCE(108)); return true; } @@ -240,7 +303,8 @@ init_xinput() { * Initializes the device. Called when the device was just connected. */ void XInputDevice:: -init_device(const XINPUT_CAPABILITIES &caps, const XINPUT_STATE &state) { +init_device(const XINPUT_CAPABILITIES_EX &caps, const XINPUT_STATE &state) { + nassertv(_initialized); // It seems that the Xbox One controller is reported as having a DevType of // zero, at least when I tested in with XInput 1.3 on Windows 7. //if (caps.Type == XINPUT_DEVTYPE_GAMEPAD) { @@ -275,8 +339,8 @@ init_device(const XINPUT_CAPABILITIES &caps, const XINPUT_STATE &state) { set_control_map(1, C_throttle); set_control_map(2, C_roll); set_control_map(3, C_pitch); - set_control_map(4, C_hat_x); - set_control_map(5, C_hat_y); + set_control_map(4, C_none); + set_control_map(5, C_none); break; case XINPUT_DEVSUBTYPE_DANCE_PAD: @@ -327,11 +391,13 @@ init_device(const XINPUT_CAPABILITIES &caps, const XINPUT_STATE &state) { set_button_map(13, GamepadButton::action_x()); set_button_map(14, GamepadButton::action_y()); - if (caps.Flags & XINPUT_CAPS_FFB_SUPPORTED) { + + if (caps.Vibration.wLeftMotorSpeed != 0 || + caps.Vibration.wRightMotorSpeed != 0) { _flags |= IDF_has_vibration; } - if (get_battery_information != NULL) { + if (get_battery_information != nullptr) { XINPUT_BATTERY_INFORMATION batt; if (get_battery_information(_index, BATTERY_DEVTYPE_GAMEPAD, &batt) == ERROR_SUCCESS) { if (batt.BatteryType != BATTERY_TYPE_DISCONNECTED && @@ -344,54 +410,11 @@ init_device(const XINPUT_CAPABILITIES &caps, const XINPUT_STATE &state) { } } - // Get information about the USB device. - // This is not documented at all. I'm probably the first to try this. - XINPUT_BUSINFO businfo; - if (get_base_bus_information != NULL && - get_base_bus_information(0, &businfo) == ERROR_SUCCESS) { - _vendor_id = businfo.VendorID; - _product_id = businfo.ProductID; - - { - // Reformat the serial number into its original hex string form. - char sn[10]; - sprintf_s(sn, 10, "%08X", businfo.InstanceID); - _serial_number.assign(sn, 8); - } - - // Get information about the device from Windows. For that, we'll - // first need to construct the device path. Fortunately, we now have - // enough information to do so. - char path[32]; - sprintf_s(path, 32, "USB\\VID_%04X&PID_%04X\\%08X", businfo.VendorID, businfo.ProductID, businfo.InstanceID); - - DEVINST inst; - if (CM_Locate_DevNodeA(&inst, path, 0) != 0) { - if (device_cat.is_debug()) { - device_cat.debug() - << "Could not locate device node " << path << "\n"; - } - } else { - // Get the device properties we need. - 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) { - _manufacturer.assign(buffer); - } - } - } - WORD buttons = state.Gamepad.wButtons; WORD mask = 1; for (int i = 0; i < 16; ++i) { - if (buttons & mask) { - // Set the state without triggering a button event. - _buttons[i].state = (buttons & mask) ? S_down : S_up; - } + // Set the state without triggering a button event. + _buttons[i].state = (buttons & mask) ? S_down : S_up; mask <<= 1; if (i == 10) { // XInput skips 0x0800. @@ -417,6 +440,8 @@ init_device(const XINPUT_CAPABILITIES &caps, const XINPUT_STATE &state) { */ void XInputDevice:: do_set_vibration(double strong, double weak) { + nassertv_always(_is_connected); + XINPUT_VIBRATION vibration; vibration.wLeftMotorSpeed = strong * 0xffff; vibration.wRightMotorSpeed = weak * 0xffff; @@ -430,6 +455,11 @@ do_set_vibration(double strong, double weak) { */ void XInputDevice:: do_poll() { + // Not sure why someone would call this on a disconnected device. + if (!_is_connected) { + return; + } + XINPUT_STATE state; if (get_state(_index, &state) != ERROR_SUCCESS) { @@ -440,18 +470,6 @@ do_poll() { mgr->remove_device(this); } return; - - } else if (!_is_connected) { - // Device was (re)connected. It's a bit strange to call poll() on a - // disconnected device, but there's nothing stopping the user from - // doing so. - XINPUT_CAPABILITIES caps; - if (get_capabilities(_index, 0, &caps) == ERROR_SUCCESS) { - _is_connected = true; - init_device(caps, state); - InputDeviceManager *mgr = InputDeviceManager::get_global_ptr(); - mgr->add_device(this); - } } if (state.dwPacketNumber == _last_packet) { diff --git a/panda/src/device/xInputDevice.h b/panda/src/device/xInputDevice.h index 73d8c8747d..598cd5ec74 100644 --- a/panda/src/device/xInputDevice.h +++ b/panda/src/device/xInputDevice.h @@ -17,13 +17,17 @@ #include "pandabase.h" #include "inputDevice.h" -#ifdef _WIN32 +#if defined(_WIN32) && !defined(CPPPARSER) + +#include class InputDeviceManager; -typedef struct _XINPUT_CAPABILITIES XINPUT_CAPABILITIES; +typedef struct _XINPUT_CAPABILITIES_EX XINPUT_CAPABILITIES_EX; typedef struct _XINPUT_STATE XINPUT_STATE; +typedef struct tagRID_DEVICE_INFO RID_DEVICE_INFO; + /** * This implementation of InputDevice uses Microsoft's XInput library to * interface with an Xbox 360 game controller. @@ -33,11 +37,13 @@ public: XInputDevice(DWORD user_index); ~XInputDevice(); + bool check_arrival(const RID_DEVICE_INFO &info, DEVINST inst, + const string &name, const string &manufacturer); void detect(InputDeviceManager *mgr); static bool init_xinput(); private: - void init_device(const XINPUT_CAPABILITIES &caps, const XINPUT_STATE &state); + void init_device(const XINPUT_CAPABILITIES_EX &caps, const XINPUT_STATE &state); virtual void do_set_vibration(double strong, double weak); virtual void do_poll();