/** ****************************************************************************** * 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(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 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(0xFFFFu); out_caps->gamepad.thumb_ly = static_cast(0xFFFFu); out_caps->gamepad.thumb_rx = static_cast(0xFFFFu); out_caps->gamepad.thumb_ry = static_cast(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 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 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 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(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 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 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(event->caxis.value >> 7); break; case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: pad->right_trigger = static_cast(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 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 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 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