-
-
Notifications
You must be signed in to change notification settings - Fork 22
refactor!: Update to windows GameInput api #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
lea108
wants to merge
8
commits into
flame-engine:main
Choose a base branch
from
lea108:windows-api-rework
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+218
−158
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a7440e5
Update to Windows GameInput API
lea108 25006f4
change: use axis names from GameInput api
lea108 9de4490
change: use named button names based on GameInput api enum names
lea108 36ce1c7
remove: unused imports and dead code
lea108 3be492e
fix: in get_button_name() fallback, return something more useful
lea108 e710da8
remove: cleanup unused code/imports
lea108 d04a6f1
fix: if GameInput v3 redistributable is installed controllerButtonCou…
lea108 a4b8ded
cleanup: remove unused GetDeviceInfo()
lea108 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,171 +1,243 @@ | ||
| #include <iostream> | ||
| #define WIN32_LEAN_AND_MEAN | ||
| #include <initguid.h> | ||
| #include <windows.h> | ||
| #include <dbt.h> | ||
| #include <hidclass.h> | ||
| #pragma comment(lib, "winmm.lib") | ||
| #include <mmsystem.h> | ||
|
|
||
| #include <list> | ||
| #include <map> | ||
| #include <set> | ||
| #include <thread> | ||
| #include <algorithm> | ||
| #include <ppl.h> | ||
| #include <vector> | ||
| #include <concrt.h> | ||
| #include <winerror.h> | ||
|
|
||
| #include "gamepad.h" | ||
| #include "utils.h" | ||
| #include <GameInput.h> | ||
| #include <iomanip> | ||
| #include <sstream> | ||
| #pragma comment(lib, "GameInput.lib") | ||
|
|
||
| Gamepads gamepads; | ||
|
|
||
| std::list<Event> Gamepads::diff_states(Gamepad* gamepad, | ||
| const JOYINFOEX& old, | ||
| const JOYINFOEX& current) { | ||
|
|
||
| static IGameInput* g_gameInput = nullptr; | ||
| static IGameInputDevice* g_gamepad = nullptr; | ||
|
|
||
| std::string get_button_name(uint32_t button) { | ||
| switch (button) { | ||
| case GameInputGamepadMenu: return "menu"; | ||
| case GameInputGamepadView: return "view"; | ||
| case GameInputGamepadA: return "a"; | ||
| case GameInputGamepadB: return "b"; | ||
| case GameInputGamepadX: return "x"; | ||
| case GameInputGamepadY: return "y"; | ||
| case GameInputGamepadDPadUp: return "dpadUp"; | ||
| case GameInputGamepadDPadDown: return "dpadDown"; | ||
| case GameInputGamepadDPadLeft: return "dpadLeft"; | ||
| case GameInputGamepadDPadRight: return "dpadRight"; | ||
| case GameInputGamepadLeftShoulder: return "leftShoulder"; | ||
| case GameInputGamepadRightShoulder: return "rightShoulder"; | ||
| case GameInputGamepadLeftThumbstick: return "leftThumbstick"; | ||
| case GameInputGamepadRightThumbstick: return "rightThumbstick"; | ||
| } | ||
| return "button-" + std::to_string(button); | ||
| } | ||
|
|
||
| std::string AppLocalDeviceIdToString(const APP_LOCAL_DEVICE_ID& id) { | ||
| std::ostringstream oss; | ||
| oss << std::hex << std::setfill('0'); | ||
| for (size_t i = 0; i < APP_LOCAL_DEVICE_ID_SIZE; ++i) { | ||
| oss << std::setw(2) << static_cast<int>(id.value[i]); | ||
| } | ||
| return oss.str(); | ||
| } | ||
|
|
||
| std::list<Event> diff_states(const GameInputGamepadState& old, const GameInputGamepadState& current) { | ||
| std::time_t now = std::time(nullptr); | ||
| int time = static_cast<int>(now); | ||
|
|
||
| std::list<Event> events; | ||
| if (old.dwXpos != current.dwXpos) { | ||
| if (old.leftThumbstickX != current.leftThumbstickX) { | ||
| events.push_back( | ||
| {time, "analog", "dwXpos", static_cast<int>(current.dwXpos)}); | ||
| {time, "analog", "leftThumbstickX", current.leftThumbstickX}); | ||
| } | ||
| if (old.dwYpos != current.dwYpos) { | ||
| if (old.leftThumbstickY != current.leftThumbstickY) { | ||
| events.push_back( | ||
| {time, "analog", "dwYpos", static_cast<int>(current.dwYpos)}); | ||
| {time, "analog", "leftThumbstickY", current.leftThumbstickY}); | ||
| } | ||
| if (old.dwZpos != current.dwZpos) { | ||
| if (old.rightThumbstickX != current.rightThumbstickX) { | ||
| events.push_back( | ||
| {time, "analog", "dwZpos", static_cast<int>(current.dwZpos)}); | ||
| {time, "analog", "rightThumbstickX", current.rightThumbstickX}); | ||
| } | ||
| if (old.dwRpos != current.dwRpos) { | ||
| if (old.rightThumbstickY != current.rightThumbstickY) { | ||
| events.push_back( | ||
| {time, "analog", "dwRpos", static_cast<int>(current.dwRpos)}); | ||
| {time, "analog", "rightThumbstickY", current.rightThumbstickY}); | ||
| } | ||
| if (old.dwUpos != current.dwUpos) { | ||
| if (old.leftTrigger != current.leftTrigger) { | ||
| events.push_back( | ||
| {time, "analog", "dwUpos", static_cast<int>(current.dwUpos)}); | ||
| {time, "analog", "leftTrigger", current.leftTrigger}); | ||
| } | ||
| if (old.dwVpos != current.dwVpos) { | ||
| if (old.rightTrigger != current.rightTrigger) { | ||
| events.push_back( | ||
| {time, "analog", "dwVpos", static_cast<int>(current.dwVpos)}); | ||
| {time, "analog", "rightTrigger", current.rightTrigger}); | ||
| } | ||
| if (old.dwPOV != current.dwPOV) { | ||
| events.push_back({time, "analog", "pov", static_cast<int>(current.dwPOV)}); | ||
| } | ||
| if (old.dwButtons != current.dwButtons) { | ||
| for (int i = 0; i < gamepad->num_buttons; ++i) { | ||
| bool was_pressed = old.dwButtons & (1 << i); | ||
| bool is_pressed = current.dwButtons & (1 << i); | ||
| if (old.buttons != current.buttons) { | ||
| // While GameInputDeviceInfo.controllerButtonCount often gives 14, | ||
| // if you install GameInput v3 redistributable, the reported | ||
| // button count drops to zero. Button input is still reported. | ||
| for (uint32_t i = 0; i < 14; ++i) { | ||
| bool was_pressed = old.buttons & (1 << i); | ||
| bool is_pressed = current.buttons & (1 << i); | ||
| if (was_pressed != is_pressed) { | ||
| double value = is_pressed ? 1.0 : 0.0; | ||
| auto key = get_button_name(1 << i); | ||
| events.push_back( | ||
| {time, "button", "button-" + std::to_string(i), is_pressed}); | ||
| {time, "button", key, value}); | ||
| } | ||
| } | ||
| } | ||
| return events; | ||
| } | ||
|
|
||
| bool Gamepads::are_states_different(const JOYINFOEX& a, const JOYINFOEX& b) { | ||
| return a.dwXpos != b.dwXpos || a.dwYpos != b.dwYpos || a.dwZpos != b.dwZpos || | ||
| a.dwRpos != b.dwRpos || a.dwUpos != b.dwUpos || a.dwVpos != b.dwVpos || | ||
| a.dwButtons != b.dwButtons || a.dwPOV != b.dwPOV; | ||
| bool are_states_different(const GameInputGamepadState& a, const GameInputGamepadState& b) { | ||
| return a.leftThumbstickX != b.leftThumbstickX || | ||
| a.leftThumbstickY != b.leftThumbstickY || | ||
| a.leftTrigger != b.leftTrigger || | ||
| a.rightThumbstickX != b.rightThumbstickX || | ||
| a.rightThumbstickY != b.rightThumbstickY || | ||
| a.rightTrigger != b.rightTrigger || | ||
| a.buttons != b.buttons; | ||
| } | ||
|
|
||
| void Gamepads::read_gamepad(Gamepad* gamepad) { | ||
| JOYINFOEX state; | ||
| state.dwSize = sizeof(JOYINFOEX); | ||
| state.dwFlags = JOY_RETURNALL; | ||
| void Gamepads::init() | ||
| { | ||
| GameInputCreate(&g_gameInput); | ||
|
|
||
| int joy_id = gamepad->joy_id; | ||
| if (g_gameInput != nullptr) { | ||
| // Register listener for gamepad events | ||
| if (g_gameInput != nullptr) { | ||
| g_gameInput->RegisterDeviceCallback( | ||
| nullptr, // All devices | ||
| GameInputKindGamepad, | ||
| GameInputDeviceConnected, | ||
| GameInputAsyncEnumeration, | ||
| static_cast<void*>(this), | ||
| []( | ||
| _In_ GameInputCallbackToken callbackToken, | ||
| _In_ void * context, | ||
| _In_ IGameInputDevice * device, | ||
| _In_ uint64_t timestamp, | ||
| _In_ GameInputDeviceStatus currentStatus, | ||
| _In_ GameInputDeviceStatus previousStatus | ||
| ) { | ||
| auto* self = static_cast<Gamepads*>(context); | ||
| if (currentStatus & GameInputDeviceConnected) { | ||
| self->on_gamepad_connected(device); | ||
| } else { | ||
| self->on_gamepad_disconnected(device); | ||
| } | ||
| }, | ||
| this->deviceCallbackToken | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| std::cout << "Listening to gamepad " << joy_id << std::endl; | ||
| void Gamepads::stop() | ||
| { | ||
| if (g_gamepad) g_gamepad->Release(); | ||
| if (g_gameInput) { | ||
| g_gameInput->UnregisterCallback(*this->deviceCallbackToken, 5000); | ||
| g_gameInput->Release(); | ||
| } | ||
|
|
||
| while (gamepad->alive) { | ||
| JOYINFOEX previous_state = state; | ||
| MMRESULT result = joyGetPosEx(joy_id, &state); | ||
| if (result == JOYERR_NOERROR) { | ||
| if (are_states_different(previous_state, state)) { | ||
| std::list<Event> events = diff_states(gamepad, previous_state, state); | ||
| for (auto joy_event : events) { | ||
| if (event_emitter.has_value()) { | ||
| (*event_emitter)(gamepad, joy_event); | ||
| } | ||
| } | ||
| // Stop/cleanup threads | ||
| for (auto gp : this->gamepads) { | ||
| if (!gp->stop_thead) { | ||
| if (gp->alive) { | ||
| gp->stop_thead = true; | ||
| } else { | ||
| // Cleanup data of threads that exited due to error state. | ||
| delete gp; | ||
| } | ||
| } else { | ||
| std::cout << "Fail to listen to gamepad " << joy_id << std::endl; | ||
| gamepad->alive = false; | ||
| gamepads.erase(joy_id); | ||
| } | ||
| } | ||
| this->gamepads.clear(); | ||
| } | ||
|
|
||
| std::list<GamepadData*> Gamepads::get_gamepads() { | ||
| return this->gamepads; | ||
| } | ||
|
|
||
| void Gamepads::connect_gamepad(UINT joy_id, std::string name, int num_buttons) { | ||
| gamepads[joy_id] = {joy_id, name, num_buttons, true}; | ||
| void Gamepads::on_gamepad_connected(IGameInputDevice * device) | ||
| { | ||
| auto info = device->GetDeviceInfo(); | ||
| if (info == nullptr) { | ||
| std::cerr << "Gamepad connected but failed to read info" << std::endl; | ||
| return; | ||
| } | ||
| auto gp = new GamepadData(); | ||
| gp->id = AppLocalDeviceIdToString(info->deviceId); | ||
| gp->name = info->displayName != nullptr && info->displayName->data != nullptr ? info->displayName->data : ""; | ||
| gp->num_buttons = info->controllerButtonCount; | ||
| gp->stop_thead = false; | ||
| gp->alive = true; | ||
| this->gamepads.push_back(gp); | ||
|
|
||
| std::cout << "Gamepad connected: " << gp->id << " : " << gp->name << std::endl; | ||
|
|
||
| std::thread read_thread( | ||
| [this, joy_id]() { read_gamepad(&gamepads[joy_id]); }); | ||
| [this, gp, device]() { this->read_gamepad(gp, device); }); | ||
| read_thread.detach(); | ||
| } | ||
|
|
||
| void Gamepads::update_gamepads() { | ||
| std::cout << "Updating gamepads..." << std::endl; | ||
| UINT max_joysticks = joyGetNumDevs(); | ||
| JOYCAPSW joy_caps; | ||
| for (UINT joy_id = 0; joy_id < max_joysticks; ++joy_id) { | ||
| MMRESULT result = joyGetDevCapsW(joy_id, &joy_caps, sizeof(JOYCAPSW)); | ||
| if (result == JOYERR_NOERROR) { | ||
| std::string name = to_string(joy_caps.szPname); | ||
| int num_buttons = static_cast<int>(joy_caps.wNumButtons); | ||
| std::optional<Gamepad> gamepad = gamepads[joy_id]; | ||
| if (gamepad) { | ||
| if (gamepad->name != name) { | ||
| std::cout << "Updated gamepad " << joy_id << std::endl; | ||
| gamepad->alive = false; | ||
| gamepads.erase(joy_id); | ||
|
|
||
| connect_gamepad(joy_id, name, num_buttons); | ||
| } | ||
| } else { | ||
| std::cout << "New gamepad connected " << joy_id << std::endl; | ||
| connect_gamepad(joy_id, name, num_buttons); | ||
| } | ||
| void Gamepads::on_gamepad_disconnected(IGameInputDevice * device) | ||
| { | ||
| auto info = device->GetDeviceInfo(); | ||
| if (info == nullptr) { | ||
| std::cerr << "Gamepad disconnected but failed to read info" << std::endl; | ||
| return; | ||
| } | ||
| std::string removeId = AppLocalDeviceIdToString(info->deviceId); | ||
| std::cout << "Gamepad disconnected: " << removeId << std::endl; | ||
| GamepadData* removeGp = nullptr; | ||
| for (auto gp : this->gamepads) { | ||
| if (gp->id == removeId) { | ||
| gp->stop_thead = true; | ||
| removeGp = gp; | ||
| break; | ||
| } | ||
| } | ||
| // Remove the gamepad from list. The thread will free up memory. | ||
| if (removeGp != nullptr) { | ||
| this->gamepads.remove(removeGp); | ||
| } | ||
| } | ||
|
|
||
| std::set<std::wstring> connected_devices; | ||
|
|
||
| std::optional<LRESULT> CALLBACK GamepadListenerProc(HWND hwnd, | ||
| UINT uMsg, | ||
| WPARAM wParam, | ||
| LPARAM lParam) { | ||
| switch (uMsg) { | ||
| case WM_DEVICECHANGE: { | ||
| if (lParam != NULL) { | ||
| PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam; | ||
| if (pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { | ||
| PDEV_BROADCAST_DEVICEINTERFACE pDevInterface = | ||
| (PDEV_BROADCAST_DEVICEINTERFACE)pHdr; | ||
| if (IsEqualGUID(pDevInterface->dbcc_classguid, | ||
| GUID_DEVINTERFACE_HID)) { | ||
| std::wstring device_path = pDevInterface->dbcc_name; | ||
| bool is_connected = | ||
| connected_devices.find(device_path) != connected_devices.end(); | ||
| if (!is_connected && wParam == DBT_DEVICEARRIVAL) { | ||
| connected_devices.insert(device_path); | ||
| gamepads.update_gamepads(); | ||
| } else if (is_connected && wParam == DBT_DEVICEREMOVECOMPLETE) { | ||
| connected_devices.erase(device_path); | ||
| gamepads.update_gamepads(); | ||
|
|
||
| void Gamepads::read_gamepad(GamepadData* gamepad, IGameInputDevice* device) { | ||
| GameInputGamepadState previous_state; | ||
| while (!gamepad->stop_thead && g_gameInput != nullptr) { | ||
| IGameInputReading* reading; | ||
| GameInputGamepadState state; | ||
| g_gameInput->GetCurrentReading(GameInputKindGamepad, device, &reading); | ||
| if (reading != nullptr) { | ||
| if(reading->GetGamepadState(&state)) { | ||
| if (are_states_different(previous_state, state)) { | ||
| auto events = diff_states(state, previous_state); | ||
| for (auto event : events) { | ||
| if (event_emitter.has_value()) { | ||
| (*event_emitter)(gamepad, event); | ||
| } | ||
| } | ||
| } | ||
| previous_state = state; | ||
| reading->Release(); | ||
| } | ||
| return 0; | ||
| } | ||
| case WM_DESTROY: { | ||
| PostQuitMessage(0); | ||
| return 0; | ||
| } | ||
|
|
||
| Sleep(1); | ||
| } | ||
|
|
||
| if (gamepad->stop_thead) { | ||
| std::cout << "Gamepad thread exit (via signal) " << gamepad->id << std::endl; | ||
| delete gamepad; | ||
| } else { | ||
| std::cout << "Gamepad thread exit (due to error state) " << gamepad->id << std::endl; | ||
| gamepad->alive = false; | ||
| } | ||
| return std::nullopt; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we still have a thread reading the gamepad state?Can we use RegisterReadingCallback to listen to the update of gamepad states?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have an experiment branch on my github using that. I haven't wired it up back to Flutter, but it prints out to the log when those events are recevied.
However, it requires both developers and end-users to install an MSI that the NuGet package downloads which requires elevated user privileges in Windows.
I had a chat with @spydon in discord where we decided that due to that limitation, it was better to stay at GameInput v0 which this PR uses that does not require this MSI to be installed in order to receive gamepad data.
That said, if you have a game title that ship with an installer, at that point it probably is doable to include it. But then I think it is more and more common for even games to install to you user folder to skip the elevated user privileges dialog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it wasn't for the requirement to install this .msi-file, I am all with you and I think using the callback would be better as GameInput itself has an input thread that calls the callback.
Now it is a question of priority - easy setup and easy distribution vs best possible performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense!