rawinput: add basic support for xinput (#617)

## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
Adds preliminary support for XInput controllers. Enough to manually bind
buttons and map analogs.
This commit is contained in:
bicarus
2026-04-09 02:49:20 -07:00
committed by GitHub
parent f89efa2558
commit 7b4227d2a9
13 changed files with 511 additions and 6 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ namespace rawinput {
SEXTET_OUTPUT,
PIUIO_DEVICE,
SMX_STAGE,
SMX_DEDICAB
SMX_DEDICAB,
XINPUT_GAMEPAD,
};
enum MouseKeys {
+31
View File
@@ -70,6 +70,8 @@ void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
rawinput::RawInputManager::RawInputManager() {
XINPUT_MGR = std::make_unique<xinput::XInputManager>();
// create input window and load in devices
this->input_hwnd_create();
this->devices_reload();
@@ -106,6 +108,8 @@ void rawinput::RawInputManager::stop() {
// destruct all devices and input window
this->devices_destruct();
this->input_hwnd_destroy();
XINPUT_MGR.reset();
}
void rawinput::RawInputManager::input_hwnd_create() {
@@ -191,6 +195,8 @@ void rawinput::RawInputManager::devices_reload() {
this->devices_scan_smxdedicab();
}
this->devices_scan_xinput();
// check for LIT Board
sextet_register("COM54", "LIT Board", false);
@@ -1009,6 +1015,27 @@ void rawinput::RawInputManager::devices_scan_smxdedicab() {
}
}
void rawinput::RawInputManager::devices_scan_xinput() {
log_misc("rawinput", "scan XInput devices...");
const auto players = XINPUT_MGR->get_available_players();
for (const auto player : players) {
auto *new_xinput_device = new Device();
new_xinput_device->type = XINPUT_GAMEPAD;
new_xinput_device->name = fmt::format(";XINPUT;{}", player);
new_xinput_device->desc = fmt::format("XInput Gamepad P{}", player + 1);
new_xinput_device->handle = reinterpret_cast<HANDLE>(player);
new_xinput_device->mutex = new std::mutex();
new_xinput_device->mutex_out = new std::mutex();
auto &device = this->devices.emplace_back(*new_xinput_device);
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
}
}
void rawinput::RawInputManager::flush_start() {
// start flush thread
@@ -2734,6 +2761,10 @@ void rawinput::RawInputManager::devices_print() {
log_misc("rawinput", "device type: SMX_DEDICAB");
break;
}
case XINPUT_GAMEPAD: {
log_misc("rawinput", "device type: XINPUT");
break;
}
case UNKNOWN:
default:
log_warning("rawinput", "device type: UNKNOWN");
+3
View File
@@ -10,6 +10,7 @@
#include "device.h"
#include "hotplug.h"
#include "rawinput/xinput.h"
#include "util/scope_guard.h"
namespace rawinput {
@@ -84,6 +85,7 @@ namespace rawinput {
void devices_scan_piuio();
void devices_scan_smxstage();
void devices_scan_smxdedicab();
void devices_scan_xinput();
void devices_destruct();
void devices_destruct(Device *device, bool log = true);
void flush_start();
@@ -101,6 +103,7 @@ namespace rawinput {
public:
HWND input_hwnd = nullptr;
std::unique_ptr<xinput::XInputManager> XINPUT_MGR = nullptr;
RawInputManager();
~RawInputManager();
+329
View File
@@ -0,0 +1,329 @@
#include "rawinput/xinput.h"
#include "util/logging.h"
namespace xinput {
// this is all we need to emulate xinput.h which we avoid including here...
#define XINPUT_GAMEPAD_TRIGGER_THRESHOLD (30 / 255.0f)
#define XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE 7849
#define XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 8689
#define XINPUT_GAMEPAD_DPAD_UP 0x0001
#define XINPUT_GAMEPAD_DPAD_DOWN 0x0002
#define XINPUT_GAMEPAD_DPAD_LEFT 0x0004
#define XINPUT_GAMEPAD_DPAD_RIGHT 0x0008
#define XINPUT_GAMEPAD_START 0x0010
#define XINPUT_GAMEPAD_BACK 0x0020
#define XINPUT_GAMEPAD_LEFT_THUMB 0x0040
#define XINPUT_GAMEPAD_RIGHT_THUMB 0x0080
#define XINPUT_GAMEPAD_LEFT_SHOULDER 0x0100
#define XINPUT_GAMEPAD_RIGHT_SHOULDER 0x0200
#define XINPUT_GAMEPAD_A 0x1000
#define XINPUT_GAMEPAD_B 0x2000
#define XINPUT_GAMEPAD_X 0x4000
#define XINPUT_GAMEPAD_Y 0x8000
typedef struct {
uint32_t dwPacketNumber;
XINPUT_GAMEPAD_STATE Gamepad;
} XINPUT_STATE;
DWORD
XInputGetState(
DWORD dwUserIndex,
XINPUT_STATE *pState
);
// end xinput definitions
std::string get_button_string(XInputButtonEnum button) {
switch (button) {
case XInputButtonEnum::DPAD_UP:
return "Up";
case XInputButtonEnum::DPAD_DOWN:
return "Down";
case XInputButtonEnum::DPAD_LEFT:
return "Left";
case XInputButtonEnum::DPAD_RIGHT:
return "Right";
case XInputButtonEnum::START:
return "Start";
case XInputButtonEnum::BACK:
return "Back";
case XInputButtonEnum::LEFT_STICK:
return "Left Stick Click";
case XInputButtonEnum::RIGHT_STICK:
return "Right Stick Click";
case XInputButtonEnum::LEFT_SHOULDER:
return "Left Bumper";
case XInputButtonEnum::RIGHT_SHOULDER:
return "Right Bumper";
case XInputButtonEnum::BUTTON_A:
return "Button A";
case XInputButtonEnum::BUTTON_B:
return "Button B";
case XInputButtonEnum::BUTTON_X:
return "Button X";
case XInputButtonEnum::BUTTON_Y:
return "Button Y";
case XInputButtonEnum::LEFT_TRIGGER:
return "Left Trigger";
case XInputButtonEnum::RIGHT_TRIGGER:
return "Right Trigger";
case XInputButtonEnum::LEFT_STICK_UP:
return "Left Stick Up";
case XInputButtonEnum::LEFT_STICK_DOWN:
return "Left Stick Down";
case XInputButtonEnum::LEFT_STICK_LEFT:
return "Left Stick Left";
case XInputButtonEnum::LEFT_STICK_RIGHT:
return "Left Stick Right";
case XInputButtonEnum::RIGHT_STICK_UP:
return "Right Stick Up";
case XInputButtonEnum::RIGHT_STICK_DOWN:
return "Right Stick Down";
case XInputButtonEnum::RIGHT_STICK_LEFT:
return "Right Stick Left";
case XInputButtonEnum::RIGHT_STICK_RIGHT:
return "Right Stick Right";
default:
break;
}
return fmt::format("Unknown Button ({})", static_cast<int>(button));
}
std::string get_analog_string(XInputAnalogEnum analog) {
switch (analog) {
case XInputAnalogEnum::LEFT_TRIGGER:
return "Left Trigger";
case XInputAnalogEnum::RIGHT_TRIGGER:
return "Right Trigger";
case XInputAnalogEnum::LEFT_STICK_X:
return "Left Stick X";
case XInputAnalogEnum::LEFT_STICK_Y:
return "Left Stick Y";
case XInputAnalogEnum::RIGHT_STICK_X:
return "Right Stick X";
case XInputAnalogEnum::RIGHT_STICK_Y:
return "Right Stick Y";
default:
break;
}
return fmt::format("Unknown Analog ({})", static_cast<int>(analog));
}
#if defined(SPICE_XP)
XInputManager::XInputManager() {}
XInputManager::~XInputManager() {}
void XInputManager::stop() {}
std::vector<uint8_t> XInputManager::get_available_players() {
return {};
}
float XInputManager::get_analog_state(uint8_t player, XInputAnalogEnum analog) {
return 0.5f;
}
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button) {
return false;
}
#else
static decltype(XInputGetState) *XInputGetState_addr = nullptr;
XInputManager::XInputManager() {
log_info("xinput", "initialize...");
this->xinput_lib = LoadLibraryA("xinput1_3.dll");
if (!this->xinput_lib) {
log_warning("xinput", "failed to load xinput1_3.dll");
return;
}
XInputGetState_addr = reinterpret_cast<decltype(XInputGetState) *>(
GetProcAddress(this->xinput_lib, "XInputGetState"));
if (!XInputGetState_addr) {
log_warning("xinput", "failed to get XInputGetState address");
this->stop();
return;
}
initialized = true;
log_info("xinput", "initialized");
}
XInputManager::~XInputManager() {
this->stop();
}
void XInputManager::stop() {
if (this->xinput_lib) {
FreeLibrary(this->xinput_lib);
this->xinput_lib = nullptr;
}
XInputGetState_addr = nullptr;
this->initialized = false;
log_info("xinput", "destroyed");
}
std::vector<uint8_t> XInputManager::get_available_players() {
std::vector<uint8_t> players;
if (!this->initialized) {
return players;
}
for (uint8_t i = 0; i < XUSER_MAX_COUNT; i++) {
XINPUT_STATE x;
if (XInputGetState_addr(i, &x) == ERROR_SUCCESS) {
players.emplace_back(i);
}
}
return players;
}
void XInputManager::get_state(uint8_t player, XINPUT_GAMEPAD_STATE_NORMALIZED *state) {
*state = {};
if (!this->initialized) {
return;
}
XINPUT_STATE x;
if (XInputGetState_addr(player, &x) != ERROR_SUCCESS) {
return;
}
state->wButtons = x.Gamepad.wButtons;
state->bLeftTrigger = x.Gamepad.bLeftTrigger / 255.0f;
state->bRightTrigger = x.Gamepad.bRightTrigger / 255.0f;
// left stick circular dead zone
{
const auto x_raw = x.Gamepad.sThumbLX;
const auto y_raw = x.Gamepad.sThumbLY;
const float magnitude = sqrtf(x_raw * x_raw + y_raw * y_raw);
if (magnitude > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) {
const float scaled =
(magnitude - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) /
(32767.0f - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
state->sThumbLX = (x_raw / magnitude) * scaled;
state->sThumbLY = (y_raw / magnitude) * scaled;
// normalize to [0, 1]
state->sThumbLX = std::clamp((state->sThumbLX + 1.f) / 2.f, 0.f, 1.f);
state->sThumbLY = std::clamp((state->sThumbLY + 1.f) / 2.f, 0.f, 1.f);
} else {
// within deadzone
state->sThumbLX = 0.5f;
state->sThumbLY = 0.5f;
}
}
// right stick circular dead zone
{
const auto x_raw = x.Gamepad.sThumbRX;
const auto y_raw = x.Gamepad.sThumbRY;
const float magnitude = sqrtf(x_raw * x_raw + y_raw * y_raw);
if (magnitude > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) {
const float scaled =
(magnitude - XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) /
(32767.0f - XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
state->sThumbRX = (x_raw / magnitude) * scaled;
state->sThumbRY = (y_raw / magnitude) * scaled;
// normalize to [0, 1]
state->sThumbRX = std::clamp((state->sThumbRX + 1.f) / 2.f, 0.f, 1.f);
state->sThumbRY = std::clamp((state->sThumbRY + 1.f) / 2.f, 0.f, 1.f);
} else {
// within deadzone
state->sThumbRX = 0.5f;
state->sThumbRY = 0.5f;
}
}
}
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button) {
XINPUT_GAMEPAD_STATE_NORMALIZED state;
get_state(player, &state);
switch (button) {
case XInputButtonEnum::DPAD_UP:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_UP) != 0;
case XInputButtonEnum::DPAD_DOWN:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) != 0;
case XInputButtonEnum::DPAD_LEFT:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) != 0;
case XInputButtonEnum::DPAD_RIGHT:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0;
case XInputButtonEnum::START:
return (state.wButtons & XINPUT_GAMEPAD_START) != 0;
case XInputButtonEnum::BACK:
return (state.wButtons & XINPUT_GAMEPAD_BACK) != 0;
case XInputButtonEnum::LEFT_STICK:
return (state.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0;
case XInputButtonEnum::RIGHT_STICK:
return (state.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) != 0;
case XInputButtonEnum::LEFT_SHOULDER:
return (state.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0;
case XInputButtonEnum::RIGHT_SHOULDER:
return (state.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0;
case XInputButtonEnum::BUTTON_A:
return (state.wButtons & XINPUT_GAMEPAD_A) != 0;
case XInputButtonEnum::BUTTON_B:
return (state.wButtons & XINPUT_GAMEPAD_B) != 0;
case XInputButtonEnum::BUTTON_X:
return (state.wButtons & XINPUT_GAMEPAD_X) != 0;
case XInputButtonEnum::BUTTON_Y:
return (state.wButtons & XINPUT_GAMEPAD_Y) != 0;
case XInputButtonEnum::LEFT_TRIGGER:
return state.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
case XInputButtonEnum::RIGHT_TRIGGER:
return state.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
case XInputButtonEnum::LEFT_STICK_UP:
return state.sThumbLY > 0.6f;
case XInputButtonEnum::LEFT_STICK_DOWN:
return state.sThumbLY < 0.4f;
case XInputButtonEnum::LEFT_STICK_LEFT:
return state.sThumbLX < 0.4f;
case XInputButtonEnum::LEFT_STICK_RIGHT:
return state.sThumbLX > 0.6f;
case XInputButtonEnum::RIGHT_STICK_UP:
return state.sThumbRY > 0.6f;
case XInputButtonEnum::RIGHT_STICK_DOWN:
return state.sThumbRY < 0.4f;
case XInputButtonEnum::RIGHT_STICK_LEFT:
return state.sThumbRX < 0.4f;
case XInputButtonEnum::RIGHT_STICK_RIGHT:
return state.sThumbRX > 0.6f;
default:
break;
}
return false;
}
float XInputManager::get_analog_state(uint8_t player, XInputAnalogEnum analog) {
XINPUT_GAMEPAD_STATE_NORMALIZED state;
get_state(player, &state);
switch (analog) {
case XInputAnalogEnum::LEFT_TRIGGER:
return state.bLeftTrigger;
case XInputAnalogEnum::RIGHT_TRIGGER:
return state.bRightTrigger;
case XInputAnalogEnum::LEFT_STICK_X:
return state.sThumbLX;
case XInputAnalogEnum::LEFT_STICK_Y:
return state.sThumbLY;
case XInputAnalogEnum::RIGHT_STICK_X:
return state.sThumbRX;
case XInputAnalogEnum::RIGHT_STICK_Y:
return state.sThumbRY;
default:
break;
}
return 0.5f;
}
#endif
}
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include <cstdint>
#include <array>
#include <vector>
#include <string>
#include <windows.h>
namespace xinput {
#define XUSER_MAX_COUNT 4
typedef struct _XINPUT_GAMEPAD_STATE {
uint16_t wButtons;
uint8_t bLeftTrigger;
uint8_t bRightTrigger;
int16_t sThumbLX;
int16_t sThumbLY;
int16_t sThumbRX;
int16_t sThumbRY;
} XINPUT_GAMEPAD_STATE;
typedef struct _XINPUT_GAMEPAD_STATE_NORMALIZED {
uint16_t wButtons;
float bLeftTrigger;
float bRightTrigger;
float sThumbLX;
float sThumbLY;
float sThumbRX;
float sThumbRY;
} XINPUT_GAMEPAD_STATE_NORMALIZED;
enum class XInputButtonEnum {
// actual buttons
DPAD_UP,
DPAD_DOWN,
DPAD_LEFT,
DPAD_RIGHT,
START,
BACK,
LEFT_STICK,
RIGHT_STICK,
LEFT_SHOULDER,
RIGHT_SHOULDER,
BUTTON_A,
BUTTON_B,
BUTTON_X,
BUTTON_Y,
// analog values that can be used as buttons
LEFT_TRIGGER,
RIGHT_TRIGGER,
LEFT_STICK_UP,
LEFT_STICK_DOWN,
LEFT_STICK_LEFT,
LEFT_STICK_RIGHT,
RIGHT_STICK_UP,
RIGHT_STICK_DOWN,
RIGHT_STICK_LEFT,
RIGHT_STICK_RIGHT,
COUNT
};
enum class XInputAnalogEnum {
// analog values
LEFT_TRIGGER,
RIGHT_TRIGGER,
LEFT_STICK_X,
LEFT_STICK_Y,
RIGHT_STICK_X,
RIGHT_STICK_Y,
COUNT
};
std::string get_button_string(XInputButtonEnum button);
std::string get_analog_string(XInputAnalogEnum analog);
class XInputManager {
private:
bool initialized = false;
HMODULE xinput_lib = nullptr;
void get_state(uint8_t player, XINPUT_GAMEPAD_STATE_NORMALIZED *state);
public:
XInputManager();
~XInputManager();
void stop();
std::vector<uint8_t> get_available_players();
bool is_button_pressed(uint8_t player, XInputButtonEnum button);
float get_analog_state(uint8_t player, XInputAnalogEnum analog);
};
}