diff --git a/.gitmodules b/.gitmodules index 9b7dd46b8..507ae7a99 100644 --- a/.gitmodules +++ b/.gitmodules @@ -52,6 +52,9 @@ [submodule "libyuv"] path = externals/libyuv url = https://github.com/lemenkov/libyuv.git +[submodule "sdl2"] + path = externals/sdl2/SDL + url = https://github.com/libsdl-org/SDL [submodule "cryptopp-cmake"] path = externals/cryptopp-cmake url = https://github.com/abdes/cryptopp-cmake.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 099cf226e..5dbf63c5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,9 @@ else() set(DEFAULT_ENABLE_LTO OFF) endif() +option(ENABLE_SDL2 "Enable using SDL2" ON) +option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF) + # Set bundled qt as dependent options. option(ENABLE_QT "Enable the Qt frontend" ON) option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF) @@ -393,6 +396,15 @@ if (NOT USE_SYSTEM_BOOST) add_library(Boost::iostreams ALIAS boost_iostreams) endif() +# SDL2 +if (ENABLE_SDL2 AND USE_SYSTEM_SDL2) + find_package(SDL2 REQUIRED) + add_library(SDL2 INTERFACE) + target_link_libraries(SDL2 INTERFACE "${SDL2_LIBRARY}") + target_include_directories(SDL2 INTERFACE "${SDL2_INCLUDE_DIR}") + add_library(SDL2::SDL2 ALIAS SDL2) +endif() + if (ENABLE_LIBUSB AND USE_SYSTEM_LIBUSB) include(FindPkgConfig) find_package(LibUSB) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 80201b960..eb7279724 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -183,6 +183,14 @@ endif() # Teakra add_subdirectory(teakra EXCLUDE_FROM_ALL) +# SDL2 +if (ENABLE_SDL2 AND NOT USE_SYSTEM_SDL2) + if (MSVC) + set (SDL_LIBC ON) + endif() + add_subdirectory(sdl2) +endif() + # libusb if (ENABLE_LIBUSB AND NOT USE_SYSTEM_LIBUSB) add_subdirectory(libusb) diff --git a/externals/sdl2/CMakeLists.txt b/externals/sdl2/CMakeLists.txt new file mode 100644 index 000000000..7bdea6343 --- /dev/null +++ b/externals/sdl2/CMakeLists.txt @@ -0,0 +1,25 @@ +# Configure static library build +set(SDL_SHARED OFF CACHE BOOL "") +set(SDL_STATIC ON CACHE BOOL "") + +# Subsystems +set(SDL_ATOMIC ON CACHE BOOL "") +set(SDL_AUDIO ON CACHE BOOL "") +set(SDL_VIDEO ON CACHE BOOL "") +set(SDL_RENDER OFF CACHE BOOL "") +set(SDL_EVENTS ON CACHE BOOL "") +set(SDL_JOYSTICK ON CACHE BOOL "") +set(SDL_HAPTIC OFF CACHE BOOL "") +set(SDL_HIDAPI ON CACHE BOOL "") +set(SDL_POWER OFF CACHE BOOL "") +set(SDL_THREADS ON CACHE BOOL "") +set(SDL_TIMERS ON CACHE BOOL "") +set(SDL_FILE ON CACHE BOOL "") +set(SDL_LOADSO ON CACHE BOOL "") +set(SDL_CPUINFO ON CACHE BOOL "") +set(SDL_FILESYSTEM OFF CACHE BOOL "") +set(SDL_DLOPEN ON CACHE BOOL "") +set(SDL_SENSOR OFF CACHE BOOL "") +set(SDL_LOCALE OFF CACHE BOOL "") + +add_subdirectory(SDL) \ No newline at end of file diff --git a/externals/sdl2/SDL b/externals/sdl2/SDL new file mode 160000 index 000000000..ba2f78a00 --- /dev/null +++ b/externals/sdl2/SDL @@ -0,0 +1 @@ +Subproject commit ba2f78a0069118a6c583f1fbf1420144ffa35bad diff --git a/src/android/app/src/main/jni/input_manager.cpp b/src/android/app/src/main/jni/input_manager.cpp index 4c2656b85..6585ff626 100644 --- a/src/android/app/src/main/jni/input_manager.cpp +++ b/src/android/app/src/main/jni/input_manager.cpp @@ -13,6 +13,7 @@ #include "common/math_util.h" #include "common/param_package.h" #include "input_common/main.h" +#include "input_common/sdl/sdl.h" #include "jni/input_manager.h" #include "jni/ndk_motion.h" diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 071c58b8c..14a63c076 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(audio_core STATIC time_stretch.cpp time_stretch.h + $<$:sdl2_sink.cpp sdl2_sink.h> $<$:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h> $<$:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h> ) @@ -45,6 +46,11 @@ create_target_directory_groups(audio_core) target_link_libraries(audio_core PUBLIC lime_common lime_core) target_link_libraries(audio_core PRIVATE faad2 SoundTouch teakra) +if(ENABLE_SDL2) + target_link_libraries(audio_core PRIVATE SDL2::SDL2) + target_compile_definitions(audio_core PRIVATE HAVE_SDL2) +endif() + if(ENABLE_CUBEB) target_link_libraries(audio_core PRIVATE cubeb) target_compile_definitions(audio_core PUBLIC HAVE_CUBEB) diff --git a/src/audio_core/sdl2_sink.cpp b/src/audio_core/sdl2_sink.cpp new file mode 100644 index 000000000..2aa90c48a --- /dev/null +++ b/src/audio_core/sdl2_sink.cpp @@ -0,0 +1,108 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "audio_core/audio_types.h" +#include "audio_core/sdl2_sink.h" +#include "common/assert.h" +#include "common/logging/log.h" + +namespace AudioCore { + +struct SDL2Sink::Impl { + unsigned int sample_rate = 0; + + SDL_AudioDeviceID audio_device_id = 0; + + std::function cb; + + static void Callback(void* impl_, u8* buffer, int buffer_size_in_bytes); +}; + +SDL2Sink::SDL2Sink(std::string device_name) : impl(std::make_unique()) { + if (SDL_Init(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_Init(SDL_INIT_AUDIO) failed with: {}", SDL_GetError()); + impl->audio_device_id = 0; + return; + } + + SDL_AudioSpec desired_audiospec; + SDL_zero(desired_audiospec); + desired_audiospec.format = AUDIO_S16; + desired_audiospec.channels = 2; + desired_audiospec.freq = native_sample_rate; + desired_audiospec.samples = 512; + desired_audiospec.userdata = impl.get(); + desired_audiospec.callback = &Impl::Callback; + + SDL_AudioSpec obtained_audiospec; + SDL_zero(obtained_audiospec); + + const char* device = nullptr; + if (device_name != auto_device_name && !device_name.empty()) { + device = device_name.c_str(); + } + + impl->audio_device_id = + SDL_OpenAudioDevice(device, false, &desired_audiospec, &obtained_audiospec, 0); + if (impl->audio_device_id <= 0) { + LOG_CRITICAL(Audio_Sink, "SDL_OpenAudioDevice failed with code {} for device \"{}\"", + impl->audio_device_id, device_name); + return; + } + + impl->sample_rate = obtained_audiospec.freq; + + // SDL2 audio devices start out paused, unpause it: + SDL_PauseAudioDevice(impl->audio_device_id, 0); +} + +SDL2Sink::~SDL2Sink() { + if (impl->audio_device_id <= 0) + return; + + SDL_CloseAudioDevice(impl->audio_device_id); +} + +unsigned int SDL2Sink::GetNativeSampleRate() const { + if (impl->audio_device_id <= 0) + return native_sample_rate; + + return impl->sample_rate; +} + +void SDL2Sink::SetCallback(std::function cb) { + impl->cb = cb; +} + +void SDL2Sink::Impl::Callback(void* impl_, u8* buffer, int buffer_size_in_bytes) { + Impl* impl = reinterpret_cast(impl_); + if (!impl || !impl->cb) + return; + + const std::size_t num_frames = buffer_size_in_bytes / (2 * sizeof(s16)); + + impl->cb(reinterpret_cast(buffer), num_frames); +} + +std::vector ListSDL2SinkDevices() { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem failed with: {}", SDL_GetError()); + return {}; + } + + std::vector device_list; + const int device_count = SDL_GetNumAudioDevices(0); + for (int i = 0; i < device_count; ++i) { + device_list.push_back(SDL_GetAudioDeviceName(i, 0)); + } + + SDL_QuitSubSystem(SDL_INIT_AUDIO); + + return device_list; +} + +} // namespace AudioCore diff --git a/src/audio_core/sdl2_sink.h b/src/audio_core/sdl2_sink.h new file mode 100644 index 000000000..6e262a0b6 --- /dev/null +++ b/src/audio_core/sdl2_sink.h @@ -0,0 +1,29 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "audio_core/sink.h" + +namespace AudioCore { + +class SDL2Sink final : public Sink { +public: + explicit SDL2Sink(std::string device_id); + ~SDL2Sink() override; + + unsigned int GetNativeSampleRate() const override; + + void SetCallback(std::function cb) override; + +private: + struct Impl; + std::unique_ptr impl; +}; + +std::vector ListSDL2SinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index 0eaa8398b..961e040b3 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -8,6 +8,9 @@ #include #include "audio_core/null_sink.h" #include "audio_core/sink_details.h" +#ifdef HAVE_SDL2 +#include "audio_core/sdl2_sink.h" +#endif #ifdef HAVE_CUBEB #include "audio_core/cubeb_sink.h" #endif @@ -33,6 +36,13 @@ constexpr std::array sink_details = { return std::make_unique(std::string(device_id)); }, &ListOpenALSinkDevices}, +#endif +#ifdef HAVE_SDL2 + SinkDetails{SinkType::SDL2, "SDL2", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(std::string(device_id)); + }, + &ListSDL2SinkDevices}, #endif SinkDetails{SinkType::Null, "None", [](std::string_view device_id) -> std::unique_ptr { diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index d470e1746..e0aa3b1c3 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -10,6 +10,8 @@ add_library(input_common STATIC precompiled_headers.h touch_from_button.cpp touch_from_button.h + sdl/sdl.cpp + sdl/sdl.h udp/client.cpp udp/client.h udp/protocol.cpp @@ -18,6 +20,15 @@ add_library(input_common STATIC udp/udp.h ) +if(ENABLE_SDL2) + target_sources(input_common PRIVATE + sdl/sdl_impl.cpp + sdl/sdl_impl.h + ) + target_link_libraries(input_common PRIVATE SDL2::SDL2) + target_compile_definitions(input_common PRIVATE HAVE_SDL2) +endif() + if(ENABLE_LIBUSB) target_sources(input_common PRIVATE gcadapter/gc_adapter.cpp diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index f6c64c208..bcff11d20 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -13,6 +13,8 @@ #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/motion_emu.h" +#include "input_common/sdl/sdl.h" +#include "input_common/sdl/sdl_impl.h" #include "input_common/touch_from_button.h" #include "input_common/udp/udp.h" @@ -26,6 +28,7 @@ std::shared_ptr gcadapter; static std::shared_ptr keyboard; static std::shared_ptr motion_emu; static std::unique_ptr udp; +static std::unique_ptr sdl; void Init() { #ifdef ENABLE_GCADAPTER @@ -44,6 +47,8 @@ void Init() { Input::RegisterFactory("touch_from_button", std::make_shared()); + sdl = SDL::Init(); + udp = CemuhookUDP::Init(); } @@ -61,6 +66,7 @@ void Shutdown() { motion_emu.reset(); Input::UnregisterFactory("emu_window"); Input::UnregisterFactory("touch_from_button"); + sdl.reset(); udp.reset(); } @@ -97,6 +103,10 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, Common::ParamPackage GetControllerButtonBinds(const Common::ParamPackage& params, int button) { const auto native_button{static_cast(button)}; const auto engine{params.Get("engine", "")}; + if (engine == "sdl") { + return dynamic_cast(sdl.get())->GetSDLControllerButtonBindByGUID( + params.Get("guid", "0"), params.Get("port", 0), native_button); + } #ifdef ENABLE_GCADAPTER if (engine == "gcpad") { return gcbuttons->GetGcTo3DSMappedButton(params.Get("port", 0), native_button); @@ -108,6 +118,10 @@ Common::ParamPackage GetControllerButtonBinds(const Common::ParamPackage& params Common::ParamPackage GetControllerAnalogBinds(const Common::ParamPackage& params, int analog) { const auto native_analog{static_cast(analog)}; const auto engine{params.Get("engine", "")}; + if (engine == "sdl") { + return dynamic_cast(sdl.get())->GetSDLControllerAnalogBindByGUID( + params.Get("guid", "0"), params.Get("port", 0), native_analog); + } #ifdef ENABLE_GCADAPTER if (engine == "gcpad") { return gcanalog->GetGcTo3DSMappedAnalog(params.Get("port", 0), native_analog); @@ -128,6 +142,9 @@ namespace Polling { std::vector> GetPollers(DeviceType type) { std::vector> pollers; +#ifdef HAVE_SDL2 + pollers = sdl->GetPollers(type); +#endif #ifdef ENABLE_GCADAPTER switch (type) { case DeviceType::Analog: diff --git a/src/input_common/sdl/sdl.cpp b/src/input_common/sdl/sdl.cpp new file mode 100644 index 000000000..644db3448 --- /dev/null +++ b/src/input_common/sdl/sdl.cpp @@ -0,0 +1,19 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "input_common/sdl/sdl.h" +#ifdef HAVE_SDL2 +#include "input_common/sdl/sdl_impl.h" +#endif + +namespace InputCommon::SDL { + +std::unique_ptr Init() { +#ifdef HAVE_SDL2 + return std::make_unique(); +#else + return std::make_unique(); +#endif +} +} // namespace InputCommon::SDL diff --git a/src/input_common/sdl/sdl.h b/src/input_common/sdl/sdl.h new file mode 100644 index 000000000..d7f24c68a --- /dev/null +++ b/src/input_common/sdl/sdl.h @@ -0,0 +1,44 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "core/frontend/input.h" +#include "input_common/main.h" + +union SDL_Event; + +namespace Common { +class ParamPackage; +} // namespace Common + +namespace InputCommon::Polling { +class DevicePoller; +enum class DeviceType; +} // namespace InputCommon::Polling + +namespace InputCommon::SDL { + +class State { +public: + using Pollers = std::vector>; + + /// Unregisters SDL device factories and shut them down. + virtual ~State() = default; + + virtual Pollers GetPollers(Polling::DeviceType type) = 0; +}; + +class NullState : public State { +public: + Pollers GetPollers(Polling::DeviceType type) override { + return {}; + } +}; + +std::unique_ptr Init(); + +} // namespace InputCommon::SDL diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp new file mode 100644 index 000000000..1833e49cb --- /dev/null +++ b/src/input_common/sdl/sdl_impl.cpp @@ -0,0 +1,1092 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/math_util.h" +#include "common/param_package.h" +#include "common/threadsafe_queue.h" +#include "core/frontend/input.h" +#include "input_common/sdl/sdl_impl.h" + +// These structures are not actually defined in the headers, so we need to define them here to use +// them. +typedef struct { + SDL_GameControllerBindType inputType; + union { + int button; + + struct { + int axis; + int axis_min; + int axis_max; + } axis; + + struct { + int hat; + int hat_mask; + } hat; + + } input; + + SDL_GameControllerBindType outputType; + union { + SDL_GameControllerButton button; + + struct { + SDL_GameControllerAxis axis; + int axis_min; + int axis_max; + } axis; + + } output; + +} SDL_ExtendedGameControllerBind; + +#if SDL_VERSION_ATLEAST(2, 26, 0) +/* our hard coded list of mapping support */ +typedef enum { + SDL_CONTROLLER_MAPPING_PRIORITY_DEFAULT, + SDL_CONTROLLER_MAPPING_PRIORITY_API, + SDL_CONTROLLER_MAPPING_PRIORITY_USER, +} SDL_ControllerMappingPriority; + +typedef struct _ControllerMapping_t { + SDL_JoystickGUID guid; + char* name; + char* mapping; + SDL_ControllerMappingPriority priority; + struct _ControllerMapping_t* next; +} ControllerMapping_t; +#endif + +struct _SDL_GameController { +#if SDL_VERSION_ATLEAST(2, 26, 0) + const void* magic; +#endif + + SDL_Joystick* joystick; /* underlying joystick device */ + int ref_count; + + const char* name; +#if SDL_VERSION_ATLEAST(2, 26, 0) + ControllerMapping_t* mapping; +#endif + int num_bindings; + SDL_ExtendedGameControllerBind* bindings; + SDL_ExtendedGameControllerBind** last_match_axis; + Uint8* last_hat_mask; + Uint32 guide_button_down; + + struct _SDL_GameController* next; /* pointer to next game controller we have allocated */ +}; + +namespace InputCommon { + +namespace SDL { + +static std::string GetGUID(SDL_Joystick* joystick) { + SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); + char guid_str[33]; + SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); + return guid_str; +} + +/// Creates a ParamPackage from an SDL_Event that can directly be used to create a ButtonDevice +static Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event); + +static int SDLEventWatcher(void* userdata, SDL_Event* event) { + SDLState* sdl_state = reinterpret_cast(userdata); + // Don't handle the event if we are configuring + if (sdl_state->polling) { + sdl_state->event_queue.Push(*event); + } else { + sdl_state->HandleGameControllerEvent(*event); + } + return 0; +} + +constexpr std::array + xinput_to_3ds_mapping = {{ + SDL_CONTROLLER_BUTTON_B, + SDL_CONTROLLER_BUTTON_A, + SDL_CONTROLLER_BUTTON_Y, + SDL_CONTROLLER_BUTTON_X, + SDL_CONTROLLER_BUTTON_DPAD_UP, + SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SDL_CONTROLLER_BUTTON_DPAD_RIGHT, + SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SDL_CONTROLLER_BUTTON_START, + SDL_CONTROLLER_BUTTON_BACK, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_GUIDE, + SDL_CONTROLLER_BUTTON_INVALID, + }}; + +struct SDLJoystickDeleter { + void operator()(SDL_Joystick* object) { + SDL_JoystickClose(object); + } +}; +class SDLJoystick { +public: + SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick, + SDL_GameController* game_controller) + : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose}, + sdl_controller{game_controller, &SDL_GameControllerClose} { + EnableMotion(); + } + + void EnableMotion() { + if (!sdl_controller) { + return; + } +#if SDL_VERSION_ATLEAST(2, 0, 14) + SDL_GameController* controller = sdl_controller.get(); + + if (HasMotion()) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_FALSE); + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_FALSE); + } + has_accel = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; + has_gyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + if (has_accel) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } + if (has_gyro) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); + } +#endif + } + + bool HasMotion() const { + return has_gyro || has_accel; + } + + void SetButton(int button, bool value) { + std::lock_guard lock{mutex}; + state.buttons[button] = value; + } + + bool GetButton(int button) const { + std::lock_guard lock{mutex}; + return state.buttons.at(button); + } + + void SetAxis(int axis, Sint16 value) { + std::lock_guard lock{mutex}; + state.axes[axis] = value; + } + + float GetAxis(int axis) const { + std::lock_guard lock{mutex}; + return state.axes.at(axis) / 32767.0f; + } + + std::tuple GetAnalog(int axis_x, int axis_y) const { + float x = GetAxis(axis_x); + float y = GetAxis(axis_y); + y = -y; // 3DS uses an y-axis inverse from SDL + + // Make sure the coordinates are in the unit circle, + // otherwise normalize it. + float r = x * x + y * y; + if (r > 1.0f) { + r = std::sqrt(r); + x /= r; + y /= r; + } + + return std::make_tuple(x, y); + } + + void SetHat(int hat, Uint8 direction) { + std::lock_guard lock{mutex}; + state.hats[hat] = direction; + } + + bool GetHatDirection(int hat, Uint8 direction) const { + std::lock_guard lock{mutex}; + return (state.hats.at(hat) & direction) != 0; + } + + void SetAccel(const float x, const float y, const float z) { + std::lock_guard lock{mutex}; + state.accel.x = x; + state.accel.y = y; + state.accel.z = z; + } + void SetGyro(const float pitch, const float yaw, const float roll) { + std::lock_guard lock{mutex}; + state.gyro.x = pitch; + state.gyro.y = yaw; + state.gyro.z = roll; + } + std::tuple, Common::Vec3> GetMotion() const { + std::lock_guard lock{mutex}; + return std::make_tuple(state.accel, state.gyro); + } + + /** + * The guid of the joystick + */ + const std::string& GetGUID() const { + return guid; + } + + /** + * The number of joystick from the same type that were connected before this joystick + */ + int GetPort() const { + return port; + } + + SDL_Joystick* GetSDLJoystick() const { + return sdl_joystick.get(); + } + + SDL_GameController* GetSDLGameController() const { + return sdl_controller.get(); + } + + void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) { + sdl_joystick.reset(joystick); + sdl_controller.reset(controller); + } + +private: + struct State { + std::unordered_map buttons; + std::unordered_map axes; + std::unordered_map hats; + Common::Vec3 accel; + Common::Vec3 gyro; + } state; + std::string guid; + int port; + bool has_gyro{false}; + bool has_accel{false}; + std::unique_ptr sdl_joystick; + std::unique_ptr sdl_controller; + mutable std::mutex mutex; +}; + +struct SDLGameControllerDeleter { + void operator()(SDL_GameController* object) { + SDL_GameControllerClose(object); + } +}; +class SDLGameController { +public: + SDLGameController(std::string guid_, int port_, SDL_GameController* controller) + : guid{std::move(guid_)}, port{port_}, sdl_controller{controller} {} + + /** + * The guid of the joystick/controller + */ + const std::string& GetGUID() const { + return guid; + } + + /** + * The number of joystick from the same type that were connected before this joystick + */ + int GetPort() const { + return port; + } + + SDL_GameController* GetSDLGameController() const { + return sdl_controller.get(); + } + + void SetSDLGameController(SDL_GameController* controller) { + sdl_controller = std::unique_ptr(controller); + } + +private: + std::string guid; + int port; + std::unique_ptr sdl_controller; +}; + +/** + * Get the nth joystick with the corresponding GUID + */ +std::shared_ptr SDLState::GetSDLJoystickByGUID(const std::string& guid, int port) { + std::lock_guard lock{joystick_map_mutex}; + const auto it = joystick_map.find(guid); + if (it != joystick_map.end()) { + while (it->second.size() <= static_cast(port)) { + auto joystick = std::make_shared(guid, static_cast(it->second.size()), + nullptr, nullptr); + it->second.emplace_back(std::move(joystick)); + } + return it->second[static_cast(port)]; + } + auto joystick = std::make_shared(guid, 0, nullptr, nullptr); + return joystick_map[guid].emplace_back(std::move(joystick)); +} + +/** + * Check how many identical joysticks (by guid) were connected before the one with sdl_id and so tie + * it to a SDLJoystick with the same guid and that port + */ +std::shared_ptr SDLState::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) { + auto sdl_joystick = SDL_JoystickFromInstanceID(sdl_id); + const std::string guid = GetGUID(sdl_joystick); + + std::lock_guard lock{joystick_map_mutex}; + auto map_it = joystick_map.find(guid); + + if (map_it == joystick_map.end()) { + return nullptr; + } + + const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(), + [&sdl_joystick](const auto& joystick) { + return joystick->GetSDLJoystick() == sdl_joystick; + }); + + if (vec_it == map_it->second.end()) { + return nullptr; + } + + return *vec_it; +} + +Common::ParamPackage SDLState::GetSDLControllerButtonBindByGUID( + const std::string& guid, int port, Settings::NativeButton::Values button) { + Common::ParamPackage params({{"engine", "sdl"}}); + params.Set("guid", guid); + params.Set("port", port); + SDL_GameController* controller = GetSDLJoystickByGUID(guid, port)->GetSDLGameController(); + SDL_GameControllerButtonBind button_bind; + + if (!controller) { + LOG_WARNING(Input, "failed to open controller {}", guid); + return {{}}; + } + + auto mapped_button = xinput_to_3ds_mapping[static_cast(button)]; + if (mapped_button == SDL_CONTROLLER_BUTTON_INVALID) { + if (button == Settings::NativeButton::Values::ZL) { + button_bind = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT); + } else if (button == Settings::NativeButton::Values::ZR) { + button_bind = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT); + } else { + return {{}}; + } + } else { + button_bind = SDL_GameControllerGetBindForButton(controller, mapped_button); + } + + switch (button_bind.bindType) { + case SDL_CONTROLLER_BINDTYPE_BUTTON: + params.Set("button", button_bind.value.button); + break; + case SDL_CONTROLLER_BINDTYPE_HAT: + params.Set("hat", button_bind.value.hat.hat); + switch (button_bind.value.hat.hat_mask) { + case SDL_HAT_UP: + params.Set("direction", "up"); + break; + case SDL_HAT_DOWN: + params.Set("direction", "down"); + break; + case SDL_HAT_LEFT: + params.Set("direction", "left"); + break; + case SDL_HAT_RIGHT: + params.Set("direction", "right"); + break; + default: + return {{}}; + } + break; + case SDL_CONTROLLER_BINDTYPE_AXIS: + params.Set("axis", button_bind.value.axis); + +#if SDL_VERSION_ATLEAST(2, 0, 6) + { + if (mapped_button != SDL_CONTROLLER_BUTTON_INVALID) { + const SDL_ExtendedGameControllerBind extended_bind = + controller->bindings[mapped_button]; + if (extended_bind.input.axis.axis_max < extended_bind.input.axis.axis_min) { + params.Set("direction", "-"); + } else { + params.Set("direction", "+"); + } + params.Set("threshold", (extended_bind.input.axis.axis_min + + (extended_bind.input.axis.axis_max - + extended_bind.input.axis.axis_min) / + 2.0f) / + SDL_JOYSTICK_AXIS_MAX); + } + } +#else + params.Set("direction", "+"); // lacks extended_bind, so just a guess +#endif + break; + case SDL_CONTROLLER_BINDTYPE_NONE: + LOG_WARNING(Input, "Button not bound: {}", Settings::NativeButton::mapping[button]); + return {{}}; + default: + LOG_WARNING(Input, "unknown SDL bind type {}", button_bind.bindType); + return {{}}; + } + + return params; +} + +Common::ParamPackage SDLState::GetSDLControllerAnalogBindByGUID( + const std::string& guid, int port, Settings::NativeAnalog::Values analog) { + Common::ParamPackage params({{"engine", "sdl"}}); + params.Set("guid", guid); + params.Set("port", port); + SDL_GameController* controller = GetSDLJoystickByGUID(guid, port)->GetSDLGameController(); + SDL_GameControllerButtonBind button_bind_x; + SDL_GameControllerButtonBind button_bind_y; + + if (!controller) { + LOG_WARNING(Input, "failed to open controller {}", guid); + return {{}}; + } + + if (analog == Settings::NativeAnalog::Values::CirclePad) { + button_bind_x = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); + button_bind_y = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); + } else if (analog == Settings::NativeAnalog::Values::CStick) { + button_bind_x = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX); + button_bind_y = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY); + } else { + LOG_WARNING(Input, "analog value out of range {}", analog); + return {{}}; + } + + if (button_bind_x.bindType != SDL_CONTROLLER_BINDTYPE_AXIS || + button_bind_y.bindType != SDL_CONTROLLER_BINDTYPE_AXIS) { + return {{}}; + } + params.Set("axis_x", button_bind_x.value.axis); + params.Set("axis_y", button_bind_y.value.axis); + return params; +} + +void SDLState::InitJoystick(int joystick_index) { + SDL_Joystick* sdl_joystick = SDL_JoystickOpen(joystick_index); + SDL_GameController* sdl_gamecontroller = nullptr; + + if (SDL_IsGameController(joystick_index)) { + sdl_gamecontroller = SDL_GameControllerOpen(joystick_index); + } + + if (!sdl_joystick) { + LOG_ERROR(Input, "failed to open joystick {}, with error: {}", joystick_index, + SDL_GetError()); + return; + } + const std::string guid = GetGUID(sdl_joystick); + + std::lock_guard lock{joystick_map_mutex}; + if (joystick_map.find(guid) == joystick_map.end()) { + auto joystick = std::make_shared(guid, 0, sdl_joystick, sdl_gamecontroller); + joystick->EnableMotion(); + joystick_map[guid].emplace_back(std::move(joystick)); + return; + } + + auto& joystick_guid_list = joystick_map[guid]; + const auto it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(), + [](const auto& joystick) { return !joystick->GetSDLJoystick(); }); + if (it != joystick_guid_list.end()) { + (*it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller); + (*it)->EnableMotion(); + return; + } + const int port = static_cast(joystick_guid_list.size()); + auto joystick = std::make_shared(guid, port, sdl_joystick, sdl_gamecontroller); + joystick->EnableMotion(); + joystick_guid_list.emplace_back(std::move(joystick)); +} + +void SDLState::CloseJoystick(SDL_Joystick* sdl_joystick) { + const auto guid = GetGUID(sdl_joystick); + + std::scoped_lock lock{joystick_map_mutex}; + // This call to guid is safe since the joystick is guaranteed to be in the map + const auto& joystick_guid_list = joystick_map[guid]; + const auto joystick_it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(), + [&sdl_joystick](const auto& joystick) { + return joystick->GetSDLJoystick() == sdl_joystick; + }); + + if (joystick_it != joystick_guid_list.end()) { + (*joystick_it)->SetSDLJoystick(nullptr, nullptr); + } +} + +void SDLState::HandleGameControllerEvent(const SDL_Event& event) { + switch (event.type) { + case SDL_JOYBUTTONUP: { + if (auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) { + joystick->SetButton(event.jbutton.button, false); + } + break; + } + case SDL_JOYBUTTONDOWN: { + if (auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) { + joystick->SetButton(event.jbutton.button, true); + } + break; + } + case SDL_JOYHATMOTION: { + if (auto joystick = GetSDLJoystickBySDLID(event.jhat.which)) { + joystick->SetHat(event.jhat.hat, event.jhat.value); + } + break; + } + case SDL_JOYAXISMOTION: { + if (auto joystick = GetSDLJoystickBySDLID(event.jaxis.which)) { + joystick->SetAxis(event.jaxis.axis, event.jaxis.value); + } + break; + } +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: { + if (auto joystick = GetSDLJoystickBySDLID(event.csensor.which)) { + switch (event.csensor.sensor) { + case SDL_SENSOR_ACCEL: + joystick->SetAccel(event.csensor.data[0] / SDL_STANDARD_GRAVITY, + -event.csensor.data[1] / SDL_STANDARD_GRAVITY, + event.csensor.data[2] / SDL_STANDARD_GRAVITY); + break; + case SDL_SENSOR_GYRO: + joystick->SetGyro(-event.csensor.data[0] * (180.0f / Common::PI), + event.csensor.data[1] * (180.0f / Common::PI), + -event.csensor.data[2] * (180.0f / Common::PI)); + break; + } + } + break; + } +#endif + case SDL_JOYDEVICEREMOVED: + LOG_DEBUG(Input, "Joystick removed with Instance_ID {}", event.jdevice.which); + CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which)); + break; + case SDL_JOYDEVICEADDED: + LOG_DEBUG(Input, "Joystick connected with device index {}", event.jdevice.which); + InitJoystick(event.jdevice.which); + break; + } +} + +void SDLState::CloseJoysticks() { + std::lock_guard lock{joystick_map_mutex}; + joystick_map.clear(); +} + +class SDLButton final : public Input::ButtonDevice { +public: + explicit SDLButton(std::shared_ptr joystick_, int button_) + : joystick(std::move(joystick_)), button(button_) {} + + bool GetStatus() const override { + return joystick->GetButton(button); + } + +private: + std::shared_ptr joystick; + int button; +}; + +class SDLDirectionButton final : public Input::ButtonDevice { +public: + explicit SDLDirectionButton(std::shared_ptr joystick_, int hat_, Uint8 direction_) + : joystick(std::move(joystick_)), hat(hat_), direction(direction_) {} + + bool GetStatus() const override { + return joystick->GetHatDirection(hat, direction); + } + +private: + std::shared_ptr joystick; + int hat; + Uint8 direction; +}; + +class SDLAxisButton final : public Input::ButtonDevice { +public: + explicit SDLAxisButton(std::shared_ptr joystick_, int axis_, float threshold_, + bool trigger_if_greater_) + : joystick(std::move(joystick_)), axis(axis_), threshold(threshold_), + trigger_if_greater(trigger_if_greater_) {} + + bool GetStatus() const override { + float axis_value = joystick->GetAxis(axis); + if (trigger_if_greater) + return axis_value > threshold; + return axis_value < threshold; + } + +private: + std::shared_ptr joystick; + int axis; + float threshold; + bool trigger_if_greater; +}; + +class SDLAnalog final : public Input::AnalogDevice { +public: + SDLAnalog(std::shared_ptr joystick_, int axis_x_, int axis_y_, float deadzone_) + : joystick(std::move(joystick_)), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_) {} + + std::tuple GetStatus() const override { + const auto [x, y] = joystick->GetAnalog(axis_x, axis_y); + const float r = std::sqrt((x * x) + (y * y)); + if (r > deadzone) { + return std::make_tuple(x / r * (r - deadzone) / (1 - deadzone), + y / r * (r - deadzone) / (1 - deadzone)); + } + return std::make_tuple(0.0f, 0.0f); + } + +private: + std::shared_ptr joystick; + const int axis_x; + const int axis_y; + const float deadzone; +}; + +class SDLMotion final : public Input::MotionDevice { +public: + explicit SDLMotion(std::shared_ptr joystick_) : joystick(std::move(joystick_)) {} + + std::tuple, Common::Vec3> GetStatus() const override { + return joystick->GetMotion(); + } + +private: + std::shared_ptr joystick; +}; + +/// A button device factory that creates button devices from SDL joystick +class SDLButtonFactory final : public Input::Factory { +public: + explicit SDLButtonFactory(SDLState& state_) : state(state_) {} + + /** + * Creates a button device from a joystick button + * @param params contains parameters for creating the device: + * - "guid": the guid of the joystick to bind + * - "port": the nth joystick of the same type to bind + * - "button"(optional): the index of the button to bind + * - "hat"(optional): the index of the hat to bind as direction buttons + * - "axis"(optional): the index of the axis to bind + * - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", + * "down", "left" or "right" + * - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is + * triggered if the axis value crosses + * - "direction"(only used for axis): "+" means the button is triggered when the axis + * value is greater than the threshold; "-" means the button is triggered when the axis + * value is smaller than the threshold + */ + std::unique_ptr Create(const Common::ParamPackage& params) override { + const std::string guid = params.Get("guid", "0"); + const int port = params.Get("port", 0); + + auto joystick = state.GetSDLJoystickByGUID(guid, port); + + if (params.Has("hat")) { + const int hat = params.Get("hat", 0); + const std::string direction_name = params.Get("direction", ""); + Uint8 direction; + if (direction_name == "up") { + direction = SDL_HAT_UP; + } else if (direction_name == "down") { + direction = SDL_HAT_DOWN; + } else if (direction_name == "left") { + direction = SDL_HAT_LEFT; + } else if (direction_name == "right") { + direction = SDL_HAT_RIGHT; + } else { + direction = 0; + } + // This is necessary so accessing GetHat with hat won't crash + joystick->SetHat(hat, SDL_HAT_CENTERED); + return std::make_unique(joystick, hat, direction); + } + + if (params.Has("axis")) { + const int axis = params.Get("axis", 0); + const float threshold = params.Get("threshold", 0.5f); + const std::string direction_name = params.Get("direction", ""); + bool trigger_if_greater; + if (direction_name == "+") { + trigger_if_greater = true; + } else if (direction_name == "-") { + trigger_if_greater = false; + } else { + trigger_if_greater = true; + LOG_ERROR(Input, "Unknown direction {}", direction_name); + } + // This is necessary so accessing GetAxis with axis won't crash + joystick->SetAxis(axis, 0); + return std::make_unique(joystick, axis, threshold, trigger_if_greater); + } + + const int button = params.Get("button", 0); + // This is necessary so accessing GetButton with button won't crash + joystick->SetButton(button, false); + return std::make_unique(joystick, button); + } + +private: + SDLState& state; +}; + +/// An analog device factory that creates analog devices from SDL joystick +class SDLAnalogFactory final : public Input::Factory { +public: + explicit SDLAnalogFactory(SDLState& state_) : state(state_) {} + /** + * Creates analog device from joystick axes + * @param params contains parameters for creating the device: + * - "guid": the guid of the joystick to bind + * - "port": the nth joystick of the same type + * - "axis_x": the index of the axis to be bind as x-axis + * - "axis_y": the index of the axis to be bind as y-axis + */ + std::unique_ptr Create(const Common::ParamPackage& params) override { + const std::string guid = params.Get("guid", "0"); + const int port = params.Get("port", 0); + const int axis_x = params.Get("axis_x", 0); + const int axis_y = params.Get("axis_y", 1); + float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); + + auto joystick = state.GetSDLJoystickByGUID(guid, port); + + // This is necessary so accessing GetAxis with axis_x and axis_y won't crash + joystick->SetAxis(axis_x, 0); + joystick->SetAxis(axis_y, 0); + return std::make_unique(joystick, axis_x, axis_y, deadzone); + } + +private: + SDLState& state; +}; + +class SDLMotionFactory final : public Input::Factory { +public: + explicit SDLMotionFactory(SDLState& state_) : state(state_) {} + + std::unique_ptr Create(const Common::ParamPackage& params) override { + const std::string guid = params.Get("guid", "0"); + const int port = params.Get("port", 0); + + auto joystick = state.GetSDLJoystickByGUID(guid, port); + + return std::make_unique(joystick); + } + +private: + SDLState& state; +}; + +SDLState::SDLState() { + using namespace Input; + RegisterFactory("sdl", std::make_shared(*this)); + RegisterFactory("sdl", std::make_shared(*this)); + RegisterFactory("sdl", std::make_shared(*this)); + + // If the frontend is going to manage the event loop, then we dont start one here + start_thread = !SDL_WasInit(SDL_INIT_GAMECONTROLLER); + if (start_thread && SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) { + LOG_CRITICAL(Input, "SDL_Init(SDL_INIT_GAMECONTROLLER) failed with: {}", SDL_GetError()); + return; + } + if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) { + LOG_ERROR(Input, "Failed to set Hint for background events: {}", SDL_GetError()); + } +// these hints are only defined on sdl2.0.9 or higher +#if SDL_VERSION_ATLEAST(2, 0, 9) +#if !SDL_VERSION_ATLEAST(2, 0, 12) + // There are also hints to toggle the individual drivers if needed. + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); +#endif +#endif + + // Prevent SDL from adding undesired axis +#ifdef SDL_HINT_ACCELEROMETER_AS_JOYSTICK + SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); +#endif + + // Enable HIDAPI rumble. This prevents SDL from disabling motion on PS4 and PS5 controllers +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); +#endif + + SDL_AddEventWatch(&SDLEventWatcher, this); + + initialized = true; + if (start_thread) { + poll_thread = std::thread([this] { + using namespace std::chrono_literals; + while (initialized) { + SDL_PumpEvents(); + std::this_thread::sleep_for(10ms); + } + }); + } + // Because the events for joystick connection happens before we have our event watcher added, we + // can just open all the joysticks right here + for (int i = 0; i < SDL_NumJoysticks(); ++i) { + InitJoystick(i); + } +} + +SDLState::~SDLState() { + using namespace Input; + UnregisterFactory("sdl"); + UnregisterFactory("sdl"); + UnregisterFactory("sdl"); + + CloseJoysticks(); + SDL_DelEventWatch(&SDLEventWatcher, this); + + initialized = false; + if (start_thread) { + poll_thread.join(); + SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER); + } +} + +Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event) { + Common::ParamPackage params({{"engine", "sdl"}}); + + switch (event.type) { + case SDL_JOYAXISMOTION: { + auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); + params.Set("port", joystick->GetPort()); + params.Set("guid", joystick->GetGUID()); + params.Set("axis", event.jaxis.axis); + if (event.jaxis.value > 0) { + params.Set("direction", "+"); + params.Set("threshold", "0.5"); + } else { + params.Set("direction", "-"); + params.Set("threshold", "-0.5"); + } + break; + } + case SDL_JOYBUTTONUP: { + auto joystick = state.GetSDLJoystickBySDLID(event.jbutton.which); + params.Set("port", joystick->GetPort()); + params.Set("guid", joystick->GetGUID()); + params.Set("button", event.jbutton.button); + break; + } + case SDL_JOYHATMOTION: { + auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which); + params.Set("port", joystick->GetPort()); + params.Set("guid", joystick->GetGUID()); + params.Set("hat", event.jhat.hat); + switch (event.jhat.value) { + case SDL_HAT_UP: + params.Set("direction", "up"); + break; + case SDL_HAT_DOWN: + params.Set("direction", "down"); + break; + case SDL_HAT_LEFT: + params.Set("direction", "left"); + break; + case SDL_HAT_RIGHT: + params.Set("direction", "right"); + break; + default: + return {}; + } + break; + } + } + return params; +} + +namespace Polling { + +class SDLPoller : public InputCommon::Polling::DevicePoller { +public: + explicit SDLPoller(SDLState& state_) : state(state_) {} + + void Start() override { + state.event_queue.Clear(); + state.polling = true; + } + + void Stop() override { + state.polling = false; + } + +protected: + SDLState& state; +}; + +class SDLButtonPoller final : public SDLPoller { +public: + explicit SDLButtonPoller(SDLState& state_) : SDLPoller(state_) {} + + Common::ParamPackage GetNextInput() override { + SDL_Event event; + while (state.event_queue.Pop(event)) { + switch (event.type) { + case SDL_JOYAXISMOTION: + if (!axis_memory.count(event.jaxis.which) || + !axis_memory[event.jaxis.which].count(event.jaxis.axis)) { + axis_memory[event.jaxis.which][event.jaxis.axis] = event.jaxis.value; + axis_event_count[event.jaxis.which][event.jaxis.axis] = 1; + break; + } else { + axis_event_count[event.jaxis.which][event.jaxis.axis]++; + // The joystick and axis exist in our map if we take this branch, so no checks + // needed + if (std::abs( + (event.jaxis.value - axis_memory[event.jaxis.which][event.jaxis.axis]) / + 32767.0) < 0.5) { + break; + } else { + if (axis_event_count[event.jaxis.which][event.jaxis.axis] == 2 && + IsAxisAtPole(event.jaxis.value) && + IsAxisAtPole(axis_memory[event.jaxis.which][event.jaxis.axis])) { + // If we have exactly two events and both are near a pole, this is + // likely a digital input masquerading as an analog axis; Instead of + // trying to look at the direction the axis travelled, assume the first + // event was press and the second was release; This should handle most + // digital axes while deferring to the direction of travel for analog + // axes + event.jaxis.value = static_cast(std::copysign( + 32767, axis_memory[event.jaxis.which][event.jaxis.axis])); + } else { + // There are more than two events, so this is likely a true analog axis, + // check the direction it travelled + event.jaxis.value = static_cast(std::copysign( + 32767, event.jaxis.value - + axis_memory[event.jaxis.which][event.jaxis.axis])); + } + axis_memory.clear(); + axis_event_count.clear(); + } + } + case SDL_JOYBUTTONUP: + case SDL_JOYHATMOTION: + return SDLEventToButtonParamPackage(state, event); + } + } + return {}; + } + +private: + // Determine whether an axis value is close to an extreme or center + // Some controllers have a digital D-Pad as a pair of analog sticks, with 3 possible values per + // axis, which is why the center must be considered a pole + bool IsAxisAtPole(int16_t value) { + return std::abs(value) >= 32767 || std::abs(value) < 327; + } + std::unordered_map> axis_memory; + std::unordered_map> axis_event_count; +}; + +class SDLAnalogPoller final : public SDLPoller { +public: + explicit SDLAnalogPoller(SDLState& state_) : SDLPoller(state_) {} + + void Start() override { + SDLPoller::Start(); + + // Reset stored axes + analog_xaxis = -1; + analog_yaxis = -1; + analog_axes_joystick = -1; + } + + Common::ParamPackage GetNextInput() override { + SDL_Event event{}; + while (state.event_queue.Pop(event)) { + if (event.type != SDL_JOYAXISMOTION || std::abs(event.jaxis.value / 32767.0) < 0.5) { + continue; + } + // An analog device needs two axes, so we need to store the axis for later and wait for + // a second SDL event. The axes also must be from the same joystick. + int axis = event.jaxis.axis; + if (analog_xaxis == -1) { + analog_xaxis = axis; + analog_axes_joystick = event.jaxis.which; + } else if (analog_yaxis == -1 && analog_xaxis != axis && + analog_axes_joystick == event.jaxis.which) { + analog_yaxis = axis; + } + } + Common::ParamPackage params; + if (analog_xaxis != -1 && analog_yaxis != -1) { + auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); + params.Set("engine", "sdl"); + params.Set("port", joystick->GetPort()); + params.Set("guid", joystick->GetGUID()); + params.Set("axis_x", analog_xaxis); + params.Set("axis_y", analog_yaxis); + analog_xaxis = -1; + analog_yaxis = -1; + analog_axes_joystick = -1; + return params; + } + return params; + } + +private: + int analog_xaxis = -1; + int analog_yaxis = -1; + SDL_JoystickID analog_axes_joystick = -1; +}; +} // namespace Polling + +SDLState::Pollers SDLState::GetPollers(InputCommon::Polling::DeviceType type) { + Pollers pollers; + + switch (type) { + case InputCommon::Polling::DeviceType::Analog: + pollers.emplace_back(std::make_unique(*this)); + break; + case InputCommon::Polling::DeviceType::Button: + pollers.emplace_back(std::make_unique(*this)); + break; + } + + return pollers; +} + +} // namespace SDL +} // namespace InputCommon diff --git a/src/input_common/sdl/sdl_impl.h b/src/input_common/sdl/sdl_impl.h new file mode 100644 index 000000000..56f53121b --- /dev/null +++ b/src/input_common/sdl/sdl_impl.h @@ -0,0 +1,74 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/settings.h" +#include "common/threadsafe_queue.h" +#include "input_common/sdl/sdl.h" + +union SDL_Event; +using SDL_Joystick = struct _SDL_Joystick; +using SDL_JoystickID = s32; +using SDL_GameController = struct _SDL_GameController; + +namespace InputCommon::SDL { + +class SDLJoystick; +class SDLGameController; +class SDLButtonFactory; +class SDLAnalogFactory; +class SDLMotionFactory; + +class SDLState : public State { +public: + /// Initializes and registers SDL device factories + SDLState(); + + /// Unregisters SDL device factories and shut them down. + ~SDLState() override; + + /// Handle SDL_Events for joysticks from SDL_PollEvent + void HandleGameControllerEvent(const SDL_Event& event); + + std::shared_ptr GetSDLJoystickBySDLID(SDL_JoystickID sdl_id); + std::shared_ptr GetSDLJoystickByGUID(const std::string& guid, int port); + + Common::ParamPackage GetSDLControllerButtonBindByGUID(const std::string& guid, int port, + Settings::NativeButton::Values button); + Common::ParamPackage GetSDLControllerAnalogBindByGUID(const std::string& guid, int port, + Settings::NativeAnalog::Values analog); + + /// Get all DevicePoller that use the SDL backend for a specific device type + Pollers GetPollers(Polling::DeviceType type) override; + + /// Used by the Pollers during config + std::atomic polling = false; + Common::SPSCQueue event_queue; + +private: + void InitJoystick(int joystick_index); + void CloseJoystick(SDL_Joystick* sdl_joystick); + + /// Needs to be called before SDL_QuitSubSystem. + void CloseJoysticks(); + + /// Map of GUID of a list of corresponding virtual Joysticks + std::unordered_map>> joystick_map; + std::mutex joystick_map_mutex; + + std::shared_ptr button_factory; + std::shared_ptr analog_factory; + std::shared_ptr motion_factory; + + bool start_thread = false; + std::atomic initialized = false; + + std::thread poll_thread; +}; +} // namespace InputCommon::SDL diff --git a/src/lime_qt/main.cpp b/src/lime_qt/main.cpp index 68541d757..2e77c8157 100644 --- a/src/lime_qt/main.cpp +++ b/src/lime_qt/main.cpp @@ -119,6 +119,10 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; } #endif +#ifdef HAVE_SDL2 +#include +#endif + constexpr int default_mouse_timeout = 2500; /** @@ -1129,9 +1133,58 @@ void GMainWindow::MigrateUserData() { QMessageBox::Ok); } +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Lime3DS needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Lime3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + void GMainWindow::PreventOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) #endif // _WIN32 }