Panda3D
Loading...
Searching...
No Matches
xInputDevice.cxx
Go to the documentation of this file.
1/**
2 * PANDA 3D SOFTWARE
3 * Copyright (c) Carnegie Mellon University. All rights reserved.
4 *
5 * All use of this software is subject to the terms of the revised BSD
6 * license. You should have received a copy of this license along
7 * with this source code in a file named "LICENSE."
8 *
9 * @file xInputDevice.cxx
10 * @author rdb
11 * @date 2015-07-15
12 */
13
14#include "xInputDevice.h"
15
16#if defined(_WIN32) && !defined(CPPPARSER)
17
18#include "gamepadButton.h"
19#include "inputDeviceManager.h"
20#include "string_utils.h"
21
22#include <xinput.h>
23#include <cfgmgr32.h>
24
25#ifndef XUSER_MAX_COUNT
26#define XUSER_MAX_COUNT 4
27#endif
28
29#ifndef XINPUT_CAPS_FFB_SUPPORTED
30#define XINPUT_CAPS_FFB_SUPPORTED 0x0001
31#endif
32#ifndef XINPUT_CAPS_NO_NAVIGATION
33#define XINPUT_CAPS_NO_NAVIGATION 0x0010
34#endif
35
36#ifndef BATTERY_DEVTYPE_GAMEPAD
37#define BATTERY_DEVTYPE_GAMEPAD 0x00
38#endif
39
40#ifndef XINPUT_DEVSUBTYPE_WHEEL
41#define XINPUT_DEVSUBTYPE_WHEEL 0x02
42#endif
43#ifndef XINPUT_DEVSUBTYPE_ARCADE_STICK
44#define XINPUT_DEVSUBTYPE_ARCADE_STICK 0x03
45#endif
46#ifndef XINPUT_DEVSUBTYPE_FLIGHT_STICK
47#define XINPUT_DEVSUBTYPE_FLIGHT_STICK 0x04
48#endif
49#ifndef XINPUT_DEVSUBTYPE_DANCE_PAD
50#define XINPUT_DEVSUBTYPE_DANCE_PAD 0x05
51#endif
52#ifndef XINPUT_DEVSUBTYPE_GUITAR
53#define XINPUT_DEVSUBTYPE_GUITAR 0x06
54#endif
55#ifndef XINPUT_DEVSUBTYPE_DRUM_KIT
56#define XINPUT_DEVSUBTYPE_DRUM_KIT 0x08
57#endif
58
59#ifndef BATTERY_TYPE_DISCONNECTED
60#define BATTERY_TYPE_DISCONNECTED 0x00
61#endif
62
63#ifndef BATTERY_TYPE_WIRED
64#define BATTERY_TYPE_WIRED 0x01
65#endif
66
67#ifndef BATTERY_LEVEL_FULL
68#define BATTERY_LEVEL_FULL 0x03
69#endif
70
71typedef struct _XINPUT_BATTERY_INFORMATION {
72 BYTE BatteryType;
73 BYTE BatteryLevel;
74} XINPUT_BATTERY_INFORMATION;
75
76// Undocumented, I figured out how this looks by trial and error.
77typedef struct _XINPUT_BUSINFO {
78 WORD VendorID;
79 WORD ProductID;
80 WORD RevisionID;
81 WORD Unknown1; // Unknown - padding?
82 DWORD InstanceID;
83 DWORD Unknown2;
84 WORD Unknown3;
85} XINPUT_BUSINFO;
86
87typedef struct _XINPUT_CAPABILITIES_EX {
88 BYTE Type;
89 BYTE SubType;
90 WORD Flags;
91 XINPUT_GAMEPAD Gamepad;
92 XINPUT_VIBRATION Vibration;
93
94 // The following fields are undocumented.
95 WORD VendorID;
96 WORD ProductID;
97 WORD RevisionID;
98 WORD Unknown1;
99 WORD Unknown2;
100} XINPUT_CAPABILITIES_EX;
101
102typedef DWORD (WINAPI *pXInputGetState)(DWORD, XINPUT_STATE *);
103typedef DWORD (WINAPI *pXInputSetState)(DWORD, XINPUT_VIBRATION *);
104typedef DWORD (WINAPI *pXInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES *);
105typedef DWORD (WINAPI *pXInputGetCapabilitiesEx)(DWORD, DWORD, DWORD, XINPUT_CAPABILITIES_EX *);
106typedef DWORD (WINAPI *pXInputGetBatteryInformation)(DWORD, BYTE, XINPUT_BATTERY_INFORMATION *);
107typedef DWORD (WINAPI *pXInputGetBaseBusInformation)(DWORD, XINPUT_BUSINFO *);
108
109static pXInputGetState get_state = nullptr;
110static pXInputSetState set_state = nullptr;
111static pXInputGetCapabilities get_capabilities = nullptr;
112static pXInputGetCapabilitiesEx get_capabilities_ex = nullptr;
113static pXInputGetBatteryInformation get_battery_information = nullptr;
114static pXInputGetBaseBusInformation get_base_bus_information = nullptr;
115
116bool XInputDevice::_initialized = false;
117
118/**
119 * Protected constructor. user_index is a number 0-3.
120 */
121XInputDevice::
122XInputDevice(DWORD user_index) :
123 _index(user_index),
124 _last_packet(-1),
125 _last_buttons(0) {
126
127 nassertv(user_index >= 0 && user_index < XUSER_MAX_COUNT);
128
129 _axes.resize(6);
130 _buttons.resize(16);
131}
132
133/**
134 *
135 */
136XInputDevice::
137~XInputDevice() {
138 do_set_vibration(0, 0);
139}
140
141/**
142 * Called when a new input device arrives in the InputDeviceManager. This
143 * method checks whether it matches this XInput device.
144 */
145bool XInputDevice::
146check_arrival(const RID_DEVICE_INFO &info, DEVINST inst,
147 const std::string &name, const std::string &manufacturer) {
148 LightMutexHolder holder(_lock);
149 if (_is_connected) {
150 return false;
151 }
152
153 if (!_initialized) {
154 nassertr_always(init_xinput(), false);
155 }
156
157 XINPUT_CAPABILITIES_EX caps = {0};
158 XINPUT_STATE state;
159 if ((get_capabilities_ex && get_capabilities_ex(1, _index, 0, &caps) != ERROR_SUCCESS) &&
160 get_capabilities(_index, 0, (XINPUT_CAPABILITIES *)&caps) != ERROR_SUCCESS) {
161 return false;
162 }
163
164 if (get_state(_index, &state) != ERROR_SUCCESS) {
165 return false;
166 }
167
168 // Extra check for VID/PID if we have it, just to be sure.
169 if ((caps.VendorID != 0 && caps.VendorID != info.hid.dwVendorId) ||
170 (caps.ProductID != 0 && caps.ProductID != info.hid.dwProductId)) {
171 return false;
172 }
173
174 // Yes, take the name and manufacturer.
175 if (!name.empty()) {
176 _name = name;
177 } else {
178 _name = "XInput Device #";
179 _name += format_string(_index + 1);
180 }
181 _manufacturer = manufacturer;
182
183 if (inst && caps.ProductID == 0 && caps.RevisionID != 0) {
184 // XInput does not report a product ID for the Xbox 360 wireless adapter.
185 // Instead, we check that the RevisionID matches.
186 char buffer[4096];
187 ULONG buflen = sizeof(buffer);
188 if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_HARDWAREID, 0, buffer, &buflen, 0) == CR_SUCCESS) {
189 std::string ids(buffer, buflen);
190 char revstr[16];
191 sprintf(revstr, "REV_%04x", caps.RevisionID);
192 if (ids.find(revstr) == std::string::npos) {
193 return false;
194 }
195 }
196 }
197
198 _is_connected = true;
199 init_device(caps, state);
200 _vendor_id = info.hid.dwVendorId;
201 _product_id = info.hid.dwProductId;
202 return true;
203}
204
205/**
206 * Called periodically by the InputDeviceManager to detect whether the device
207 * is currently connected.
208 * Returns true if the device wasn't connected, but now is.
209 */
210void XInputDevice::
211detect(InputDeviceManager *mgr) {
212 if (!_initialized) {
213 nassertv_always(init_xinput());
214 }
215
216 bool connected = false;
217
218 XINPUT_CAPABILITIES_EX caps = {0};
219 XINPUT_STATE state;
220 if (((get_capabilities_ex && get_capabilities_ex(1, _index, 0, &caps) == ERROR_SUCCESS) ||
221 get_capabilities(_index, 0, (XINPUT_CAPABILITIES *)&caps) == ERROR_SUCCESS) &&
222 get_state(_index, &state) == ERROR_SUCCESS) {
223 connected = true;
224 } else {
225 connected = false;
226 }
227
228 LightMutexHolder holder(_lock);
229 if (connected == _is_connected) {
230 // Nothing changed.
231 return;
232 }
233 _is_connected = connected;
234
235 if (connected) {
236 _name = "XInput Device #";
237 _name += format_string(_index + 1);
238 _vendor_id = caps.VendorID;
239 _product_id = caps.ProductID;
240 init_device(caps, state);
241 mgr->add_device(this);
242 } else {
243 mgr->remove_device(this);
244 }
245}
246
247/**
248 * Static method to initialize the XInput library.
249 */
250bool XInputDevice::
251init_xinput() {
252 if (_initialized) {
253 return true;
254 }
255
256 if (device_cat.is_debug()) {
257 device_cat.debug() << "Initializing XInput library.\n";
258 }
259
260 _initialized = true;
261 const char *dll_name = "Xinput1_4.dll";
262 HMODULE module = LoadLibraryA(dll_name);
263
264 // If we didn't find XInput 1.4, fall back to the older 1.3 version.
265 if (!module) {
266 if (device_cat.is_debug()) {
267 device_cat.debug()
268 << "Xinput1_4.dll not found, falling back to Xinput1_3.dll\n";
269 }
270
271 dll_name = "Xinput1_3.dll";
272 module = LoadLibraryA(dll_name);
273 }
274
275 if (module) {
276 if (device_cat.is_debug()) {
277 device_cat.debug()
278 << "Successfully loaded " << dll_name << "\n";
279 }
280
281 // Undocumented version (XInputGetStateEx) that includes a
282 // state bit for the guide button.
283 get_state = (pXInputGetState)GetProcAddress(module, MAKEINTRESOURCE(100));
284 if (get_state == nullptr) {
285 get_state = (pXInputGetState)GetProcAddress(module, "XInputGetState");
286 if (get_state == nullptr) {
287 device_cat.error()
288 << "Failed to find function XInputGetState in " << dll_name << ".\n";
289 return false;
290 }
291 }
292
293 set_state = (pXInputSetState)GetProcAddress(module, "XInputSetState");
294 if (set_state == nullptr) {
295 device_cat.error()
296 << "Failed to find function XInputSetState in " << dll_name << ".\n";
297 return false;
298 }
299
300 get_capabilities = (pXInputGetCapabilities)GetProcAddress(module, "XInputGetCapabilities");
301 if (get_capabilities == nullptr) {
302 device_cat.error()
303 << "Failed to find function XInputGetCapabilities in " << dll_name << ".\n";
304 return false;
305 }
306
307 get_battery_information = (pXInputGetBatteryInformation)GetProcAddress(module, "XInputGetBatteryInformation");
308 get_base_bus_information = (pXInputGetBaseBusInformation)GetProcAddress(module, MAKEINTRESOURCE(104));
309 get_capabilities_ex = (pXInputGetCapabilitiesEx)GetProcAddress(module, MAKEINTRESOURCE(108));
310 return true;
311 }
312
313 device_cat.error()
314 << "Failed to load Xinput1_4.dll or Xinput1_3.dll.\n";
315 return false;
316}
317
318/**
319 * Initializes the device. Called when the device was just connected.
320 * Assumes either the lock is held or this is called from the constructor.
321 */
322void XInputDevice::
323init_device(const XINPUT_CAPABILITIES_EX &caps, const XINPUT_STATE &state) {
324 nassertv(_initialized);
325 // It seems that the Xbox One controller is reported as having a DevType of
326 // zero, at least when I tested in with XInput 1.3 on Windows 7.
327 //if (caps.Type == XINPUT_DEVTYPE_GAMEPAD) {
328
329 // For subtypes and mappings, see this page:
330 // https://msdn.microsoft.com/en-us/library/windows/desktop/hh405050.aspx
331 switch (caps.SubType) {
332 default:
333 case XINPUT_DEVSUBTYPE_GAMEPAD:
334 _device_class = DeviceClass::gamepad;
335 _axes[0].axis = Axis::left_trigger;
336 _axes[1].axis = Axis::right_trigger;
337 _axes[2].axis = Axis::left_x;
338 _axes[3].axis = Axis::left_y;
339 _axes[4].axis = Axis::right_x;
340 _axes[5].axis = Axis::right_y;
341 break;
342
343 case XINPUT_DEVSUBTYPE_WHEEL:
344 _device_class = DeviceClass::steering_wheel;
345 _axes[0].axis = Axis::brake;
346 _axes[1].axis = Axis::accelerator;
347 _axes[2].axis = Axis::wheel;
348 _axes[3].axis = Axis::none;
349 _axes[4].axis = Axis::none;
350 _axes[5].axis = Axis::none;
351 break;
352
353 case XINPUT_DEVSUBTYPE_FLIGHT_STICK:
354 _device_class = DeviceClass::flight_stick;
355 _axes[0].axis = Axis::yaw;
356 _axes[1].axis = Axis::throttle;
357 _axes[2].axis = Axis::roll;
358 _axes[3].axis = Axis::pitch;
359 _axes[4].axis = Axis::none;
360 _axes[5].axis = Axis::none;
361 break;
362
363 case XINPUT_DEVSUBTYPE_DANCE_PAD:
364 _device_class = DeviceClass::dance_pad;
365 _axes[0].axis = Axis::none;
366 _axes[1].axis = Axis::none;
367 _axes[2].axis = Axis::none;
368 _axes[3].axis = Axis::none;
369 _axes[4].axis = Axis::none;
370 _axes[5].axis = Axis::none;
371 break;
372 }
373
374 _axes[0]._scale = 1.0 / 255.0;
375 _axes[1]._scale = 1.0 / 255.0;
376 _axes[2]._scale = 1.0 / 32767.5;
377 _axes[3]._scale = 1.0 / 32767.5;
378 _axes[4]._scale = 1.0 / 32767.5;
379 _axes[5]._scale = 1.0 / 32767.5;
380
381 _axes[2]._bias = 0.5 / 32767.5;
382 _axes[3]._bias = 0.5 / 32767.5;
383 _axes[4]._bias = 0.5 / 32767.5;
384 _axes[5]._bias = 0.5 / 32767.5;
385
386 if (caps.Flags & XINPUT_CAPS_NO_NAVIGATION) {
387 _buttons[0].handle = ButtonHandle::none();
388 _buttons[1].handle = ButtonHandle::none();
389 _buttons[2].handle = ButtonHandle::none();
390 _buttons[3].handle = ButtonHandle::none();
391 _buttons[4].handle = ButtonHandle::none();
392 _buttons[5].handle = ButtonHandle::none();
393 } else {
394 _buttons[0].handle = GamepadButton::dpad_up();
395 _buttons[1].handle = GamepadButton::dpad_down();
396 _buttons[2].handle = GamepadButton::dpad_left();
397 _buttons[3].handle = GamepadButton::dpad_right();
398 _buttons[4].handle = GamepadButton::start();
399 _buttons[5].handle = GamepadButton::back();
400 }
401 _buttons[6].handle = GamepadButton::lstick();
402 _buttons[7].handle = GamepadButton::rstick();
403 _buttons[8].handle = GamepadButton::lshoulder();
404 _buttons[9].handle = GamepadButton::rshoulder();
405 _buttons[10].handle = GamepadButton::guide();
406 _buttons[11].handle = GamepadButton::face_a();
407 _buttons[12].handle = GamepadButton::face_b();
408 _buttons[13].handle = GamepadButton::face_x();
409 _buttons[14].handle = GamepadButton::face_y();
410
411 if (caps.Vibration.wLeftMotorSpeed != 0 ||
412 caps.Vibration.wRightMotorSpeed != 0) {
413 enable_feature(Feature::vibration);
414 }
415
416 if (get_battery_information != nullptr) {
417 XINPUT_BATTERY_INFORMATION batt;
418 if (get_battery_information(_index, BATTERY_DEVTYPE_GAMEPAD, &batt) == ERROR_SUCCESS) {
419 if (batt.BatteryType != BATTERY_TYPE_DISCONNECTED &&
420 batt.BatteryType != BATTERY_TYPE_WIRED) {
421 // This device has a battery. Report the battery level.
422 enable_feature(Feature::battery);
423 _battery_data.level = batt.BatteryLevel;
424 _battery_data.max_level = BATTERY_LEVEL_FULL;
425 }
426 }
427 }
428
429 WORD buttons = state.Gamepad.wButtons;
430 WORD mask = 1;
431 for (int i = 0; i < 16; ++i) {
432 // Set the state without triggering a button event.
433 _buttons[i]._state = (buttons & mask) ? S_down : S_up;
434 mask <<= 1;
435 if (i == 10) {
436 // XInput skips 0x0800.
437 mask <<= 1;
438 }
439 }
440
441 axis_changed(0, state.Gamepad.bLeftTrigger);
442 axis_changed(1, state.Gamepad.bRightTrigger);
443 axis_changed(2, state.Gamepad.sThumbLX);
444 axis_changed(3, state.Gamepad.sThumbLY);
445 axis_changed(4, state.Gamepad.sThumbRX);
446 axis_changed(5, state.Gamepad.sThumbRY);
447
448 _last_buttons = buttons;
449 _last_packet = state.dwPacketNumber;
450}
451
452/**
453 * Sets the vibration strength. The first argument controls a low-frequency
454 * motor, if present, and the latter controls a high-frequency motor.
455 * The values are within the 0-1 range.
456 */
457void XInputDevice::
458do_set_vibration(double strong, double weak) {
459 nassertv_always(_is_connected);
460
461 XINPUT_VIBRATION vibration;
462 vibration.wLeftMotorSpeed = strong * 0xffff;
463 vibration.wRightMotorSpeed = weak * 0xffff;
464 set_state(_index, &vibration);
465}
466
467/**
468 * Polls the input device for new activity, to ensure it contains the latest
469 * events. This will only have any effect for some types of input devices;
470 * others may be updated automatically, and this method will be a no-op.
471 */
472void XInputDevice::
473do_poll() {
474 // Not sure why someone would call this on a disconnected device.
475 if (!_is_connected) {
476 return;
477 }
478
479 XINPUT_STATE state;
480
481 if (get_state(_index, &state) != ERROR_SUCCESS) {
482 // Device was disconnected.
483 if (_is_connected) {
484 _is_connected = false;
486 mgr->remove_device(this);
487 }
488 return;
489 }
490
491 if (state.dwPacketNumber == _last_packet) {
492 // No change since last time we asked.
493 return;
494 }
495
496 // Did any buttons change state?
497 WORD changed_buttons = _last_buttons ^ state.Gamepad.wButtons;
498
499 WORD mask = 1;
500 for (int i = 0; i < 16; ++i) {
501 if (changed_buttons & mask) {
502 button_changed(i, (state.Gamepad.wButtons & mask) != 0);
503 }
504 mask <<= 1;
505 if (i == 10) {
506 // XInput skips 0x0800.
507 mask <<= 1;
508 }
509 }
510
511 axis_changed(0, state.Gamepad.bLeftTrigger);
512 axis_changed(1, state.Gamepad.bRightTrigger);
513 axis_changed(2, state.Gamepad.sThumbLX);
514 axis_changed(3, state.Gamepad.sThumbLY);
515 axis_changed(4, state.Gamepad.sThumbRX);
516 axis_changed(5, state.Gamepad.sThumbRY);
517
518 _last_buttons = state.Gamepad.wButtons;
519 _last_packet = state.dwPacketNumber;
520}
521
522#endif // _WIN32
This class keeps track of all the devices on a system, and sends out events when a device has been ho...
void add_device(InputDevice *device)
Called when a new device has been discovered.
static InputDeviceManager * get_global_ptr()
Returns the singleton InputDeviceManager instance.
void remove_device(InputDevice *device)
Called when a device has been removed, or when a device should otherwise no longer be tracked.
Similar to MutexHolder, but for a light mutex.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.