Files
Xenia-Canary/src/xenia/hid/sdl/sdl_input_driver.cc
Sandy Carter c8e64da4eb filesystem: use std for PathExists
Remove custom platform implementation of `PathExists` and replace uses
with `std::filesystem::exists`.
2020-04-09 09:44:48 -05:00

427 lines
14 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"
#if XE_PLATFORM_WIN32
#include "xenia/base/platform_win.h"
#endif // XE_PLATFORM_WIN32
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/ui/window.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)
: InputDriver(window),
sdl_events_initialized_(false),
sdl_gamecontroller_initialized_(false),
sdl_events_unflushed_(0),
sdl_pumpevents_queued_(false),
controllers_(),
controllers_mutex_() {}
SDLInputDriver::~SDLInputDriver() {
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() {
XELOG_SDL_INIT()
if (!TestSDLVersion()) {
return X_STATUS_UNSUCCESSFUL;
}
// SDL_PumpEvents should only be run in the thread that initialized SDL - we
// are hijacking the window loop thread for that.
window_->loop()->PostSynchronous([&]() {
// 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;
SDL_EventFilter event_filter{[](void* userdata, SDL_Event* event) -> int {
if (!userdata) {
assert_always();
return 0;
}
// Event queue should never be (this) full
assert(SDL_PeepEvents(nullptr, 0, SDL_PEEKEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT) < 0xFFFF);
const auto type = event->type;
if (type < SDL_JOYAXISMOTION || type > SDL_CONTROLLERDEVICEREMAPPED) {
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);
// The queue could grow up to 3.5MB since it is never polled.
if (++driver->sdl_events_unflushed_ > 64) {
SDL_FlushEvents(SDL_JOYAXISMOTION, SDL_CONTROLLERDEVICEREMAPPED);
driver->sdl_events_unflushed_ = 0;
}
switch (type) {
case SDL_CONTROLLERDEVICEADDED:
driver->OnControllerDeviceAdded(event);
break;
case SDL_CONTROLLERDEVICEREMOVED:
driver->OnControllerDeviceRemoved(event);
break;
case SDL_CONTROLLERAXISMOTION:
driver->OnControllerDeviceAxisMotion(event);
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
driver->OnControllerDeviceButtonChanged(event);
break;
default:
break;
}
return 0;
}};
// 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(event_filter, this);
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
return;
}
sdl_gamecontroller_initialized_ = true;
if (!cvars::mappings_file.empty()) {
if (!std::filesystem::exists(cvars::mappings_file)) {
XELOGW("SDL GameControllerDB: file '{}' does not exist.",
xe::path_to_utf8(cvars::mappings_file));
} else {
auto mappings_file = filesystem::OpenFile(cvars::mappings_file, "rb");
if (!mappings_file) {
XELOGE("SDL GameControllerDB: failed to open file '{}'.",
xe::path_to_utf8(cvars::mappings_file));
} else {
auto mappings_result = SDL_GameControllerAddMappingsFromRW(
SDL_RWFromFP(mappings_file, SDL_TRUE), 1);
if (mappings_result < 0) {
XELOGE("SDL GameControllerDB: error loading file '{}': {}.",
xe::path_to_utf8(cvars::mappings_file), mappings_result);
} else {
XELOGI("SDL GameControllerDB: loaded {} mappings.",
mappings_result);
}
}
}
}
});
return sdl_events_initialized_ && sdl_gamecontroller_initialized_;
}
X_RESULT SDLInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags,
X_INPUT_CAPABILITIES* out_caps) {
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
QueueControllerUpdate();
std::unique_lock<std::mutex> guard(controllers_mutex_);
auto controller = GetControllerState(user_index);
if (!controller) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
out_caps->type = 0x01; // XINPUT_DEVTYPE_GAMEPAD
out_caps->sub_type = 0x01; // XINPUT_DEVSUBTYPE_GAMEPAD
out_caps->flags = 0;
out_caps->gamepad.buttons = 0xF3FF;
out_caps->gamepad.left_trigger = 0xFF;
out_caps->gamepad.right_trigger = 0xFF;
out_caps->gamepad.thumb_lx = static_cast<int16_t>(0xFFFFu);
out_caps->gamepad.thumb_ly = static_cast<int16_t>(0xFFFFu);
out_caps->gamepad.thumb_rx = static_cast<int16_t>(0xFFFFu);
out_caps->gamepad.thumb_ry = static_cast<int16_t>(0xFFFFu);
out_caps->vibration.left_motor_speed = 0xFFFFu;
out_caps->vibration.right_motor_speed = 0xFFFFu;
return X_ERROR_SUCCESS;
}
X_RESULT SDLInputDriver::GetState(uint32_t user_index,
X_INPUT_STATE* out_state) {
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
QueueControllerUpdate();
std::unique_lock<std::mutex> guard(controllers_mutex_);
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.
if (controller->state_changed) {
controller->state.packet_number++;
controller->state_changed = false;
}
*out_state = controller->state;
return X_ERROR_SUCCESS;
}
X_RESULT SDLInputDriver::SetState(uint32_t user_index,
X_INPUT_VIBRATION* vibration) {
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
QueueControllerUpdate();
std::unique_lock<std::mutex> guard(controllers_mutex_);
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 user_index, uint32_t flags,
X_INPUT_KEYSTROKE* out_keystroke) {
// TODO(joellinn) translate keyboard events for chatpad emulation.
return X_ERROR_EMPTY;
}
void SDLInputDriver::OnControllerDeviceAdded(SDL_Event* event) {
assert(window_->loop()->is_on_loop_thread());
std::unique_lock<std::mutex> guard(controllers_mutex_);
// Open the controller.
const auto controller = SDL_GameControllerOpen(event->cdevice.which);
if (!controller) {
assert_always();
return;
}
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);
break;
}
}
}
if (user_id >= 0) {
controllers_.at(user_id) = {controller, {}};
// XInput seems to start with packet_number = 1 .
controllers_.at(user_id).state_changed = true;
} else {
// No more controllers needed, close it.
SDL_GameControllerClose(controller);
}
}
void SDLInputDriver::OnControllerDeviceRemoved(SDL_Event* event) {
assert(window_->loop()->is_on_loop_thread());
std::unique_lock<std::mutex> guard(controllers_mutex_);
// Find the disconnected gamecontroller and close it.
bool found;
size_t i;
std::tie(found, i) = GetControllerIndexFromInstanceID(event->cdevice.which);
assert(found);
SDL_GameControllerClose(controllers_.at(i).sdl);
controllers_.at(i) = {};
}
void SDLInputDriver::OnControllerDeviceAxisMotion(SDL_Event* event) {
assert(window_->loop()->is_on_loop_thread());
std::unique_lock<std::mutex> guard(controllers_mutex_);
bool found;
size_t i;
std::tie(found, i) = GetControllerIndexFromInstanceID(event->caxis.which);
assert(found);
const auto pad = &controllers_.at(i).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(i).state_changed = true;
}
void SDLInputDriver::OnControllerDeviceButtonChanged(SDL_Event* event) {
assert(window_->loop()->is_on_loop_thread());
std::unique_lock<std::mutex> guard(controllers_mutex_);
// 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<uint16_t, SDL_CONTROLLER_BUTTON_MAX>
xbutton_lookup = {X_INPUT_GAMEPAD_A,
X_INPUT_GAMEPAD_B,
X_INPUT_GAMEPAD_X,
X_INPUT_GAMEPAD_Y,
X_INPUT_GAMEPAD_BACK,
0 /* Guide button */,
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};
bool found;
size_t i;
std::tie(found, i) = GetControllerIndexFromInstanceID(event->cbutton.which);
assert(found);
const auto controller = &controllers_.at(i);
uint16_t xbuttons = controller->state.gamepad.buttons;
// Lookup the XInput button code.
auto xbutton = xbutton_lookup.at(event->cbutton.button);
// Pressed or released?
if (event->cbutton.state == SDL_PRESSED) {
xbuttons |= xbutton;
} else {
xbuttons &= ~xbutton;
}
controller->state.gamepad.buttons = xbuttons;
controller->state_changed = true;
}
std::pair<bool, 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 {true, i};
}
}
return {false, 0};
}
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() {
#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::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_->loop()->Post([this]() {
SDL_PumpEvents();
sdl_pumpevents_queued_ = false;
});
}
}
} // namespace sdl
} // namespace hid
} // namespace xe