Skip to content

Commit a7440e5

Browse files
committed
Update to Windows GameInput API
Breaking change: event keys does change as well as the value range of axis input. Fixes: * Crash when gamepad is disconnected * Possible stale thread on disconnect * Sleep(4) in input loop to reduce CPU load
1 parent 34400bf commit a7440e5

File tree

5 files changed

+240
-153
lines changed

5 files changed

+240
-153
lines changed

packages/gamepads_windows/windows/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,8 @@ set(gamepads_windows_bundled_libraries
5555
""
5656
PARENT_SCOPE
5757
)
58+
59+
# Adds Microsoft.GameInput library from NuGet
60+
set_property(TARGET ${PLUGIN_NAME}
61+
PROPERTY VS_PACKAGE_REFERENCES "Microsoft.GameInput_3.0.26100.6154"
62+
)
Lines changed: 200 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,255 @@
1-
#include <iostream>
2-
#define WIN32_LEAN_AND_MEAN
3-
#include <initguid.h>
4-
#include <windows.h>
5-
#include <dbt.h>
6-
#include <hidclass.h>
7-
#pragma comment(lib, "winmm.lib")
8-
#include <mmsystem.h>
9-
10-
#include <list>
11-
#include <map>
12-
#include <set>
13-
#include <thread>
1+
#include <algorithm>
2+
#include <ppl.h>
3+
#include <vector>
4+
#include <concrt.h>
5+
#include <winerror.h>
6+
#include <winrt/Windows.Gaming.Input.h>
147

158
#include "gamepad.h"
169
#include "utils.h"
10+
#include <optional>
11+
#include <chrono>
12+
#include <GameInput.h>
13+
#include <iomanip>
14+
#include <sstream>
15+
#pragma comment(lib, "GameInput.lib")
1716

1817
Gamepads gamepads;
1918

20-
std::list<Event> Gamepads::diff_states(Gamepad* gamepad,
21-
const JOYINFOEX& old,
22-
const JOYINFOEX& current) {
19+
using namespace concurrency;
20+
using namespace winrt;
21+
using namespace Windows::Gaming;
22+
using namespace std::chrono_literals;
23+
24+
static concurrency::critical_section m_lock{};
25+
26+
static IGameInput* g_gameInput = nullptr;
27+
static IGameInputDevice* g_gamepad = nullptr;
28+
29+
std::string AppLocalDeviceIdToString(const APP_LOCAL_DEVICE_ID& id) {
30+
std::ostringstream oss;
31+
oss << std::hex << std::setfill('0');
32+
for (size_t i = 0; i < APP_LOCAL_DEVICE_ID_SIZE; ++i) {
33+
oss << std::setw(2) << static_cast<int>(id.value[i]);
34+
}
35+
return oss.str();
36+
}
37+
38+
std::list<Event> diff_states(const GameInputDeviceInfo& device_info,
39+
const GameInputGamepadState& old,
40+
const GameInputGamepadState& current) {
2341
std::time_t now = std::time(nullptr);
2442
int time = static_cast<int>(now);
2543

2644
std::list<Event> events;
27-
if (old.dwXpos != current.dwXpos) {
45+
if (old.leftThumbstickX != current.leftThumbstickX) {
2846
events.push_back(
29-
{time, "analog", "dwXpos", static_cast<int>(current.dwXpos)});
47+
{time, "analog", "dwXpos", current.leftThumbstickX});
3048
}
31-
if (old.dwYpos != current.dwYpos) {
49+
if (old.leftThumbstickY != current.leftThumbstickY) {
3250
events.push_back(
33-
{time, "analog", "dwYpos", static_cast<int>(current.dwYpos)});
51+
{time, "analog", "dwYpos", current.leftThumbstickY});
3452
}
35-
if (old.dwZpos != current.dwZpos) {
53+
if (old.rightThumbstickX != current.rightThumbstickX) {
3654
events.push_back(
37-
{time, "analog", "dwZpos", static_cast<int>(current.dwZpos)});
55+
{time, "analog", "dwZpos", current.rightThumbstickX});
3856
}
39-
if (old.dwRpos != current.dwRpos) {
57+
if (old.rightThumbstickY != current.rightThumbstickY) {
4058
events.push_back(
41-
{time, "analog", "dwRpos", static_cast<int>(current.dwRpos)});
59+
{time, "analog", "dwRpos", current.rightThumbstickY});
4260
}
43-
if (old.dwUpos != current.dwUpos) {
61+
if (old.leftTrigger != current.leftTrigger) {
4462
events.push_back(
45-
{time, "analog", "dwUpos", static_cast<int>(current.dwUpos)});
63+
{time, "analog", "dwUpos", current.leftTrigger});
4664
}
47-
if (old.dwVpos != current.dwVpos) {
65+
if (old.rightTrigger != current.rightTrigger) {
4866
events.push_back(
49-
{time, "analog", "dwVpos", static_cast<int>(current.dwVpos)});
50-
}
51-
if (old.dwPOV != current.dwPOV) {
52-
events.push_back({time, "analog", "pov", static_cast<int>(current.dwPOV)});
67+
{time, "analog", "dwVpos", current.rightTrigger});
5368
}
54-
if (old.dwButtons != current.dwButtons) {
55-
for (int i = 0; i < gamepad->num_buttons; ++i) {
56-
bool was_pressed = old.dwButtons & (1 << i);
57-
bool is_pressed = current.dwButtons & (1 << i);
69+
if (old.buttons != current.buttons) {
70+
for (uint32_t i = 0; i < device_info.controllerButtonCount; ++i) {
71+
bool was_pressed = old.buttons & (1 << i);
72+
bool is_pressed = current.buttons & (1 << i);
5873
if (was_pressed != is_pressed) {
74+
double value = is_pressed ? 1.0 : 0.0;
5975
events.push_back(
60-
{time, "button", "button-" + std::to_string(i), is_pressed});
76+
{time, "button", "button-" + std::to_string(i), value});
6177
}
6278
}
6379
}
6480
return events;
6581
}
6682

67-
bool Gamepads::are_states_different(const JOYINFOEX& a, const JOYINFOEX& b) {
68-
return a.dwXpos != b.dwXpos || a.dwYpos != b.dwYpos || a.dwZpos != b.dwZpos ||
69-
a.dwRpos != b.dwRpos || a.dwUpos != b.dwUpos || a.dwVpos != b.dwVpos ||
70-
a.dwButtons != b.dwButtons || a.dwPOV != b.dwPOV;
83+
bool are_states_different(const GameInputGamepadState& a, const GameInputGamepadState& b) {
84+
return a.leftThumbstickX != b.leftThumbstickX ||
85+
a.leftThumbstickY != b.leftThumbstickY ||
86+
a.leftTrigger != b.leftTrigger ||
87+
a.rightThumbstickX != b.rightThumbstickX ||
88+
a.rightThumbstickY != b.rightThumbstickY ||
89+
a.rightTrigger != b.rightTrigger ||
90+
a.buttons != b.buttons;
7191
}
7292

73-
void Gamepads::read_gamepad(Gamepad* gamepad) {
74-
JOYINFOEX state;
75-
state.dwSize = sizeof(JOYINFOEX);
76-
state.dwFlags = JOY_RETURNALL;
77-
78-
int joy_id = gamepad->joy_id;
93+
void OnDeviceEvent(
94+
GameInputCallbackToken callbackToken,
95+
void* context,
96+
IGameInputReading* reading,
97+
bool hasOverrunOccurred
98+
) {
99+
//auto* self = static_cast<Gamepads*>(context);
100+
std::cout << "Gamepad event" << std::endl;
101+
}
79102

80-
std::cout << "Listening to gamepad " << joy_id << std::endl;
103+
void Gamepads::init()
104+
{
105+
GameInputCreate(&g_gameInput);
81106

82-
while (gamepad->alive) {
83-
JOYINFOEX previous_state = state;
84-
MMRESULT result = joyGetPosEx(joy_id, &state);
85-
if (result == JOYERR_NOERROR) {
86-
if (are_states_different(previous_state, state)) {
87-
std::list<Event> events = diff_states(gamepad, previous_state, state);
88-
for (auto joy_event : events) {
89-
if (event_emitter.has_value()) {
90-
(*event_emitter)(gamepad, joy_event);
107+
if (g_gameInput != nullptr) {
108+
// Register listener for gamepad events
109+
if (g_gameInput != nullptr) {
110+
g_gameInput->RegisterDeviceCallback(
111+
nullptr, // All devices
112+
GameInputKindGamepad,
113+
GameInputDeviceConnected,
114+
GameInputAsyncEnumeration,
115+
static_cast<void*>(this),
116+
[](
117+
_In_ GameInputCallbackToken callbackToken,
118+
_In_ void * context,
119+
_In_ IGameInputDevice * device,
120+
_In_ uint64_t timestamp,
121+
_In_ GameInputDeviceStatus currentStatus,
122+
_In_ GameInputDeviceStatus previousStatus
123+
) {
124+
auto* self = static_cast<Gamepads*>(context);
125+
if (currentStatus & GameInputDeviceConnected) {
126+
self->on_gamepad_connected(device);
127+
} else {
128+
self->on_gamepad_disconnected(device);
91129
}
92-
}
130+
},
131+
this->deviceCallbackToken
132+
);
133+
}
134+
135+
/*
136+
// Currently doesn't produce any data, but perhaps in future, it can be used instead of read_thread.
137+
g_gameInput->RegisterReadingCallback(
138+
nullptr, // Any device,
139+
GameInputKindGamepad,
140+
0.0,
141+
static_cast<void*>(this),
142+
OnDeviceEvent,
143+
this->readingCallbackToken
144+
);
145+
*/
146+
}
147+
}
148+
149+
void Gamepads::stop()
150+
{
151+
if (g_gamepad) g_gamepad->Release();
152+
if (g_gameInput) {
153+
g_gameInput->UnregisterCallback(*this->deviceCallbackToken, 5000);
154+
//g_gameInput->UnregisterCallback(*this->readingCallbackToken, 5000);
155+
g_gameInput->Release();
156+
}
157+
158+
// Stop/cleanup threads
159+
for (auto gp : this->gamepads) {
160+
if (!gp->stop_thead) {
161+
if (gp->alive) {
162+
gp->stop_thead = true;
163+
} else {
164+
// Cleanup data of threads that exited due to error state.
165+
delete gp;
93166
}
94-
} else {
95-
std::cout << "Fail to listen to gamepad " << joy_id << std::endl;
96-
gamepad->alive = false;
97-
gamepads.erase(joy_id);
98167
}
99168
}
169+
this->gamepads.clear();
170+
}
171+
172+
std::list<GamepadData*> Gamepads::get_gamepads() {
173+
return this->gamepads;
100174
}
101175

102-
void Gamepads::connect_gamepad(UINT joy_id, std::string name, int num_buttons) {
103-
gamepads[joy_id] = {joy_id, name, num_buttons, true};
176+
void Gamepads::on_gamepad_connected(IGameInputDevice * device)
177+
{
178+
auto info = device->GetDeviceInfo();
179+
if (info == nullptr) {
180+
std::cerr << "Gamepad connected but failed to read info" << std::endl;
181+
return;
182+
}
183+
auto gp = new GamepadData();
184+
gp->id = AppLocalDeviceIdToString(info->deviceId);
185+
gp->name = info->displayName != nullptr && info->displayName->data != nullptr ? info->displayName->data : "";
186+
gp->num_buttons = info->controllerButtonCount;
187+
gp->stop_thead = false;
188+
gp->alive = true;
189+
this->gamepads.push_back(gp);
190+
191+
std::cout << "Gamepad connected: " << gp->id << " : " << gp->name << std::endl;
192+
104193
std::thread read_thread(
105-
[this, joy_id]() { read_gamepad(&gamepads[joy_id]); });
194+
[this, gp, device]() { this->read_gamepad(gp, device); });
106195
read_thread.detach();
107196
}
108197

109-
void Gamepads::update_gamepads() {
110-
std::cout << "Updating gamepads..." << std::endl;
111-
UINT max_joysticks = joyGetNumDevs();
112-
JOYCAPSW joy_caps;
113-
for (UINT joy_id = 0; joy_id < max_joysticks; ++joy_id) {
114-
MMRESULT result = joyGetDevCapsW(joy_id, &joy_caps, sizeof(JOYCAPSW));
115-
if (result == JOYERR_NOERROR) {
116-
std::string name = to_string(joy_caps.szPname);
117-
int num_buttons = static_cast<int>(joy_caps.wNumButtons);
118-
std::optional<Gamepad> gamepad = gamepads[joy_id];
119-
if (gamepad) {
120-
if (gamepad->name != name) {
121-
std::cout << "Updated gamepad " << joy_id << std::endl;
122-
gamepad->alive = false;
123-
gamepads.erase(joy_id);
124-
125-
connect_gamepad(joy_id, name, num_buttons);
126-
}
127-
} else {
128-
std::cout << "New gamepad connected " << joy_id << std::endl;
129-
connect_gamepad(joy_id, name, num_buttons);
130-
}
198+
void Gamepads::on_gamepad_disconnected(IGameInputDevice * device)
199+
{
200+
auto info = device->GetDeviceInfo();
201+
if (info == nullptr) {
202+
std::cerr << "Gamepad disconnected but failed to read info" << std::endl;
203+
return;
204+
}
205+
std::string removeId = AppLocalDeviceIdToString(info->deviceId);
206+
std::cout << "Gamepad disconnected: " << removeId << std::endl;
207+
GamepadData* removeGp = nullptr;
208+
for (auto gp : this->gamepads) {
209+
if (gp->id == removeId) {
210+
gp->stop_thead = true;
211+
removeGp = gp;
212+
break;
131213
}
132214
}
215+
// Remove the gamepad from list. The thread will free up memory.
216+
if (removeGp != nullptr) {
217+
this->gamepads.remove(removeGp);
218+
}
133219
}
134220

135-
std::set<std::wstring> connected_devices;
136-
137-
std::optional<LRESULT> CALLBACK GamepadListenerProc(HWND hwnd,
138-
UINT uMsg,
139-
WPARAM wParam,
140-
LPARAM lParam) {
141-
switch (uMsg) {
142-
case WM_DEVICECHANGE: {
143-
if (lParam != NULL) {
144-
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
145-
if (pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
146-
PDEV_BROADCAST_DEVICEINTERFACE pDevInterface =
147-
(PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
148-
if (IsEqualGUID(pDevInterface->dbcc_classguid,
149-
GUID_DEVINTERFACE_HID)) {
150-
std::wstring device_path = pDevInterface->dbcc_name;
151-
bool is_connected =
152-
connected_devices.find(device_path) != connected_devices.end();
153-
if (!is_connected && wParam == DBT_DEVICEARRIVAL) {
154-
connected_devices.insert(device_path);
155-
gamepads.update_gamepads();
156-
} else if (is_connected && wParam == DBT_DEVICEREMOVECOMPLETE) {
157-
connected_devices.erase(device_path);
158-
gamepads.update_gamepads();
221+
222+
void Gamepads::read_gamepad(GamepadData* gamepad, IGameInputDevice* device) {
223+
auto info = device->GetDeviceInfo();
224+
225+
GameInputGamepadState previous_state;
226+
while (info != nullptr && !gamepad->stop_thead && g_gameInput != nullptr) {
227+
IGameInputReading* reading;
228+
GameInputGamepadState state;
229+
g_gameInput->GetCurrentReading(GameInputKindGamepad, device, &reading);
230+
if (reading != nullptr) {
231+
if(reading->GetGamepadState(&state)) {
232+
if (are_states_different(previous_state, state)) {
233+
auto events = diff_states(*info, state, previous_state);
234+
for (auto event : events) {
235+
if (event_emitter.has_value()) {
236+
(*event_emitter)(gamepad, event);
159237
}
160238
}
161239
}
240+
previous_state = state;
241+
reading->Release();
162242
}
163-
return 0;
164-
}
165-
case WM_DESTROY: {
166-
PostQuitMessage(0);
167-
return 0;
168243
}
244+
245+
Sleep(1);
246+
}
247+
248+
if (gamepad->stop_thead) {
249+
std::cout << "Gamepad thread exit (via signal) " << gamepad->id << std::endl;
250+
delete gamepad;
251+
} else {
252+
std::cout << "Gamepad thread exit (due to error state) " << gamepad->id << std::endl;
253+
gamepad->alive = false;
169254
}
170-
return std::nullopt;
171255
}

0 commit comments

Comments
 (0)