Files
Xenia-Canary/src/xenia/hid/sdl/sdl_input_driver.cc
2024-12-26 23:42:31 +01:00

807 lines
26 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/hid/sdl/sdl_input_driver.h"
#include <array>
#if XE_PLATFORM_WIN32
#include "xenia/base/platform_win.h"
#endif // XE_PLATFORM_WIN32
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/helper/sdl/sdl_helper.h"
#include "xenia/hid/hid_flags.h"
#include "xenia/ui/virtual_key.h"
#include "xenia/ui/window.h"
#include "xenia/ui/windowed_app_context.h"
// TODO(joellinn) make this path relative to the config folder.
DEFINE_path(mappings_file, "gamecontrollerdb.txt",
"Filename of a database with custom game controller mappings.",
"SDL");
namespace xe {
namespace hid {
namespace sdl {
SDLInputDriver::SDLInputDriver(xe::ui::Window* window, size_t window_z_order)
: InputDriver(window, window_z_order),
sdl_events_initialized_(false),
sdl_gamecontroller_initialized_(false),
sdl_events_unflushed_(0),
sdl_pumpevents_queued_(false),
controllers_(),
keystroke_states_() {}
SDLInputDriver::~SDLInputDriver() {
// Make sure the CallInUIThread is executed before destroying the references.
if (sdl_pumpevents_queued_) {
window()->app_context().CallInUIThreadSynchronous([this]() {
window()->app_context().ExecutePendingFunctionsFromUIThread();
});
}
for (size_t i = 0; i < controllers_.size(); i++) {
if (controllers_.at(i).sdl) {
SDL_GameControllerClose(controllers_.at(i).sdl);
controllers_.at(i) = {};
}
}
if (sdl_events_initialized_) {
SDL_QuitSubSystem(SDL_INIT_EVENTS);
sdl_events_initialized_ = false;
}
if (sdl_gamecontroller_initialized_) {
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
sdl_gamecontroller_initialized_ = false;
}
}
X_STATUS SDLInputDriver::Setup() {
if (!TestSDLVersion()) {
return X_STATUS_UNSUCCESSFUL;
}
// SDL_PumpEvents should only be run in the thread that initialized SDL - we
// are hijacking the UI thread for that. If this function fails to be queued,
// the "initialized" variables will be false - that's handled safely.
window()->app_context().CallInUIThreadSynchronous([this]() {
if (!xe::helper::sdl::SDLHelper::Prepare()) {
return;
}
// Initialize the event system early, so we catch device events for already
// connected controllers.
if (SDL_InitSubSystem(SDL_INIT_EVENTS) < 0) {
return;
}
sdl_events_initialized_ = true;
// With an event watch we will always get notified, even if the event queue
// is full, which can happen if another subsystem does not clear its events.
SDL_AddEventWatch(
[](void* userdata, SDL_Event* event) -> int {
if (!userdata || !event) {
assert_always();
return 0;
}
const auto type = event->type;
if (type < SDL_JOYAXISMOTION || type >= SDL_FINGERDOWN) {
return 0;
}
// If another part of xenia uses another SDL subsystem that generates
// events, this may seem like a bad idea. They will however not
// subscribe to controller events so we get away with that.
const auto driver = static_cast<SDLInputDriver*>(userdata);
driver->HandleEvent(*event);
return 0;
},
this);
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
return;
}
sdl_gamecontroller_initialized_ = true;
LoadGameControllerDB();
});
return (sdl_events_initialized_ && sdl_gamecontroller_initialized_)
? X_STATUS_SUCCESS
: X_STATUS_UNSUCCESSFUL;
}
void SDLInputDriver::LoadGameControllerDB() {
if (cvars::mappings_file.empty()) {
return;
}
if (!std::filesystem::exists(cvars::mappings_file)) {
XELOGW("SDL GameControllerDB: file '{}' does not exist.",
cvars::mappings_file);
return;
}
XELOGI("SDL GameControllerDB: Loading {}", cvars::mappings_file);
uint32_t updated_mappings = 0;
uint32_t added_mappings = 0;
rapidcsv::Document mappings(
xe::path_to_utf8(cvars::mappings_file), rapidcsv::LabelParams(-1, -1),
rapidcsv::SeparatorParams(), rapidcsv::ConverterParams(),
rapidcsv::LineReaderParams(true /* pSkipCommentLines */,
'#' /* pCommentPrefix */,
true /* pSkipEmptyLines */));
for (size_t i = 0; i < mappings.GetRowCount(); i++) {
std::vector<std::string> row = mappings.GetRow<std::string>(i);
if (row.size() < 2) {
continue;
}
std::string guid = row[0];
std::string controller_name = row[1];
auto format = [](std::string ss, const std::string& s) {
return ss.empty() ? s : ss + "," + s;
};
std::string mapping_str =
std::accumulate(row.begin(), row.end(), std::string{}, format);
int updated = SDL_GameControllerAddMapping(mapping_str.c_str());
switch (updated) {
case 0: {
XELOGI("SDL GameControllerDB: Updated {}, {}", controller_name, guid);
updated_mappings++;
} break;
case 1: {
added_mappings++;
} break;
default:
XELOGW("SDL GameControllerDB: error loading mapping '{}'", mapping_str);
break;
}
}
for (uint32_t i = 0; i < HID_SDL_USER_COUNT; i++) {
auto controller = GetControllerState(i);
if (controller) {
XELOGI("SDL Controller {}: {}", i,
SDL_GameControllerMapping(controller->sdl));
}
}
XELOGI("SDL GameControllerDB: Updated {} mappings.", updated_mappings);
XELOGI("SDL GameControllerDB: Added {} mappings.", added_mappings);
}
X_RESULT SDLInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags,
X_INPUT_CAPABILITIES* out_caps) {
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
if (user_index >= HID_SDL_USER_COUNT || !out_caps) {
return X_ERROR_BAD_ARGUMENTS;
}
QueueControllerUpdate();
auto controller = GetControllerState(user_index);
if (!controller) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
// Unfortunately drivers can't present all information immediately (e.g.
// battery information) so this needs to be refreshed every time.
UpdateXCapabilities(*controller);
std::memcpy(out_caps, &controller->caps, sizeof(*out_caps));
return X_ERROR_SUCCESS;
}
X_RESULT SDLInputDriver::GetState(uint32_t user_index,
X_INPUT_STATE* out_state) {
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
if (user_index >= HID_SDL_USER_COUNT) {
return X_ERROR_BAD_ARGUMENTS;
}
auto is_active = this->is_active();
if (is_active) {
QueueControllerUpdate();
}
auto controller = GetControllerState(user_index);
if (!controller) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
// Make sure packet_number is only incremented by 1, even if there have been
// multiple updates between GetState calls. Also track `is_active` to
// increment the packet number if it changed.
if ((is_active != controller->is_active) ||
(is_active && controller->state_changed)) {
controller->state.packet_number++;
controller->is_active = is_active;
controller->state_changed = false;
}
std::memcpy(out_state, &controller->state, sizeof(*out_state));
if (!is_active) {
// Simulate an "untouched" controller. When we become active again the
// pressed buttons aren't lost and will be visible again.
std::memset(&out_state->gamepad, 0, sizeof(out_state->gamepad));
}
return X_ERROR_SUCCESS;
}
X_RESULT SDLInputDriver::SetState(uint32_t user_index,
X_INPUT_VIBRATION* vibration) {
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
if (user_index >= HID_SDL_USER_COUNT) {
return X_ERROR_BAD_ARGUMENTS;
}
QueueControllerUpdate();
auto controller = GetControllerState(user_index);
if (!controller) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (SDL_GameControllerRumble(controller->sdl, vibration->left_motor_speed,
vibration->right_motor_speed, 0)) {
return X_ERROR_FUNCTION_FAILED;
} else {
return X_ERROR_SUCCESS;
}
#else
return X_ERROR_SUCCESS;
#endif
}
X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags,
X_INPUT_KEYSTROKE* out_keystroke) {
// TODO(JoelLinn): Figure out the flags
// https://github.com/evilC/UCR/blob/0489929e2a8e39caa3484c67f3993d3fba39e46f/Libraries/XInput.ahk#L85-L98
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
bool user_any = users == XUserIndexAny;
if (users >= HID_SDL_USER_COUNT && !user_any) {
return X_ERROR_BAD_ARGUMENTS;
}
if (!out_keystroke) {
return X_ERROR_BAD_ARGUMENTS;
}
// The order of this list is also the order in which events are send if
// multiple buttons change at once.
static_assert(sizeof(X_INPUT_GAMEPAD::buttons) == 2);
static constexpr std::array<ui::VirtualKey, 35> kVkLookup = {
// 00 - True buttons from xinput button field
ui::VirtualKey::kXInputPadDpadUp,
ui::VirtualKey::kXInputPadDpadDown,
ui::VirtualKey::kXInputPadDpadLeft,
ui::VirtualKey::kXInputPadDpadRight,
ui::VirtualKey::kXInputPadStart,
ui::VirtualKey::kXInputPadBack,
ui::VirtualKey::kXInputPadLThumbPress,
ui::VirtualKey::kXInputPadRThumbPress,
ui::VirtualKey::kXInputPadLShoulder,
ui::VirtualKey::kXInputPadRShoulder,
// Guide has no VK (kNone), however using kXInputPadGuide.
ui::VirtualKey::kXInputPadGuide,
ui::VirtualKey::kNone, /* Unknown */
ui::VirtualKey::kXInputPadA,
ui::VirtualKey::kXInputPadB,
ui::VirtualKey::kXInputPadX,
ui::VirtualKey::kXInputPadY,
// 16 - Fake buttons generated from analog inputs
ui::VirtualKey::kXInputPadLTrigger,
ui::VirtualKey::kXInputPadRTrigger,
// 18
ui::VirtualKey::kXInputPadLThumbUp,
ui::VirtualKey::kXInputPadLThumbDown,
ui::VirtualKey::kXInputPadLThumbRight,
ui::VirtualKey::kXInputPadLThumbLeft,
ui::VirtualKey::kXInputPadLThumbUpLeft,
ui::VirtualKey::kXInputPadLThumbUpRight,
ui::VirtualKey::kXInputPadLThumbDownRight,
ui::VirtualKey::kXInputPadLThumbDownLeft,
// 26
ui::VirtualKey::kXInputPadRThumbUp,
ui::VirtualKey::kXInputPadRThumbDown,
ui::VirtualKey::kXInputPadRThumbRight,
ui::VirtualKey::kXInputPadRThumbLeft,
ui::VirtualKey::kXInputPadRThumbUpLeft,
ui::VirtualKey::kXInputPadRThumbUpRight,
ui::VirtualKey::kXInputPadRThumbDownRight,
ui::VirtualKey::kXInputPadRThumbDownLeft,
};
auto is_active = this->is_active();
if (is_active) {
QueueControllerUpdate();
}
for (uint32_t user_index = (user_any ? 0 : users);
user_index < (user_any ? HID_SDL_USER_COUNT : users + 1); user_index++) {
auto controller = GetControllerState(user_index);
if (!controller) {
if (user_any) {
continue;
} else {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
}
// If input is not active (e.g. due to a dialog overlay), force buttons to
// "unpressed". The algorithm will automatically send UP events when
// `is_active()` goes low and DOWN events when it goes high again.
const uint64_t curr_butts =
is_active ? (controller->state.gamepad.buttons |
AnalogToKeyfield(controller->state.gamepad))
: uint64_t(0);
KeystrokeState& last = keystroke_states_.at(user_index);
// Handle repeating
auto guest_now = Clock::QueryGuestUptimeMillis();
static_assert(HID_SDL_REPEAT_DELAY >= HID_SDL_REPEAT_RATE);
if (last.repeat_state == RepeatState::Waiting &&
(last.repeat_time + HID_SDL_REPEAT_DELAY < guest_now)) {
last.repeat_state = RepeatState::Repeating;
}
if (last.repeat_state == RepeatState::Repeating &&
(last.repeat_time + HID_SDL_REPEAT_RATE < guest_now)) {
last.repeat_time = guest_now;
ui::VirtualKey vk = kVkLookup.at(last.repeat_butt_idx);
assert_true(vk != ui::VirtualKey::kNone);
out_keystroke->virtual_key = uint16_t(vk);
out_keystroke->unicode = 0;
out_keystroke->user_index = user_index;
out_keystroke->hid_code = 0;
out_keystroke->flags =
X_INPUT_KEYSTROKE_KEYDOWN | X_INPUT_KEYSTROKE_REPEAT;
return X_ERROR_SUCCESS;
}
auto butts_changed = curr_butts ^ last.buttons;
if (!butts_changed) {
continue;
}
// First try to clear buttons with up events. This is to match xinput
// behaviour when transitioning thumb sticks, e.g. so that THUMB_UPLEFT is
// up before THUMB_LEFT is down.
for (auto [clear_pass, i] = std::tuple{true, 0}; i < 2;
clear_pass = false, i++) {
for (uint8_t i = 0; i < uint8_t(std::size(kVkLookup)); i++) {
auto fbutton = uint64_t(1) << i;
if (!(butts_changed & fbutton)) {
continue;
}
ui::VirtualKey vk = kVkLookup.at(i);
if (vk == ui::VirtualKey::kNone) {
continue;
}
out_keystroke->virtual_key = uint16_t(vk);
out_keystroke->unicode = 0;
out_keystroke->user_index = user_index;
out_keystroke->hid_code = 0;
bool is_pressed = curr_butts & fbutton;
if (clear_pass && !is_pressed) {
// up
out_keystroke->flags = X_INPUT_KEYSTROKE_KEYUP;
last.buttons &= ~fbutton;
last.repeat_state = RepeatState::Idle;
return X_ERROR_SUCCESS;
}
if (!clear_pass && is_pressed) {
// down
out_keystroke->flags = X_INPUT_KEYSTROKE_KEYDOWN;
last.buttons |= fbutton;
last.repeat_state = RepeatState::Waiting;
last.repeat_butt_idx = i;
last.repeat_time = guest_now;
return X_ERROR_SUCCESS;
}
}
}
}
return X_ERROR_EMPTY;
}
InputType SDLInputDriver::GetInputType() const { return InputType::Controller; }
void SDLInputDriver::HandleEvent(const SDL_Event& event) {
// This callback will likely run on the thread that posts the event, which
// may be a dedicated thread SDL has created for the joystick subsystem.
// Event queue should never be (this) full
assert(SDL_PeepEvents(nullptr, 0, SDL_PEEKEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT) < 0xFFFF);
// The queue could grow up to 3.5MB since it is never polled.
if (++sdl_events_unflushed_ > 64) {
SDL_FlushEvents(SDL_JOYAXISMOTION, SDL_FINGERDOWN - 1);
sdl_events_unflushed_ = 0;
}
switch (event.type) {
case SDL_CONTROLLERDEVICEADDED:
OnControllerDeviceAdded(event);
break;
case SDL_CONTROLLERDEVICEREMOVED:
OnControllerDeviceRemoved(event);
break;
case SDL_CONTROLLERAXISMOTION:
OnControllerDeviceAxisMotion(event);
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
OnControllerDeviceButtonChanged(event);
break;
default:
break;
}
return;
}
void SDLInputDriver::OnControllerDeviceAdded(const SDL_Event& event) {
// Open the controller.
const auto controller = SDL_GameControllerOpen(event.cdevice.which);
if (!controller) {
assert_always();
return;
}
char guid_str[33];
SDL_JoystickGetGUIDString(
SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(controller)), guid_str,
33);
XELOGI(
"SDL OnControllerDeviceAdded: \"{}\", "
"JoystickType({}), "
"GameControllerType({}), "
"VendorID(0x{:04X}), "
"ProductID(0x{:04X}), "
"GUID({})",
SDL_GameControllerName(controller),
static_cast<uint32_t>(
SDL_JoystickGetType(SDL_GameControllerGetJoystick(controller))),
#if SDL_VERSION_ATLEAST(2, 0, 12)
static_cast<uint32_t>(SDL_GameControllerGetType(controller)),
#else
"?",
#endif
#if SDL_VERSION_ATLEAST(2, 0, 6)
SDL_GameControllerGetVendor(controller),
SDL_GameControllerGetProduct(controller),
#else
"?", "?",
#endif
guid_str);
int user_id = -1;
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Check if the controller has a player index LED.
user_id = SDL_GameControllerGetPlayerIndex(controller);
// Is that id already taken?
if (user_id < 0 || user_id >= controllers_.size() ||
controllers_.at(user_id).sdl) {
user_id = -1;
}
#endif
// No player index or already taken, just take the first free slot.
if (user_id < 0) {
for (size_t i = 0; i < controllers_.size(); i++) {
if (!controllers_.at(i).sdl) {
user_id = static_cast<int>(i);
#if SDL_VERSION_ATLEAST(2, 0, 12)
SDL_GameControllerSetPlayerIndex(controller, user_id);
#endif
break;
}
}
}
if (user_id >= 0) {
auto& state = controllers_.at(user_id);
state = {controller, {}};
// XInput seems to start with packet_number = 1 .
state.state_changed = true;
UpdateXCapabilities(state);
XELOGI("SDL OnControllerDeviceAdded: Added at index {}.", user_id);
XELOGI("SDL Controller {}: {}", user_id,
SDL_GameControllerMapping(controller));
} else {
// No more controllers needed, close it.
SDL_GameControllerClose(controller);
XELOGW("SDL OnControllerDeviceAdded: Ignored. No free slots.");
}
}
void SDLInputDriver::OnControllerDeviceRemoved(const SDL_Event& event) {
// Find the disconnected gamecontroller and close it.
auto idx = GetControllerIndexFromInstanceID(event.cdevice.which);
if (idx) {
SDL_GameControllerClose(controllers_.at(*idx).sdl);
controllers_.at(*idx) = {};
keystroke_states_.at(*idx) = {};
XELOGI("SDL OnControllerDeviceRemoved: Removed at player index {}.", *idx);
} else {
// Can happen in case all slots where full previously.
XELOGW("SDL OnControllerDeviceRemoved: Ignored. Unused device.");
}
}
void SDLInputDriver::OnControllerDeviceAxisMotion(const SDL_Event& event) {
auto idx = GetControllerIndexFromInstanceID(event.caxis.which);
assert(idx);
auto& pad = controllers_.at(*idx).state.gamepad;
switch (event.caxis.axis) {
case SDL_CONTROLLER_AXIS_LEFTX:
pad.thumb_lx = event.caxis.value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
pad.thumb_ly = ~event.caxis.value;
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
pad.thumb_rx = event.caxis.value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
pad.thumb_ry = ~event.caxis.value;
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
pad.left_trigger = static_cast<uint8_t>(event.caxis.value >> 7);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
pad.right_trigger = static_cast<uint8_t>(event.caxis.value >> 7);
break;
default:
assert_always();
break;
}
controllers_.at(*idx).state_changed = true;
}
void SDLInputDriver::OnControllerDeviceButtonChanged(const SDL_Event& event) {
// Define a lookup table to map between SDL and XInput button codes.
// These need to be in the order of the SDL_GameControllerButton enum.
static constexpr std::array<
std::underlying_type<X_INPUT_GAMEPAD_BUTTON>::type, 21>
xbutton_lookup = {
// Standard buttons:
X_INPUT_GAMEPAD_A,
X_INPUT_GAMEPAD_B,
X_INPUT_GAMEPAD_X,
X_INPUT_GAMEPAD_Y,
X_INPUT_GAMEPAD_BACK,
X_INPUT_GAMEPAD_GUIDE,
X_INPUT_GAMEPAD_START,
X_INPUT_GAMEPAD_LEFT_THUMB,
X_INPUT_GAMEPAD_RIGHT_THUMB,
X_INPUT_GAMEPAD_LEFT_SHOULDER,
X_INPUT_GAMEPAD_RIGHT_SHOULDER,
X_INPUT_GAMEPAD_DPAD_UP,
X_INPUT_GAMEPAD_DPAD_DOWN,
X_INPUT_GAMEPAD_DPAD_LEFT,
X_INPUT_GAMEPAD_DPAD_RIGHT,
// There are additional buttons only available on some controllers.
// For now just assign sensible defaults
// Misc:
X_INPUT_GAMEPAD_GUIDE,
// Xbox Elite paddles:
X_INPUT_GAMEPAD_Y,
X_INPUT_GAMEPAD_B,
X_INPUT_GAMEPAD_X,
X_INPUT_GAMEPAD_A,
// PS touchpad button
X_INPUT_GAMEPAD_GUIDE,
};
static_assert(SDL_CONTROLLER_BUTTON_A == 0);
static_assert(SDL_CONTROLLER_BUTTON_DPAD_RIGHT == 14);
auto idx = GetControllerIndexFromInstanceID(event.cbutton.which);
assert(idx);
auto& controller = controllers_.at(*idx);
uint16_t xbuttons = controller.state.gamepad.buttons;
// Lookup the XInput button code.
if (event.cbutton.button >= xbutton_lookup.size()) {
// A newer SDL Version may have added new buttons.
XELOGI("SDL HID: Unknown button was pressed: {}.", event.cbutton.button);
return;
}
auto xbutton = xbutton_lookup.at(event.cbutton.button);
// Pressed or released?
if (event.cbutton.state == SDL_PRESSED) {
if (xbutton == X_INPUT_GAMEPAD_GUIDE && !cvars::guide_button) {
return;
}
xbuttons |= xbutton;
} else {
xbuttons &= ~xbutton;
}
controller.state.gamepad.buttons = xbuttons;
controller.state_changed = true;
}
std::optional<size_t> SDLInputDriver::GetControllerIndexFromInstanceID(
SDL_JoystickID instance_id) {
// Loop through our controllers and try to match the given ID.
for (size_t i = 0; i < controllers_.size(); i++) {
auto controller = controllers_.at(i).sdl;
if (!controller) {
continue;
}
auto joystick = SDL_GameControllerGetJoystick(controller);
assert(joystick);
auto joy_instance_id = SDL_JoystickInstanceID(joystick);
assert(joy_instance_id >= 0);
if (joy_instance_id == instance_id) {
return i;
}
}
return std::nullopt;
}
SDLInputDriver::ControllerState* SDLInputDriver::GetControllerState(
uint32_t user_index) {
if (user_index >= controllers_.size()) {
return nullptr;
}
auto controller = &controllers_.at(user_index);
if (!controller->sdl) {
return nullptr;
}
return controller;
}
bool SDLInputDriver::TestSDLVersion() const {
#if SDL_VERSION_ATLEAST(2, 0, 9)
// SDL 2.0.9 or newer is required for simple rumble support and player
// index.
const Uint8 min_patchlevel = 9;
#else
// SDL 2.0.4 or newer is required to read game controller mappings from
// file.
const Uint8 min_patchlevel = 4;
#endif
SDL_version ver = {};
SDL_GetVersion(&ver);
if ((ver.major < 2) ||
(ver.major == 2 && ver.minor == 0 && ver.patch < min_patchlevel)) {
return false;
}
return true;
}
void SDLInputDriver::UpdateXCapabilities(ControllerState& state) {
assert(state.sdl);
uint16_t cap_flags = 0x0;
// The RAWINPUT driver combines and enhances input from different APIs. For
// details, see `SDL_rawinputjoystick.c`. This correlation however has latency
// which might confuse games calling `GetCapabilities()` (The power level is
// only available after the controller has been "touched"). Generally that
// should not be a problem, when in doubt disable the RAWINPUT driver via hint
// (env var).
// Guess if we are wireless
auto power_level =
SDL_JoystickCurrentPowerLevel(SDL_GameControllerGetJoystick(state.sdl));
if (power_level >= SDL_JOYSTICK_POWER_EMPTY &&
power_level <= SDL_JOYSTICK_POWER_FULL) {
cap_flags |= X_INPUT_CAPS_WIRELESS;
}
// Check if all navigational buttons are present
static constexpr std::array<SDL_GameControllerButton, 6> nav_buttons = {
SDL_CONTROLLER_BUTTON_START, SDL_CONTROLLER_BUTTON_BACK,
SDL_CONTROLLER_BUTTON_DPAD_UP, SDL_CONTROLLER_BUTTON_DPAD_DOWN,
SDL_CONTROLLER_BUTTON_DPAD_LEFT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
};
for (auto it = nav_buttons.begin(); it < nav_buttons.end(); it++) {
auto bind = SDL_GameControllerGetBindForButton(state.sdl, *it);
if (bind.bindType == SDL_CONTROLLER_BINDTYPE_NONE) {
cap_flags |= X_INPUT_CAPS_NO_NAVIGATION;
break;
}
}
auto& c = state.caps;
c.type = 0x01; // XINPUT_DEVTYPE_GAMEPAD
c.sub_type = 0x01; // XINPUT_DEVSUBTYPE_GAMEPAD
c.flags = cap_flags;
c.gamepad.buttons =
0xF3FF | (cvars::guide_button ? X_INPUT_GAMEPAD_GUIDE : 0x0);
c.gamepad.left_trigger = 0xFF;
c.gamepad.right_trigger = 0xFF;
c.gamepad.thumb_lx = static_cast<int16_t>(0xFFFFu);
c.gamepad.thumb_ly = static_cast<int16_t>(0xFFFFu);
c.gamepad.thumb_rx = static_cast<int16_t>(0xFFFFu);
c.gamepad.thumb_ry = static_cast<int16_t>(0xFFFFu);
c.vibration.left_motor_speed = 0xFFFFu;
c.vibration.right_motor_speed = 0xFFFFu;
}
void SDLInputDriver::QueueControllerUpdate() {
// To minimize consecutive event pumps do not queue before previous pump is
// finished.
bool is_queued = false;
sdl_pumpevents_queued_.compare_exchange_strong(is_queued, true);
if (!is_queued) {
window()->app_context().CallInUIThread([this]() {
SDL_PumpEvents();
sdl_pumpevents_queued_ = false;
});
}
}
// Check if the analog inputs exceed their thresholds to become a button press
// and build the bitfield.
inline uint64_t SDLInputDriver::AnalogToKeyfield(
const X_INPUT_GAMEPAD& gamepad) const {
uint64_t f = 0;
f |= static_cast<uint64_t>(gamepad.left_trigger > HID_SDL_TRIGG_THRES) << 16;
f |= static_cast<uint64_t>(gamepad.right_trigger > HID_SDL_TRIGG_THRES) << 17;
auto thumb_x = gamepad.thumb_lx;
auto thumb_y = gamepad.thumb_ly;
for (size_t i = 0; i <= 8; i = i + 8) {
uint64_t u = thumb_y > HID_SDL_THUMB_THRES;
uint64_t d = thumb_y < ~HID_SDL_THUMB_THRES;
uint64_t r = thumb_x > HID_SDL_THUMB_THRES;
uint64_t l = thumb_x < ~HID_SDL_THUMB_THRES;
if (u && l) {
u = l = 0;
f |= uint64_t(1) << (22 + i);
}
if (u && r) {
u = r = 0;
f |= uint64_t(1) << (23 + i);
}
if (d && r) {
d = r = 0;
f |= uint64_t(1) << (24 + i);
}
if (d && l) {
d = l = 0;
f |= uint64_t(1) << (25 + i);
}
f |= u << (18 + i);
f |= d << (19 + i);
f |= r << (20 + i);
f |= l << (21 + i);
thumb_x = gamepad.thumb_rx;
thumb_y = gamepad.thumb_ry;
}
return f;
}
} // namespace sdl
} // namespace hid
} // namespace xe