Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 188 additions & 116 deletions packages/gamepads_windows/windows/gamepad.cpp
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(
Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

[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;
}
Loading