diff options
Diffstat (limited to '')
-rw-r--r-- | src/input_common/CMakeLists.txt | 8 | ||||
-rw-r--r-- | src/input_common/gcadapter/gc_adapter.cpp | 290 | ||||
-rw-r--r-- | src/input_common/gcadapter/gc_adapter.h | 51 | ||||
-rw-r--r-- | src/input_common/gcadapter/gc_poller.cpp | 40 | ||||
-rw-r--r-- | src/input_common/main.cpp | 263 | ||||
-rw-r--r-- | src/input_common/main.h | 152 | ||||
-rw-r--r-- | src/input_common/motion_emu.cpp | 17 | ||||
-rw-r--r-- | src/input_common/motion_from_button.cpp | 34 | ||||
-rw-r--r-- | src/input_common/motion_from_button.h | 25 | ||||
-rw-r--r-- | src/input_common/motion_input.cpp | 305 | ||||
-rw-r--r-- | src/input_common/motion_input.h | 73 | ||||
-rw-r--r-- | src/input_common/sdl/sdl.h | 19 | ||||
-rw-r--r-- | src/input_common/sdl/sdl_impl.cpp | 665 | ||||
-rw-r--r-- | src/input_common/sdl/sdl_impl.h | 10 | ||||
-rw-r--r-- | src/input_common/settings.cpp | 40 | ||||
-rw-r--r-- | src/input_common/settings.h | 352 | ||||
-rw-r--r-- | src/input_common/touch_from_button.cpp | 50 | ||||
-rw-r--r-- | src/input_common/touch_from_button.h | 23 | ||||
-rw-r--r-- | src/input_common/udp/client.cpp | 185 | ||||
-rw-r--r-- | src/input_common/udp/client.h | 77 | ||||
-rw-r--r-- | src/input_common/udp/udp.cpp | 177 | ||||
-rw-r--r-- | src/input_common/udp/udp.h | 54 |
22 files changed, 2380 insertions, 530 deletions
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 317c25bad..c84685214 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -7,6 +7,14 @@ add_library(input_common STATIC main.h motion_emu.cpp motion_emu.h + motion_from_button.cpp + motion_from_button.h + motion_input.cpp + motion_input.h + settings.cpp + settings.h + touch_from_button.cpp + touch_from_button.h gcadapter/gc_adapter.cpp gcadapter/gc_adapter.h gcadapter/gc_poller.cpp diff --git a/src/input_common/gcadapter/gc_adapter.cpp b/src/input_common/gcadapter/gc_adapter.cpp index 898a278a9..89c148aba 100644 --- a/src/input_common/gcadapter/gc_adapter.cpp +++ b/src/input_common/gcadapter/gc_adapter.cpp @@ -4,9 +4,20 @@ #include <chrono> #include <thread> + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4200) // nonstandard extension used : zero-sized array in struct/union +#endif #include <libusb.h> +#ifdef _MSC_VER +#pragma warning(pop) +#endif + #include "common/logging/log.h" +#include "common/param_package.h" #include "input_common/gcadapter/gc_adapter.h" +#include "input_common/settings.h" namespace GCAdapter { @@ -24,12 +35,9 @@ Adapter::Adapter() { } LOG_INFO(Input, "GC Adapter Initialization started"); - current_status = NO_ADAPTER_DETECTED; - get_origin.fill(true); - const int init_res = libusb_init(&libusb_ctx); if (init_res == LIBUSB_SUCCESS) { - StartScanThread(); + Setup(); } else { LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res); } @@ -37,9 +45,9 @@ Adapter::Adapter() { GCPadStatus Adapter::GetPadStatus(std::size_t port, const std::array<u8, 37>& adapter_payload) { GCPadStatus pad = {}; + const std::size_t offset = 1 + (9 * port); - ControllerTypes type = ControllerTypes(adapter_payload[1 + (9 * port)] >> 4); - adapter_controllers_status[port] = type; + adapter_controllers_status[port] = static_cast<ControllerTypes>(adapter_payload[offset] >> 4); static constexpr std::array<PadButton, 8> b1_buttons{ PadButton::PAD_BUTTON_A, PadButton::PAD_BUTTON_B, PadButton::PAD_BUTTON_X, @@ -54,14 +62,19 @@ GCPadStatus Adapter::GetPadStatus(std::size_t port, const std::array<u8, 37>& ad PadButton::PAD_TRIGGER_L, }; + static constexpr std::array<PadAxes, 6> axes{ + PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX, + PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight, + }; + if (adapter_controllers_status[port] == ControllerTypes::None && !get_origin[port]) { // Controller may have been disconnected, recalibrate if reconnected. get_origin[port] = true; } if (adapter_controllers_status[port] != ControllerTypes::None) { - const u8 b1 = adapter_payload[1 + (9 * port) + 1]; - const u8 b2 = adapter_payload[1 + (9 * port) + 2]; + const u8 b1 = adapter_payload[offset + 1]; + const u8 b2 = adapter_payload[offset + 2]; for (std::size_t i = 0; i < b1_buttons.size(); ++i) { if ((b1 & (1U << i)) != 0) { @@ -74,21 +87,13 @@ GCPadStatus Adapter::GetPadStatus(std::size_t port, const std::array<u8, 37>& ad pad.button |= static_cast<u16>(b2_buttons[j]); } } - - pad.stick_x = adapter_payload[1 + (9 * port) + 3]; - pad.stick_y = adapter_payload[1 + (9 * port) + 4]; - pad.substick_x = adapter_payload[1 + (9 * port) + 5]; - pad.substick_y = adapter_payload[1 + (9 * port) + 6]; - pad.trigger_left = adapter_payload[1 + (9 * port) + 7]; - pad.trigger_right = adapter_payload[1 + (9 * port) + 8]; + for (PadAxes axis : axes) { + const std::size_t index = static_cast<std::size_t>(axis); + pad.axis_values[index] = adapter_payload[offset + 3 + index]; + } if (get_origin[port]) { - origin_status[port].stick_x = pad.stick_x; - origin_status[port].stick_y = pad.stick_y; - origin_status[port].substick_x = pad.substick_x; - origin_status[port].substick_y = pad.substick_y; - origin_status[port].trigger_left = pad.trigger_left; - origin_status[port].trigger_right = pad.trigger_right; + origin_status[port].axis_values = pad.axis_values; get_origin[port] = false; } } @@ -101,82 +106,47 @@ void Adapter::PadToState(const GCPadStatus& pad, GCState& state) { state.buttons.insert_or_assign(button_value, pad.button & button_value); } - state.axes.insert_or_assign(static_cast<u8>(PadAxes::StickX), pad.stick_x); - state.axes.insert_or_assign(static_cast<u8>(PadAxes::StickY), pad.stick_y); - state.axes.insert_or_assign(static_cast<u8>(PadAxes::SubstickX), pad.substick_x); - state.axes.insert_or_assign(static_cast<u8>(PadAxes::SubstickY), pad.substick_y); - state.axes.insert_or_assign(static_cast<u8>(PadAxes::TriggerLeft), pad.trigger_left); - state.axes.insert_or_assign(static_cast<u8>(PadAxes::TriggerRight), pad.trigger_right); + for (size_t i = 0; i < pad.axis_values.size(); ++i) { + state.axes.insert_or_assign(static_cast<u8>(i), pad.axis_values[i]); + } } void Adapter::Read() { LOG_DEBUG(Input, "GC Adapter Read() thread started"); - int payload_size_in, payload_size_copy; + int payload_size; std::array<u8, 37> adapter_payload; - std::array<u8, 37> adapter_payload_copy; std::array<GCPadStatus, 4> pads; while (adapter_thread_running) { libusb_interrupt_transfer(usb_adapter_handle, input_endpoint, adapter_payload.data(), - sizeof(adapter_payload), &payload_size_in, 16); - payload_size_copy = 0; - // this mutex might be redundant? - { - std::lock_guard<std::mutex> lk(s_mutex); - std::copy(std::begin(adapter_payload), std::end(adapter_payload), - std::begin(adapter_payload_copy)); - payload_size_copy = payload_size_in; - } + sizeof(adapter_payload), &payload_size, 16); - if (payload_size_copy != sizeof(adapter_payload_copy) || - adapter_payload_copy[0] != LIBUSB_DT_HID) { - LOG_ERROR(Input, "error reading payload (size: {}, type: {:02x})", payload_size_copy, - adapter_payload_copy[0]); + if (payload_size != sizeof(adapter_payload) || adapter_payload[0] != LIBUSB_DT_HID) { + LOG_ERROR(Input, + "Error reading payload (size: {}, type: {:02x}) Is the adapter connected?", + payload_size, adapter_payload[0]); adapter_thread_running = false; // error reading from adapter, stop reading. break; } for (std::size_t port = 0; port < pads.size(); ++port) { - pads[port] = GetPadStatus(port, adapter_payload_copy); + pads[port] = GetPadStatus(port, adapter_payload); if (DeviceConnected(port) && configuring) { if (pads[port].button != 0) { pad_queue[port].Push(pads[port]); } - // Accounting for a threshold here because of some controller variance - if (pads[port].stick_x > origin_status[port].stick_x + pads[port].THRESHOLD || - pads[port].stick_x < origin_status[port].stick_x - pads[port].THRESHOLD) { - pads[port].axis = GCAdapter::PadAxes::StickX; - pads[port].axis_value = pads[port].stick_x; - pad_queue[port].Push(pads[port]); - } - if (pads[port].stick_y > origin_status[port].stick_y + pads[port].THRESHOLD || - pads[port].stick_y < origin_status[port].stick_y - pads[port].THRESHOLD) { - pads[port].axis = GCAdapter::PadAxes::StickY; - pads[port].axis_value = pads[port].stick_y; - pad_queue[port].Push(pads[port]); - } - if (pads[port].substick_x > origin_status[port].substick_x + pads[port].THRESHOLD || - pads[port].substick_x < origin_status[port].substick_x - pads[port].THRESHOLD) { - pads[port].axis = GCAdapter::PadAxes::SubstickX; - pads[port].axis_value = pads[port].substick_x; - pad_queue[port].Push(pads[port]); - } - if (pads[port].substick_y > origin_status[port].substick_y + pads[port].THRESHOLD || - pads[port].substick_y < origin_status[port].substick_y - pads[port].THRESHOLD) { - pads[port].axis = GCAdapter::PadAxes::SubstickY; - pads[port].axis_value = pads[port].substick_y; - pad_queue[port].Push(pads[port]); - } - if (pads[port].trigger_left > pads[port].TRIGGER_THRESHOLD) { - pads[port].axis = GCAdapter::PadAxes::TriggerLeft; - pads[port].axis_value = pads[port].trigger_left; - pad_queue[port].Push(pads[port]); - } - if (pads[port].trigger_right > pads[port].TRIGGER_THRESHOLD) { - pads[port].axis = GCAdapter::PadAxes::TriggerRight; - pads[port].axis_value = pads[port].trigger_right; - pad_queue[port].Push(pads[port]); + // Accounting for a threshold here to ensure an intentional press + for (size_t i = 0; i < pads[port].axis_values.size(); ++i) { + const u8 value = pads[port].axis_values[i]; + const u8 origin = origin_status[port].axis_values[i]; + + if (value > origin + pads[port].THRESHOLD || + value < origin - pads[port].THRESHOLD) { + pads[port].axis = static_cast<PadAxes>(i); + pads[port].axis_value = pads[port].axis_values[i]; + pad_queue[port].Push(pads[port]); + } } } PadToState(pads[port], state[port]); @@ -185,42 +155,11 @@ void Adapter::Read() { } } -void Adapter::ScanThreadFunc() { - LOG_INFO(Input, "GC Adapter scanning thread started"); - - while (detect_thread_running) { - if (usb_adapter_handle == nullptr) { - std::lock_guard<std::mutex> lk(initialization_mutex); - Setup(); - } - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } -} - -void Adapter::StartScanThread() { - if (detect_thread_running) { - return; - } - if (!libusb_ctx) { - return; - } - - detect_thread_running = true; - detect_thread = std::thread(&Adapter::ScanThreadFunc, this); -} - -void Adapter::StopScanThread() { - detect_thread_running = false; - detect_thread.join(); -} - void Adapter::Setup() { - // Reset the error status in case the adapter gets unplugged - if (current_status < 0) { - current_status = NO_ADAPTER_DETECTED; - } - + // Initialize all controllers as unplugged adapter_controllers_status.fill(ControllerTypes::None); + // Initialize all ports to store axis origin values + get_origin.fill(true); // pointer to list of connected usb devices libusb_device** devices{}; @@ -229,8 +168,6 @@ void Adapter::Setup() { const ssize_t device_count = libusb_get_device_list(libusb_ctx, &devices); if (device_count < 0) { LOG_ERROR(Input, "libusb_get_device_list failed with error: {}", device_count); - detect_thread_running = false; // Stop the loop constantly checking for gc adapter - // TODO: For hotplug+gc adapter checkbox implementation, revert this. return; } @@ -244,9 +181,6 @@ void Adapter::Setup() { } libusb_free_device_list(devices, 1); } - // Break out of the ScanThreadFunc() loop that is constantly looking for the device - // Assumes user has GC adapter plugged in before launch to use the adapter - detect_thread_running = false; } bool Adapter::CheckDeviceAccess(libusb_device* device) { @@ -331,32 +265,23 @@ void Adapter::GetGCEndpoint(libusb_device* device) { sizeof(clear_payload), nullptr, 16); adapter_thread_running = true; - current_status = ADAPTER_DETECTED; - adapter_input_thread = std::thread([=] { Read(); }); // Read input + adapter_input_thread = std::thread(&Adapter::Read, this); } Adapter::~Adapter() { - StopScanThread(); Reset(); } void Adapter::Reset() { - std::unique_lock<std::mutex> lock(initialization_mutex, std::defer_lock); - if (!lock.try_lock()) { - return; - } - if (current_status != ADAPTER_DETECTED) { - return; - } - if (adapter_thread_running) { adapter_thread_running = false; } - adapter_input_thread.join(); + if (adapter_input_thread.joinable()) { + adapter_input_thread.join(); + } adapter_controllers_status.fill(ControllerTypes::None); get_origin.fill(true); - current_status = NO_ADAPTER_DETECTED; if (usb_adapter_handle) { libusb_release_interface(usb_adapter_handle, 1); @@ -369,7 +294,93 @@ void Adapter::Reset() { } } -bool Adapter::DeviceConnected(std::size_t port) { +std::vector<Common::ParamPackage> Adapter::GetInputDevices() const { + std::vector<Common::ParamPackage> devices; + for (std::size_t port = 0; port < state.size(); ++port) { + if (!DeviceConnected(port)) { + continue; + } + std::string name = fmt::format("Gamecube Controller {}", port); + devices.emplace_back(Common::ParamPackage{ + {"class", "gcpad"}, + {"display", std::move(name)}, + {"port", std::to_string(port)}, + }); + } + return devices; +} + +InputCommon::ButtonMapping Adapter::GetButtonMappingForDevice( + const Common::ParamPackage& params) const { + // This list is missing ZL/ZR since those are not considered buttons. + // We will add those afterwards + // This list also excludes any button that can't be really mapped + static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 12> + switch_to_gcadapter_button = { + std::pair{Settings::NativeButton::A, PadButton::PAD_BUTTON_A}, + {Settings::NativeButton::B, PadButton::PAD_BUTTON_B}, + {Settings::NativeButton::X, PadButton::PAD_BUTTON_X}, + {Settings::NativeButton::Y, PadButton::PAD_BUTTON_Y}, + {Settings::NativeButton::Plus, PadButton::PAD_BUTTON_START}, + {Settings::NativeButton::DLeft, PadButton::PAD_BUTTON_LEFT}, + {Settings::NativeButton::DUp, PadButton::PAD_BUTTON_UP}, + {Settings::NativeButton::DRight, PadButton::PAD_BUTTON_RIGHT}, + {Settings::NativeButton::DDown, PadButton::PAD_BUTTON_DOWN}, + {Settings::NativeButton::SL, PadButton::PAD_TRIGGER_L}, + {Settings::NativeButton::SR, PadButton::PAD_TRIGGER_R}, + {Settings::NativeButton::R, PadButton::PAD_TRIGGER_Z}, + }; + if (!params.Has("port")) { + return {}; + } + + InputCommon::ButtonMapping mapping{}; + for (const auto& [switch_button, gcadapter_button] : switch_to_gcadapter_button) { + Common::ParamPackage button_params({{"engine", "gcpad"}}); + button_params.Set("port", params.Get("port", 0)); + button_params.Set("button", static_cast<int>(gcadapter_button)); + mapping.insert_or_assign(switch_button, std::move(button_params)); + } + + // Add the missing bindings for ZL/ZR + static constexpr std::array<std::pair<Settings::NativeButton::Values, PadAxes>, 2> + switch_to_gcadapter_axis = { + std::pair{Settings::NativeButton::ZL, PadAxes::TriggerLeft}, + {Settings::NativeButton::ZR, PadAxes::TriggerRight}, + }; + for (const auto& [switch_button, gcadapter_axis] : switch_to_gcadapter_axis) { + Common::ParamPackage button_params({{"engine", "gcpad"}}); + button_params.Set("port", params.Get("port", 0)); + button_params.Set("button", static_cast<int>(PadButton::PAD_STICK)); + button_params.Set("axis", static_cast<int>(gcadapter_axis)); + mapping.insert_or_assign(switch_button, std::move(button_params)); + } + return mapping; +} + +InputCommon::AnalogMapping Adapter::GetAnalogMappingForDevice( + const Common::ParamPackage& params) const { + if (!params.Has("port")) { + return {}; + } + + InputCommon::AnalogMapping mapping = {}; + Common::ParamPackage left_analog_params; + left_analog_params.Set("engine", "gcpad"); + left_analog_params.Set("port", params.Get("port", 0)); + left_analog_params.Set("axis_x", static_cast<int>(PadAxes::StickX)); + left_analog_params.Set("axis_y", static_cast<int>(PadAxes::StickY)); + mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params)); + Common::ParamPackage right_analog_params; + right_analog_params.Set("engine", "gcpad"); + right_analog_params.Set("port", params.Get("port", 0)); + right_analog_params.Set("axis_x", static_cast<int>(PadAxes::SubstickX)); + right_analog_params.Set("axis_y", static_cast<int>(PadAxes::SubstickY)); + mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params)); + return mapping; +} + +bool Adapter::DeviceConnected(std::size_t port) const { return adapter_controllers_status[port] != ControllerTypes::None; } @@ -409,24 +420,7 @@ const std::array<GCState, 4>& Adapter::GetPadState() const { } int Adapter::GetOriginValue(int port, int axis) const { - const auto& status = origin_status[port]; - - switch (static_cast<PadAxes>(axis)) { - case PadAxes::StickX: - return status.stick_x; - case PadAxes::StickY: - return status.stick_y; - case PadAxes::SubstickX: - return status.substick_x; - case PadAxes::SubstickY: - return status.substick_y; - case PadAxes::TriggerLeft: - return status.trigger_left; - case PadAxes::TriggerRight: - return status.trigger_right; - default: - return 0; - } + return origin_status[port].axis_values[axis]; } } // namespace GCAdapter diff --git a/src/input_common/gcadapter/gc_adapter.h b/src/input_common/gcadapter/gc_adapter.h index 3586c8bda..75bf9fe74 100644 --- a/src/input_common/gcadapter/gc_adapter.h +++ b/src/input_common/gcadapter/gc_adapter.h @@ -10,6 +10,7 @@ #include <unordered_map> #include "common/common_types.h" #include "common/threadsafe_queue.h" +#include "input_common/main.h" struct libusb_context; struct libusb_device; @@ -47,24 +48,10 @@ enum class PadAxes : u8 { }; struct GCPadStatus { - u16 button{}; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits - u8 stick_x{}; // 0 <= stick_x <= 255 - u8 stick_y{}; // 0 <= stick_y <= 255 - u8 substick_x{}; // 0 <= substick_x <= 255 - u8 substick_y{}; // 0 <= substick_y <= 255 - u8 trigger_left{}; // 0 <= trigger_left <= 255 - u8 trigger_right{}; // 0 <= trigger_right <= 255 - - static constexpr u8 MAIN_STICK_CENTER_X = 0x80; - static constexpr u8 MAIN_STICK_CENTER_Y = 0x80; - static constexpr u8 MAIN_STICK_RADIUS = 0x7f; - static constexpr u8 C_STICK_CENTER_X = 0x80; - static constexpr u8 C_STICK_CENTER_Y = 0x80; - static constexpr u8 C_STICK_RADIUS = 0x7f; - static constexpr u8 THRESHOLD = 10; - - // 256/4, at least a quarter press to count as a press. For polling mostly - static constexpr u8 TRIGGER_THRESHOLD = 64; + u16 button{}; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits + + std::array<u8, 6> axis_values{}; // Triggers and sticks, following indices defined in PadAxes + static constexpr u8 THRESHOLD = 50; // Threshold for axis press for polling u8 port{}; PadAxes axis{PadAxes::Undefined}; @@ -78,11 +65,6 @@ struct GCState { enum class ControllerTypes { None, Wired, Wireless }; -enum { - NO_ADAPTER_DETECTED = 0, - ADAPTER_DETECTED = 1, -}; - class Adapter { public: /// Initialize the GC Adapter capture and read sequence @@ -94,8 +76,12 @@ public: void BeginConfiguration(); void EndConfiguration(); + std::vector<Common::ParamPackage> GetInputDevices() const; + InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const; + InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const; + /// Returns true if there is a device connected to port - bool DeviceConnected(std::size_t port); + bool DeviceConnected(std::size_t port) const; std::array<Common::SPSCQueue<GCPadStatus>, 4>& GetPadQueue(); const std::array<Common::SPSCQueue<GCPadStatus>, 4>& GetPadQueue() const; @@ -111,12 +97,6 @@ private: void PadToState(const GCPadStatus& pad, GCState& state); void Read(); - void ScanThreadFunc(); - /// Begin scanning for the GC Adapter. - void StartScanThread(); - - /// Stop scanning for the adapter - void StopScanThread(); /// Resets status of device connected to port void ResetDeviceType(std::size_t port); @@ -133,19 +113,11 @@ private: /// For use in initialization, querying devices to find the adapter void Setup(); - int current_status = NO_ADAPTER_DETECTED; libusb_device_handle* usb_adapter_handle = nullptr; - std::array<ControllerTypes, 4> adapter_controllers_status{}; - - std::mutex s_mutex; std::thread adapter_input_thread; bool adapter_thread_running; - std::mutex initialization_mutex; - std::thread detect_thread; - bool detect_thread_running = false; - libusb_context* libusb_ctx; u8 input_endpoint = 0; @@ -153,10 +125,11 @@ private: bool configuring = false; - std::array<Common::SPSCQueue<GCPadStatus>, 4> pad_queue; std::array<GCState, 4> state; std::array<bool, 4> get_origin; std::array<GCPadStatus, 4> origin_status; + std::array<Common::SPSCQueue<GCPadStatus>, 4> pad_queue; + std::array<ControllerTypes, 4> adapter_controllers_status{}; }; } // namespace GCAdapter diff --git a/src/input_common/gcadapter/gc_poller.cpp b/src/input_common/gcadapter/gc_poller.cpp index 96e22d3ad..92e9e8e89 100644 --- a/src/input_common/gcadapter/gc_poller.cpp +++ b/src/input_common/gcadapter/gc_poller.cpp @@ -15,7 +15,7 @@ namespace InputCommon { class GCButton final : public Input::ButtonDevice { public: - explicit GCButton(int port_, int button_, GCAdapter::Adapter* adapter) + explicit GCButton(int port_, int button_, const GCAdapter::Adapter* adapter) : port(port_), button(button_), gcadapter(adapter) {} ~GCButton() override; @@ -30,15 +30,16 @@ public: private: const int port; const int button; - GCAdapter::Adapter* gcadapter; + const GCAdapter::Adapter* gcadapter; }; class GCAxisButton final : public Input::ButtonDevice { public: explicit GCAxisButton(int port_, int axis_, float threshold_, bool trigger_if_greater_, - GCAdapter::Adapter* adapter) + const GCAdapter::Adapter* adapter) : port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_), - gcadapter(adapter), origin_value(adapter->GetOriginValue(port_, axis_)) {} + gcadapter(adapter), + origin_value(static_cast<float>(adapter->GetOriginValue(port_, axis_))) {} bool GetStatus() const override { if (gcadapter->DeviceConnected(port)) { @@ -59,7 +60,7 @@ private: const int axis; float threshold; bool trigger_if_greater; - GCAdapter::Adapter* gcadapter; + const GCAdapter::Adapter* gcadapter; const float origin_value; }; @@ -76,8 +77,7 @@ std::unique_ptr<Input::ButtonDevice> GCButtonFactory::Create(const Common::Param // button is not an axis/stick button if (button_id != PAD_STICK_ID) { - auto button = std::make_unique<GCButton>(port, button_id, adapter.get()); - return std::move(button); + return std::make_unique<GCButton>(port, button_id, adapter.get()); } // For Axis buttons, used by the binary sticks. @@ -149,19 +149,18 @@ void GCButtonFactory::EndConfiguration() { class GCAnalog final : public Input::AnalogDevice { public: - GCAnalog(int port_, int axis_x_, int axis_y_, float deadzone_, GCAdapter::Adapter* adapter) + GCAnalog(int port_, int axis_x_, int axis_y_, float deadzone_, + const GCAdapter::Adapter* adapter, float range_) : port(port_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), gcadapter(adapter), - origin_value_x(adapter->GetOriginValue(port_, axis_x_)), - origin_value_y(adapter->GetOriginValue(port_, axis_y_)) {} + origin_value_x(static_cast<float>(adapter->GetOriginValue(port_, axis_x_))), + origin_value_y(static_cast<float>(adapter->GetOriginValue(port_, axis_y_))), + range(range_) {} float GetAxis(int axis) const { if (gcadapter->DeviceConnected(port)) { std::lock_guard lock{mutex}; const auto origin_value = axis % 2 == 0 ? origin_value_x : origin_value_y; - // division is not by a perfect 128 to account for some variance in center location - // e.g. my device idled at 131 in X, 120 in Y, and full range of motion was in range - // [20-230] - return (gcadapter->GetPadState()[port].axes.at(axis) - origin_value) / 95.0f; + return (gcadapter->GetPadState()[port].axes.at(axis) - origin_value) / (100.0f * range); } return 0.0f; } @@ -194,7 +193,7 @@ public: bool GetAnalogDirectionStatus(Input::AnalogDirection direction) const override { const auto [x, y] = GetStatus(); - const float directional_deadzone = 0.4f; + const float directional_deadzone = 0.5f; switch (direction) { case Input::AnalogDirection::RIGHT: return x > directional_deadzone; @@ -213,9 +212,10 @@ private: const int axis_x; const int axis_y; const float deadzone; - GCAdapter::Adapter* gcadapter; + const GCAdapter::Adapter* gcadapter; const float origin_value_x; const float origin_value_y; + const float range; mutable std::mutex mutex; }; @@ -234,9 +234,10 @@ std::unique_ptr<Input::AnalogDevice> GCAnalogFactory::Create(const Common::Param 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); - const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); + const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, 1.0f); + const float range = std::clamp(params.Get("range", 1.0f), 0.50f, 1.50f); - return std::make_unique<GCAnalog>(port, axis_x, axis_y, deadzone, adapter.get()); + return std::make_unique<GCAnalog>(port, axis_x, axis_y, deadzone, adapter.get(), range); } void GCAnalogFactory::BeginConfiguration() { @@ -264,7 +265,8 @@ Common::ParamPackage GCAnalogFactory::GetNextInput() { if (analog_x_axis == -1) { analog_x_axis = axis; controller_number = static_cast<int>(port); - } else if (analog_y_axis == -1 && analog_x_axis != axis && controller_number == port) { + } else if (analog_y_axis == -1 && analog_x_axis != axis && + controller_number == static_cast<int>(port)) { analog_y_axis = axis; } } diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index b9d5d0ec3..3d97d95f7 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -11,6 +11,9 @@ #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/motion_emu.h" +#include "input_common/motion_from_button.h" +#include "input_common/touch_from_button.h" +#include "input_common/udp/client.h" #include "input_common/udp/udp.h" #ifdef HAVE_SDL2 #include "input_common/sdl/sdl.h" @@ -18,67 +21,227 @@ namespace InputCommon { -static std::shared_ptr<Keyboard> keyboard; -static std::shared_ptr<MotionEmu> motion_emu; +struct InputSubsystem::Impl { + void Initialize() { + gcadapter = std::make_shared<GCAdapter::Adapter>(); + gcbuttons = std::make_shared<GCButtonFactory>(gcadapter); + Input::RegisterFactory<Input::ButtonDevice>("gcpad", gcbuttons); + gcanalog = std::make_shared<GCAnalogFactory>(gcadapter); + Input::RegisterFactory<Input::AnalogDevice>("gcpad", gcanalog); + + keyboard = std::make_shared<Keyboard>(); + Input::RegisterFactory<Input::ButtonDevice>("keyboard", keyboard); + Input::RegisterFactory<Input::AnalogDevice>("analog_from_button", + std::make_shared<AnalogFromButton>()); + Input::RegisterFactory<Input::MotionDevice>("keyboard", + std::make_shared<MotionFromButton>()); + motion_emu = std::make_shared<MotionEmu>(); + Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu); + Input::RegisterFactory<Input::TouchDevice>("touch_from_button", + std::make_shared<TouchFromButtonFactory>()); + #ifdef HAVE_SDL2 -static std::unique_ptr<SDL::State> sdl; + sdl = SDL::Init(); #endif -static std::unique_ptr<CemuhookUDP::State> udp; -static std::shared_ptr<GCButtonFactory> gcbuttons; -static std::shared_ptr<GCAnalogFactory> gcanalog; - -void Init() { - auto gcadapter = std::make_shared<GCAdapter::Adapter>(); - gcbuttons = std::make_shared<GCButtonFactory>(gcadapter); - Input::RegisterFactory<Input::ButtonDevice>("gcpad", gcbuttons); - gcanalog = std::make_shared<GCAnalogFactory>(gcadapter); - Input::RegisterFactory<Input::AnalogDevice>("gcpad", gcanalog); - - keyboard = std::make_shared<Keyboard>(); - Input::RegisterFactory<Input::ButtonDevice>("keyboard", keyboard); - Input::RegisterFactory<Input::AnalogDevice>("analog_from_button", - std::make_shared<AnalogFromButton>()); - motion_emu = std::make_shared<MotionEmu>(); - Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu); + udp = std::make_shared<InputCommon::CemuhookUDP::Client>(); + udpmotion = std::make_shared<UDPMotionFactory>(udp); + Input::RegisterFactory<Input::MotionDevice>("cemuhookudp", udpmotion); + udptouch = std::make_shared<UDPTouchFactory>(udp); + Input::RegisterFactory<Input::TouchDevice>("cemuhookudp", udptouch); + } + + void Shutdown() { + Input::UnregisterFactory<Input::ButtonDevice>("keyboard"); + Input::UnregisterFactory<Input::MotionDevice>("keyboard"); + keyboard.reset(); + Input::UnregisterFactory<Input::AnalogDevice>("analog_from_button"); + Input::UnregisterFactory<Input::MotionDevice>("motion_emu"); + motion_emu.reset(); + Input::UnregisterFactory<Input::TouchDevice>("touch_from_button"); #ifdef HAVE_SDL2 - sdl = SDL::Init(); + sdl.reset(); #endif + Input::UnregisterFactory<Input::ButtonDevice>("gcpad"); + Input::UnregisterFactory<Input::AnalogDevice>("gcpad"); - udp = CemuhookUDP::Init(); -} + gcbuttons.reset(); + gcanalog.reset(); + + Input::UnregisterFactory<Input::MotionDevice>("cemuhookudp"); + Input::UnregisterFactory<Input::TouchDevice>("cemuhookudp"); + + udpmotion.reset(); + udptouch.reset(); + } + + [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const { + std::vector<Common::ParamPackage> devices = { + Common::ParamPackage{{"display", "Any"}, {"class", "any"}}, + Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "key"}}, + }; +#ifdef HAVE_SDL2 + auto sdl_devices = sdl->GetInputDevices(); + devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end()); +#endif + auto udp_devices = udp->GetInputDevices(); + devices.insert(devices.end(), udp_devices.begin(), udp_devices.end()); + auto gcpad_devices = gcadapter->GetInputDevices(); + devices.insert(devices.end(), gcpad_devices.begin(), gcpad_devices.end()); + return devices; + } + + [[nodiscard]] AnalogMapping GetAnalogMappingForDevice( + const Common::ParamPackage& params) const { + if (!params.Has("class") || params.Get("class", "") == "any") { + return {}; + } + if (params.Get("class", "") == "key") { + // TODO consider returning the SDL key codes for the default keybindings + return {}; + } + if (params.Get("class", "") == "gcpad") { + return gcadapter->GetAnalogMappingForDevice(params); + } +#ifdef HAVE_SDL2 + if (params.Get("class", "") == "sdl") { + return sdl->GetAnalogMappingForDevice(params); + } +#endif + return {}; + } + + [[nodiscard]] ButtonMapping GetButtonMappingForDevice( + const Common::ParamPackage& params) const { + if (!params.Has("class") || params.Get("class", "") == "any") { + return {}; + } + if (params.Get("class", "") == "key") { + // TODO consider returning the SDL key codes for the default keybindings + return {}; + } + if (params.Get("class", "") == "gcpad") { + return gcadapter->GetButtonMappingForDevice(params); + } +#ifdef HAVE_SDL2 + if (params.Get("class", "") == "sdl") { + return sdl->GetButtonMappingForDevice(params); + } +#endif + return {}; + } + + [[nodiscard]] MotionMapping GetMotionMappingForDevice( + const Common::ParamPackage& params) const { + if (!params.Has("class") || params.Get("class", "") == "any") { + return {}; + } + if (params.Get("class", "") == "cemuhookudp") { + // TODO return the correct motion device + return {}; + } + return {}; + } -void Shutdown() { - Input::UnregisterFactory<Input::ButtonDevice>("keyboard"); - keyboard.reset(); - Input::UnregisterFactory<Input::AnalogDevice>("analog_from_button"); - Input::UnregisterFactory<Input::MotionDevice>("motion_emu"); - motion_emu.reset(); + std::shared_ptr<Keyboard> keyboard; + std::shared_ptr<MotionEmu> motion_emu; #ifdef HAVE_SDL2 - sdl.reset(); + std::unique_ptr<SDL::State> sdl; #endif - udp.reset(); - Input::UnregisterFactory<Input::ButtonDevice>("gcpad"); - Input::UnregisterFactory<Input::AnalogDevice>("gcpad"); + std::shared_ptr<GCButtonFactory> gcbuttons; + std::shared_ptr<GCAnalogFactory> gcanalog; + std::shared_ptr<UDPMotionFactory> udpmotion; + std::shared_ptr<UDPTouchFactory> udptouch; + std::shared_ptr<CemuhookUDP::Client> udp; + std::shared_ptr<GCAdapter::Adapter> gcadapter; +}; + +InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} + +InputSubsystem::~InputSubsystem() = default; + +void InputSubsystem::Initialize() { + impl->Initialize(); +} + +void InputSubsystem::Shutdown() { + impl->Shutdown(); +} + +Keyboard* InputSubsystem::GetKeyboard() { + return impl->keyboard.get(); +} + +const Keyboard* InputSubsystem::GetKeyboard() const { + return impl->keyboard.get(); +} + +MotionEmu* InputSubsystem::GetMotionEmu() { + return impl->motion_emu.get(); +} - gcbuttons.reset(); - gcanalog.reset(); +const MotionEmu* InputSubsystem::GetMotionEmu() const { + return impl->motion_emu.get(); } -Keyboard* GetKeyboard() { - return keyboard.get(); +std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const { + return impl->GetInputDevices(); } -MotionEmu* GetMotionEmu() { - return motion_emu.get(); +AnalogMapping InputSubsystem::GetAnalogMappingForDevice(const Common::ParamPackage& device) const { + return impl->GetAnalogMappingForDevice(device); } -GCButtonFactory* GetGCButtons() { - return gcbuttons.get(); +ButtonMapping InputSubsystem::GetButtonMappingForDevice(const Common::ParamPackage& device) const { + return impl->GetButtonMappingForDevice(device); } -GCAnalogFactory* GetGCAnalogs() { - return gcanalog.get(); +GCAnalogFactory* InputSubsystem::GetGCAnalogs() { + return impl->gcanalog.get(); +} + +const GCAnalogFactory* InputSubsystem::GetGCAnalogs() const { + return impl->gcanalog.get(); +} + +GCButtonFactory* InputSubsystem::GetGCButtons() { + return impl->gcbuttons.get(); +} + +const GCButtonFactory* InputSubsystem::GetGCButtons() const { + return impl->gcbuttons.get(); +} + +UDPMotionFactory* InputSubsystem::GetUDPMotions() { + return impl->udpmotion.get(); +} + +const UDPMotionFactory* InputSubsystem::GetUDPMotions() const { + return impl->udpmotion.get(); +} + +UDPTouchFactory* InputSubsystem::GetUDPTouch() { + return impl->udptouch.get(); +} + +const UDPTouchFactory* InputSubsystem::GetUDPTouch() const { + return impl->udptouch.get(); +} + +void InputSubsystem::ReloadInputDevices() { + if (!impl->udp) { + return; + } + impl->udp->ReloadUDPClient(); +} + +std::vector<std::unique_ptr<Polling::DevicePoller>> InputSubsystem::GetPollers( + Polling::DeviceType type) const { +#ifdef HAVE_SDL2 + return impl->sdl->GetPollers(type); +#else + return {}; +#endif } std::string GenerateKeyboardParam(int key_code) { @@ -102,18 +265,4 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, }; return circle_pad_param.Serialize(); } - -namespace Polling { - -std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) { - std::vector<std::unique_ptr<DevicePoller>> pollers; - -#ifdef HAVE_SDL2 - pollers = sdl->GetPollers(type); -#endif - - return pollers; -} - -} // namespace Polling } // namespace InputCommon diff --git a/src/input_common/main.h b/src/input_common/main.h index 0e32856f6..dded3f1ef 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -6,45 +6,29 @@ #include <memory> #include <string> +#include <unordered_map> #include <vector> -#include "input_common/gcadapter/gc_poller.h" namespace Common { class ParamPackage; } -namespace InputCommon { - -/// Initializes and registers all built-in input device factories. -void Init(); - -/// Deregisters all built-in input device factories and shuts them down. -void Shutdown(); - -class Keyboard; - -/// Gets the keyboard button device factory. -Keyboard* GetKeyboard(); - -class MotionEmu; - -/// Gets the motion emulation factory. -MotionEmu* GetMotionEmu(); - -GCButtonFactory* GetGCButtons(); - -GCAnalogFactory* GetGCAnalogs(); +namespace Settings::NativeAnalog { +enum Values : int; +} -/// Generates a serialized param package for creating a keyboard button device -std::string GenerateKeyboardParam(int key_code); +namespace Settings::NativeButton { +enum Values : int; +} -/// Generates a serialized param package for creating an analog device taking input from keyboard -std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right, - int key_modifier, float modifier_scale); +namespace Settings::NativeMotion { +enum Values : int; +} +namespace InputCommon { namespace Polling { -enum class DeviceType { Button, Analog }; +enum class DeviceType { Button, AnalogPreferred, Motion }; /** * A class that can be used to get inputs from an input device like controllers without having to @@ -54,7 +38,9 @@ class DevicePoller { public: virtual ~DevicePoller() = default; /// Setup and start polling for inputs, should be called before GetNextInput - virtual void Start() = 0; + /// If a device_id is provided, events should be filtered to only include events from this + /// device id + virtual void Start(const std::string& device_id = "") = 0; /// Stop polling virtual void Stop() = 0; /** @@ -64,8 +50,110 @@ public: */ virtual Common::ParamPackage GetNextInput() = 0; }; - -// Get all DevicePoller from all backends for a specific device type -std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type); } // namespace Polling + +class GCAnalogFactory; +class GCButtonFactory; +class UDPMotionFactory; +class UDPTouchFactory; +class Keyboard; +class MotionEmu; + +/** + * Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default + * mapping for the device. This is currently only implemented for the SDL backend devices. + */ +using AnalogMapping = std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>; +using ButtonMapping = std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>; +using MotionMapping = std::unordered_map<Settings::NativeMotion::Values, Common::ParamPackage>; + +class InputSubsystem { +public: + explicit InputSubsystem(); + ~InputSubsystem(); + + InputSubsystem(const InputSubsystem&) = delete; + InputSubsystem& operator=(const InputSubsystem&) = delete; + + InputSubsystem(InputSubsystem&&) = delete; + InputSubsystem& operator=(InputSubsystem&&) = delete; + + /// Initializes and registers all built-in input device factories. + void Initialize(); + + /// Unregisters all built-in input device factories and shuts them down. + void Shutdown(); + + /// Retrieves the underlying keyboard device. + [[nodiscard]] Keyboard* GetKeyboard(); + + /// Retrieves the underlying keyboard device. + [[nodiscard]] const Keyboard* GetKeyboard() const; + + /// Retrieves the underlying motion emulation factory. + [[nodiscard]] MotionEmu* GetMotionEmu(); + + /// Retrieves the underlying motion emulation factory. + [[nodiscard]] const MotionEmu* GetMotionEmu() const; + + /** + * Returns all available input devices that this Factory can create a new device with. + * Each returned ParamPackage should have a `display` field used for display, a class field for + * backends to determine if this backend is meant to service the request and any other + * information needed to identify this in the backend later. + */ + [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const; + + /// Retrieves the analog mappings for the given device. + [[nodiscard]] AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& device) const; + + /// Retrieves the button mappings for the given device. + [[nodiscard]] ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& device) const; + + /// Retrieves the motion mappings for the given device. + [[nodiscard]] MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& device) const; + + /// Retrieves the underlying GameCube analog handler. + [[nodiscard]] GCAnalogFactory* GetGCAnalogs(); + + /// Retrieves the underlying GameCube analog handler. + [[nodiscard]] const GCAnalogFactory* GetGCAnalogs() const; + + /// Retrieves the underlying GameCube button handler. + [[nodiscard]] GCButtonFactory* GetGCButtons(); + + /// Retrieves the underlying GameCube button handler. + [[nodiscard]] const GCButtonFactory* GetGCButtons() const; + + /// Retrieves the underlying udp motion handler. + [[nodiscard]] UDPMotionFactory* GetUDPMotions(); + + /// Retrieves the underlying udp motion handler. + [[nodiscard]] const UDPMotionFactory* GetUDPMotions() const; + + /// Retrieves the underlying udp touch handler. + [[nodiscard]] UDPTouchFactory* GetUDPTouch(); + + /// Retrieves the underlying udp touch handler. + [[nodiscard]] const UDPTouchFactory* GetUDPTouch() const; + + /// Reloads the input devices + void ReloadInputDevices(); + + /// Get all DevicePoller from all backends for a specific device type + [[nodiscard]] std::vector<std::unique_ptr<Polling::DevicePoller>> GetPollers( + Polling::DeviceType type) const; + +private: + struct Impl; + std::unique_ptr<Impl> impl; +}; + +/// Generates a serialized param package for creating a keyboard button device +std::string GenerateKeyboardParam(int key_code); + +/// Generates a serialized param package for creating an analog device taking input from keyboard +std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right, + int key_modifier, float modifier_scale); + } // namespace InputCommon diff --git a/src/input_common/motion_emu.cpp b/src/input_common/motion_emu.cpp index d4cdf76a3..69fd3c1d2 100644 --- a/src/input_common/motion_emu.cpp +++ b/src/input_common/motion_emu.cpp @@ -56,7 +56,7 @@ public: is_tilting = false; } - std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() { + Input::MotionStatus GetStatus() { std::lock_guard guard{status_mutex}; return status; } @@ -76,7 +76,7 @@ private: Common::Event shutdown_event; - std::tuple<Common::Vec3<float>, Common::Vec3<float>> status; + Input::MotionStatus status; std::mutex status_mutex; // Note: always keep the thread declaration at the end so that other objects are initialized @@ -113,10 +113,19 @@ private: gravity = QuaternionRotate(inv_q, gravity); angular_rate = QuaternionRotate(inv_q, angular_rate); + // TODO: Calculate the correct rotation vector and orientation matrix + const auto matrix4x4 = q.ToMatrix(); + const auto rotation = Common::MakeVec(0.0f, 0.0f, 0.0f); + const std::array orientation{ + Common::Vec3f(matrix4x4[0], matrix4x4[1], -matrix4x4[2]), + Common::Vec3f(matrix4x4[4], matrix4x4[5], -matrix4x4[6]), + Common::Vec3f(-matrix4x4[8], -matrix4x4[9], matrix4x4[10]), + }; + // Update the sensor state { std::lock_guard guard{status_mutex}; - status = std::make_tuple(gravity, angular_rate); + status = std::make_tuple(gravity, angular_rate, rotation, orientation); } } } @@ -131,7 +140,7 @@ public: device = std::make_shared<MotionEmuDevice>(update_millisecond, sensitivity); } - std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const override { + Input::MotionStatus GetStatus() const override { return device->GetStatus(); } diff --git a/src/input_common/motion_from_button.cpp b/src/input_common/motion_from_button.cpp new file mode 100644 index 000000000..9d459f963 --- /dev/null +++ b/src/input_common/motion_from_button.cpp @@ -0,0 +1,34 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "input_common/motion_from_button.h" +#include "input_common/motion_input.h" + +namespace InputCommon { + +class MotionKey final : public Input::MotionDevice { +public: + using Button = std::unique_ptr<Input::ButtonDevice>; + + MotionKey(Button key_) : key(std::move(key_)) {} + + Input::MotionStatus GetStatus() const override { + + if (key->GetStatus()) { + return motion.GetRandomMotion(2, 6); + } + return motion.GetRandomMotion(0, 0); + } + +private: + Button key; + InputCommon::MotionInput motion{0.0f, 0.0f, 0.0f}; +}; + +std::unique_ptr<Input::MotionDevice> MotionFromButton::Create(const Common::ParamPackage& params) { + auto key = Input::CreateDevice<Input::ButtonDevice>(params.Serialize()); + return std::make_unique<MotionKey>(std::move(key)); +} + +} // namespace InputCommon diff --git a/src/input_common/motion_from_button.h b/src/input_common/motion_from_button.h new file mode 100644 index 000000000..a959046fb --- /dev/null +++ b/src/input_common/motion_from_button.h @@ -0,0 +1,25 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/input.h" + +namespace InputCommon { + +/** + * An motion device factory that takes a keyboard button and uses it as a random + * motion device. + */ +class MotionFromButton final : public Input::Factory<Input::MotionDevice> { +public: + /** + * Creates an motion device from button devices + * @param params contains parameters for creating the device: + * - "key": a serialized ParamPackage for creating a button device + */ + std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override; +}; + +} // namespace InputCommon diff --git a/src/input_common/motion_input.cpp b/src/input_common/motion_input.cpp new file mode 100644 index 000000000..e89019723 --- /dev/null +++ b/src/input_common/motion_input.cpp @@ -0,0 +1,305 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included + +#include <random> +#include "common/math_util.h" +#include "input_common/motion_input.h" + +namespace InputCommon { + +MotionInput::MotionInput(f32 new_kp, f32 new_ki, f32 new_kd) + : kp(new_kp), ki(new_ki), kd(new_kd), quat{{0, 0, -1}, 0} {} + +void MotionInput::SetAcceleration(const Common::Vec3f& acceleration) { + accel = acceleration; +} + +void MotionInput::SetGyroscope(const Common::Vec3f& gyroscope) { + gyro = gyroscope - gyro_drift; + + // Auto adjust drift to minimize drift + if (!IsMoving(0.1f)) { + gyro_drift = (gyro_drift * 0.9999f) + (gyroscope * 0.0001f); + } + + if (gyro.Length2() < gyro_threshold) { + gyro = {}; + } else { + only_accelerometer = false; + } +} + +void MotionInput::SetQuaternion(const Common::Quaternion<f32>& quaternion) { + quat = quaternion; +} + +void MotionInput::SetGyroDrift(const Common::Vec3f& drift) { + gyro_drift = drift; +} + +void MotionInput::SetGyroThreshold(f32 threshold) { + gyro_threshold = threshold; +} + +void MotionInput::EnableReset(bool reset) { + reset_enabled = reset; +} + +void MotionInput::ResetRotations() { + rotations = {}; +} + +bool MotionInput::IsMoving(f32 sensitivity) const { + return gyro.Length() >= sensitivity || accel.Length() <= 0.9f || accel.Length() >= 1.1f; +} + +bool MotionInput::IsCalibrated(f32 sensitivity) const { + return real_error.Length() < sensitivity; +} + +void MotionInput::UpdateRotation(u64 elapsed_time) { + const f32 sample_period = elapsed_time / 1000000.0f; + if (sample_period > 0.1f) { + return; + } + rotations += gyro * sample_period; +} + +void MotionInput::UpdateOrientation(u64 elapsed_time) { + if (!IsCalibrated(0.1f)) { + ResetOrientation(); + } + // Short name local variable for readability + f32 q1 = quat.w; + f32 q2 = quat.xyz[0]; + f32 q3 = quat.xyz[1]; + f32 q4 = quat.xyz[2]; + const f32 sample_period = elapsed_time / 1000000.0f; + + // Ignore invalid elapsed time + if (sample_period > 0.1f) { + return; + } + + const auto normal_accel = accel.Normalized(); + auto rad_gyro = gyro * Common::PI * 2; + const f32 swap = rad_gyro.x; + rad_gyro.x = rad_gyro.y; + rad_gyro.y = -swap; + rad_gyro.z = -rad_gyro.z; + + // Clear gyro values if there is no gyro present + if (only_accelerometer) { + rad_gyro.x = 0; + rad_gyro.y = 0; + rad_gyro.z = 0; + } + + // Ignore drift correction if acceleration is not reliable + if (accel.Length() >= 0.75f && accel.Length() <= 1.25f) { + const f32 ax = -normal_accel.x; + const f32 ay = normal_accel.y; + const f32 az = -normal_accel.z; + + // Estimated direction of gravity + const f32 vx = 2.0f * (q2 * q4 - q1 * q3); + const f32 vy = 2.0f * (q1 * q2 + q3 * q4); + const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4; + + // Error is cross product between estimated direction and measured direction of gravity + const Common::Vec3f new_real_error = { + az * vx - ax * vz, + ay * vz - az * vy, + ax * vy - ay * vx, + }; + + derivative_error = new_real_error - real_error; + real_error = new_real_error; + + // Prevent integral windup + if (ki != 0.0f && !IsCalibrated(0.05f)) { + integral_error += real_error; + } else { + integral_error = {}; + } + + // Apply feedback terms + if (!only_accelerometer) { + rad_gyro += kp * real_error; + rad_gyro += ki * integral_error; + rad_gyro += kd * derivative_error; + } else { + // Give more weight to acelerometer values to compensate for the lack of gyro + rad_gyro += 35.0f * kp * real_error; + rad_gyro += 10.0f * ki * integral_error; + rad_gyro += 10.0f * kd * derivative_error; + + // Emulate gyro values for games that need them + gyro.x = -rad_gyro.y; + gyro.y = rad_gyro.x; + gyro.z = -rad_gyro.z; + UpdateRotation(elapsed_time); + } + } + + const f32 gx = rad_gyro.y; + const f32 gy = rad_gyro.x; + const f32 gz = rad_gyro.z; + + // Integrate rate of change of quaternion + const f32 pa = q2; + const f32 pb = q3; + const f32 pc = q4; + q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period); + q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period); + q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period); + q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period); + + quat.w = q1; + quat.xyz[0] = q2; + quat.xyz[1] = q3; + quat.xyz[2] = q4; + quat = quat.Normalized(); +} + +std::array<Common::Vec3f, 3> MotionInput::GetOrientation() const { + const Common::Quaternion<float> quad{ + .xyz = {-quat.xyz[1], -quat.xyz[0], -quat.w}, + .w = -quat.xyz[2], + }; + const std::array<float, 16> matrix4x4 = quad.ToMatrix(); + + return {Common::Vec3f(matrix4x4[0], matrix4x4[1], -matrix4x4[2]), + Common::Vec3f(matrix4x4[4], matrix4x4[5], -matrix4x4[6]), + Common::Vec3f(-matrix4x4[8], -matrix4x4[9], matrix4x4[10])}; +} + +Common::Vec3f MotionInput::GetAcceleration() const { + return accel; +} + +Common::Vec3f MotionInput::GetGyroscope() const { + return gyro; +} + +Common::Quaternion<f32> MotionInput::GetQuaternion() const { + return quat; +} + +Common::Vec3f MotionInput::GetRotations() const { + return rotations; +} + +Input::MotionStatus MotionInput::GetMotion() const { + const Common::Vec3f gyroscope = GetGyroscope(); + const Common::Vec3f accelerometer = GetAcceleration(); + const Common::Vec3f rotation = GetRotations(); + const std::array<Common::Vec3f, 3> orientation = GetOrientation(); + return {accelerometer, gyroscope, rotation, orientation}; +} + +Input::MotionStatus MotionInput::GetRandomMotion(int accel_magnitude, int gyro_magnitude) const { + std::random_device device; + std::mt19937 gen(device()); + std::uniform_int_distribution<s16> distribution(-1000, 1000); + const Common::Vec3f gyroscope = { + distribution(gen) * 0.001f, + distribution(gen) * 0.001f, + distribution(gen) * 0.001f, + }; + const Common::Vec3f accelerometer = { + distribution(gen) * 0.001f, + distribution(gen) * 0.001f, + distribution(gen) * 0.001f, + }; + const Common::Vec3f rotation = {}; + const std::array<Common::Vec3f, 3> orientation = { + Common::Vec3f{1.0f, 0, 0}, + Common::Vec3f{0, 1.0f, 0}, + Common::Vec3f{0, 0, 1.0f}, + }; + return {accelerometer * accel_magnitude, gyroscope * gyro_magnitude, rotation, orientation}; +} + +void MotionInput::ResetOrientation() { + if (!reset_enabled || only_accelerometer) { + return; + } + if (!IsMoving(0.5f) && accel.z <= -0.9f) { + ++reset_counter; + if (reset_counter > 900) { + quat.w = 0; + quat.xyz[0] = 0; + quat.xyz[1] = 0; + quat.xyz[2] = -1; + SetOrientationFromAccelerometer(); + integral_error = {}; + reset_counter = 0; + } + } else { + reset_counter = 0; + } +} + +void MotionInput::SetOrientationFromAccelerometer() { + int iterations = 0; + const f32 sample_period = 0.015f; + + const auto normal_accel = accel.Normalized(); + const f32 ax = -normal_accel.x; + const f32 ay = normal_accel.y; + const f32 az = -normal_accel.z; + + while (!IsCalibrated(0.01f) && ++iterations < 100) { + // Short name local variable for readability + f32 q1 = quat.w; + f32 q2 = quat.xyz[0]; + f32 q3 = quat.xyz[1]; + f32 q4 = quat.xyz[2]; + + Common::Vec3f rad_gyro = {}; + const f32 ax = -normal_accel.x; + const f32 ay = normal_accel.y; + const f32 az = -normal_accel.z; + + // Estimated direction of gravity + const f32 vx = 2.0f * (q2 * q4 - q1 * q3); + const f32 vy = 2.0f * (q1 * q2 + q3 * q4); + const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4; + + // Error is cross product between estimated direction and measured direction of gravity + const Common::Vec3f new_real_error = { + az * vx - ax * vz, + ay * vz - az * vy, + ax * vy - ay * vx, + }; + + derivative_error = new_real_error - real_error; + real_error = new_real_error; + + rad_gyro += 10.0f * kp * real_error; + rad_gyro += 5.0f * ki * integral_error; + rad_gyro += 10.0f * kd * derivative_error; + + const f32 gx = rad_gyro.y; + const f32 gy = rad_gyro.x; + const f32 gz = rad_gyro.z; + + // Integrate rate of change of quaternion + const f32 pa = q2; + const f32 pb = q3; + const f32 pc = q4; + q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period); + q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period); + q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period); + q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period); + + quat.w = q1; + quat.xyz[0] = q2; + quat.xyz[1] = q3; + quat.xyz[2] = q4; + quat = quat.Normalized(); + } +} +} // namespace InputCommon diff --git a/src/input_common/motion_input.h b/src/input_common/motion_input.h new file mode 100644 index 000000000..6342d0318 --- /dev/null +++ b/src/input_common/motion_input.h @@ -0,0 +1,73 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included + +#pragma once + +#include "common/common_types.h" +#include "common/quaternion.h" +#include "common/vector_math.h" +#include "core/frontend/input.h" + +namespace InputCommon { + +class MotionInput { +public: + MotionInput(f32 new_kp, f32 new_ki, f32 new_kd); + + MotionInput(const MotionInput&) = default; + MotionInput& operator=(const MotionInput&) = default; + + MotionInput(MotionInput&&) = default; + MotionInput& operator=(MotionInput&&) = default; + + void SetAcceleration(const Common::Vec3f& acceleration); + void SetGyroscope(const Common::Vec3f& acceleration); + void SetQuaternion(const Common::Quaternion<f32>& quaternion); + void SetGyroDrift(const Common::Vec3f& drift); + void SetGyroThreshold(f32 threshold); + + void EnableReset(bool reset); + void ResetRotations(); + + void UpdateRotation(u64 elapsed_time); + void UpdateOrientation(u64 elapsed_time); + + std::array<Common::Vec3f, 3> GetOrientation() const; + Common::Vec3f GetAcceleration() const; + Common::Vec3f GetGyroscope() const; + Common::Vec3f GetRotations() const; + Common::Quaternion<f32> GetQuaternion() const; + Input::MotionStatus GetMotion() const; + Input::MotionStatus GetRandomMotion(int accel_magnitude, int gyro_magnitude) const; + + bool IsMoving(f32 sensitivity) const; + bool IsCalibrated(f32 sensitivity) const; + +private: + void ResetOrientation(); + void SetOrientationFromAccelerometer(); + + // PID constants + const f32 kp; + const f32 ki; + const f32 kd; + + // PID errors + Common::Vec3f real_error; + Common::Vec3f integral_error; + Common::Vec3f derivative_error; + + Common::Quaternion<f32> quat; + Common::Vec3f rotations; + Common::Vec3f accel; + Common::Vec3f gyro; + Common::Vec3f gyro_drift; + + f32 gyro_threshold = 0.0f; + u32 reset_counter = 0; + bool reset_enabled = true; + bool only_accelerometer = true; +}; + +} // namespace InputCommon diff --git a/src/input_common/sdl/sdl.h b/src/input_common/sdl/sdl.h index 5306daa70..f3554be9a 100644 --- a/src/input_common/sdl/sdl.h +++ b/src/input_common/sdl/sdl.h @@ -6,6 +6,7 @@ #include <memory> #include <vector> +#include "common/param_package.h" #include "input_common/main.h" namespace InputCommon::Polling { @@ -22,14 +23,24 @@ public: /// Unregisters SDL device factories and shut them down. virtual ~State() = default; - virtual Pollers GetPollers(Polling::DeviceType type) = 0; + virtual Pollers GetPollers(Polling::DeviceType type) { + return {}; + } + + virtual std::vector<Common::ParamPackage> GetInputDevices() { + return {}; + } + + virtual ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage&) { + return {}; + } + virtual AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage&) { + return {}; + } }; class NullState : public State { public: - Pollers GetPollers(Polling::DeviceType type) override { - return {}; - } }; std::unique_ptr<State> Init(); diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 675b477fa..bd480570a 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -3,10 +3,14 @@ // Refer to the license.txt file included. #include <algorithm> +#include <array> #include <atomic> +#include <chrono> #include <cmath> #include <functional> #include <mutex> +#include <optional> +#include <sstream> #include <string> #include <thread> #include <tuple> @@ -15,15 +19,17 @@ #include <vector> #include <SDL.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/motion_input.h" #include "input_common/sdl/sdl_impl.h" +#include "input_common/settings.h" namespace InputCommon::SDL { -static std::string GetGUID(SDL_Joystick* joystick) { +namespace { +std::string GetGUID(SDL_Joystick* joystick) { const SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); char guid_str[33]; SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); @@ -31,7 +37,8 @@ static std::string GetGUID(SDL_Joystick* joystick) { } /// 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); +Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event); +} // Anonymous namespace static int SDLEventWatcher(void* user_data, SDL_Event* event) { auto* const sdl_state = static_cast<SDLState*>(user_data); @@ -48,8 +55,10 @@ static int SDLEventWatcher(void* user_data, SDL_Event* event) { class SDLJoystick { public: - SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick) - : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose} {} + SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick, + SDL_GameController* gamecontroller) + : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose}, + sdl_controller{gamecontroller, &SDL_GameControllerClose} {} void SetButton(int button, bool value) { std::lock_guard lock{mutex}; @@ -66,14 +75,41 @@ public: state.axes.insert_or_assign(axis, value); } - float GetAxis(int axis) const { + float GetAxis(int axis, float range) const { std::lock_guard lock{mutex}; - return state.axes.at(axis) / 32767.0f; + return state.axes.at(axis) / (32767.0f * range); } - std::tuple<float, float> GetAnalog(int axis_x, int axis_y) const { - float x = GetAxis(axis_x); - float y = GetAxis(axis_y); + bool RumblePlay(f32 amp_low, f32 amp_high, int time) { + const u16 raw_amp_low = static_cast<u16>(amp_low * 0xFFFF); + const u16 raw_amp_high = static_cast<u16>(amp_high * 0xFFFF); + // Lower drastically the number of state changes + if (raw_amp_low >> 11 == last_state_rumble_low >> 11 && + raw_amp_high >> 11 == last_state_rumble_high >> 11) { + if (raw_amp_low + raw_amp_high != 0 || + last_state_rumble_low + last_state_rumble_high == 0) { + return false; + } + } + // Don't change state if last vibration was < 20ms + const auto now = std::chrono::system_clock::now(); + if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_vibration) < + std::chrono::milliseconds(20)) { + return raw_amp_low + raw_amp_high == 0; + } + + last_vibration = now; + last_state_rumble_low = raw_amp_low; + last_state_rumble_high = raw_amp_high; + if (sdl_joystick) { + SDL_JoystickRumble(sdl_joystick.get(), raw_amp_low, raw_amp_high, time); + } + return false; + } + + std::tuple<float, float> GetAnalog(int axis_x, int axis_y, float range) const { + float x = GetAxis(axis_x, range); + float y = GetAxis(axis_y, range); y = -y; // 3DS uses an y-axis inverse from SDL // Make sure the coordinates are in the unit circle, @@ -88,6 +124,10 @@ public: return std::make_tuple(x, y); } + const InputCommon::MotionInput& GetMotion() const { + return motion; + } + void SetHat(int hat, Uint8 direction) { std::lock_guard lock{mutex}; state.hats.insert_or_assign(hat, direction); @@ -115,10 +155,15 @@ public: return sdl_joystick.get(); } - void SetSDLJoystick(SDL_Joystick* joystick) { + void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) { + sdl_controller.reset(controller); sdl_joystick.reset(joystick); } + SDL_GameController* GetSDLGameController() const { + return sdl_controller.get(); + } + private: struct State { std::unordered_map<int, bool> buttons; @@ -127,8 +172,15 @@ private: } state; std::string guid; int port; + u16 last_state_rumble_high; + u16 last_state_rumble_low; + std::chrono::time_point<std::chrono::system_clock> last_vibration; std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)> sdl_joystick; + std::unique_ptr<SDL_GameController, decltype(&SDL_GameControllerClose)> sdl_controller; mutable std::mutex mutex; + + // motion is initalized without PID values as motion input is not aviable for SDL2 + InputCommon::MotionInput motion{0.0f, 0.0f, 0.0f}; }; std::shared_ptr<SDLJoystick> SDLState::GetSDLJoystickByGUID(const std::string& guid, int port) { @@ -136,18 +188,19 @@ std::shared_ptr<SDLJoystick> SDLState::GetSDLJoystickByGUID(const std::string& g const auto it = joystick_map.find(guid); if (it != joystick_map.end()) { while (it->second.size() <= static_cast<std::size_t>(port)) { - auto joystick = - std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()), nullptr); + auto joystick = std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()), + nullptr, nullptr); it->second.emplace_back(std::move(joystick)); } return it->second[port]; } - auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr); + auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr, nullptr); return joystick_map[guid].emplace_back(std::move(joystick)); } std::shared_ptr<SDLJoystick> SDLState::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) { auto sdl_joystick = SDL_JoystickFromInstanceID(sdl_id); + auto sdl_controller = SDL_GameControllerFromInstanceID(sdl_id); const std::string guid = GetGUID(sdl_joystick); std::lock_guard lock{joystick_map_mutex}; @@ -171,32 +224,36 @@ std::shared_ptr<SDLJoystick> SDLState::GetSDLJoystickBySDLID(SDL_JoystickID sdl_ }); if (nullptr_it != map_it->second.end()) { // ... and map it - (*nullptr_it)->SetSDLJoystick(sdl_joystick); + (*nullptr_it)->SetSDLJoystick(sdl_joystick, sdl_controller); return *nullptr_it; } // There is no SDLJoystick without a mapped SDL_Joystick // Create a new SDLJoystick const int port = static_cast<int>(map_it->second.size()); - auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick); + auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_controller); return map_it->second.emplace_back(std::move(joystick)); } - auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick); + auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_controller); return joystick_map[guid].emplace_back(std::move(joystick)); } 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 {}", joystick_index); + LOG_ERROR(Input, "Failed to open joystick {}", joystick_index); 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<SDLJoystick>(guid, 0, sdl_joystick); + auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller); joystick_map[guid].emplace_back(std::move(joystick)); return; } @@ -205,11 +262,11 @@ void SDLState::InitJoystick(int joystick_index) { joystick_guid_list.begin(), joystick_guid_list.end(), [](const std::shared_ptr<SDLJoystick>& joystick) { return !joystick->GetSDLJoystick(); }); if (it != joystick_guid_list.end()) { - (*it)->SetSDLJoystick(sdl_joystick); + (*it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller); return; } const int port = static_cast<int>(joystick_guid_list.size()); - auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick); + auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller); joystick_guid_list.emplace_back(std::move(joystick)); } @@ -231,7 +288,7 @@ void SDLState::CloseJoystick(SDL_Joystick* sdl_joystick) { // Destruct SDL_Joystick outside the lock guard because SDL can internally call the // event callback which locks the mutex again. - joystick->SetSDLJoystick(nullptr); + joystick->SetSDLJoystick(nullptr, nullptr); } void SDLState::HandleGameControllerEvent(const SDL_Event& event) { @@ -285,6 +342,12 @@ public: return joystick->GetButton(button); } + bool SetRumblePlay(f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) const override { + const f32 new_amp_low = pow(amp_low, 0.5f) * (3.0f - 2.0f * pow(amp_low, 0.15f)); + const f32 new_amp_high = pow(amp_high, 0.5f) * (3.0f - 2.0f * pow(amp_high, 0.15f)); + return joystick->RumblePlay(new_amp_low, new_amp_high, 250); + } + private: std::shared_ptr<SDLJoystick> joystick; int button; @@ -313,7 +376,7 @@ public: trigger_if_greater(trigger_if_greater_) {} bool GetStatus() const override { - const float axis_value = joystick->GetAxis(axis); + const float axis_value = joystick->GetAxis(axis, 1.0f); if (trigger_if_greater) { return axis_value > threshold; } @@ -329,22 +392,24 @@ private: class SDLAnalog final : public Input::AnalogDevice { public: - SDLAnalog(std::shared_ptr<SDLJoystick> joystick_, int axis_x_, int axis_y_, float deadzone_) - : joystick(std::move(joystick_)), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_) {} + SDLAnalog(std::shared_ptr<SDLJoystick> joystick_, int axis_x_, int axis_y_, float deadzone_, + float range_) + : joystick(std::move(joystick_)), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), + range(range_) {} std::tuple<float, float> GetStatus() const override { - const auto [x, y] = joystick->GetAnalog(axis_x, axis_y); + const auto [x, y] = joystick->GetAnalog(axis_x, axis_y, range); 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<float, float>(0.0f, 0.0f); + return {}; } bool GetAnalogDirectionStatus(Input::AnalogDirection direction) const override { const auto [x, y] = GetStatus(); - const float directional_deadzone = 0.4f; + const float directional_deadzone = 0.5f; switch (direction) { case Input::AnalogDirection::RIGHT: return x > directional_deadzone; @@ -363,6 +428,69 @@ private: const int axis_x; const int axis_y; const float deadzone; + const float range; +}; + +class SDLDirectionMotion final : public Input::MotionDevice { +public: + explicit SDLDirectionMotion(std::shared_ptr<SDLJoystick> joystick_, int hat_, Uint8 direction_) + : joystick(std::move(joystick_)), hat(hat_), direction(direction_) {} + + Input::MotionStatus GetStatus() const override { + if (joystick->GetHatDirection(hat, direction)) { + return joystick->GetMotion().GetRandomMotion(2, 6); + } + return joystick->GetMotion().GetRandomMotion(0, 0); + } + +private: + std::shared_ptr<SDLJoystick> joystick; + int hat; + Uint8 direction; +}; + +class SDLAxisMotion final : public Input::MotionDevice { +public: + explicit SDLAxisMotion(std::shared_ptr<SDLJoystick> joystick_, int axis_, float threshold_, + bool trigger_if_greater_) + : joystick(std::move(joystick_)), axis(axis_), threshold(threshold_), + trigger_if_greater(trigger_if_greater_) {} + + Input::MotionStatus GetStatus() const override { + const float axis_value = joystick->GetAxis(axis, 1.0f); + bool trigger = axis_value < threshold; + if (trigger_if_greater) { + trigger = axis_value > threshold; + } + + if (trigger) { + return joystick->GetMotion().GetRandomMotion(2, 6); + } + return joystick->GetMotion().GetRandomMotion(0, 0); + } + +private: + std::shared_ptr<SDLJoystick> joystick; + int axis; + float threshold; + bool trigger_if_greater; +}; + +class SDLButtonMotion final : public Input::MotionDevice { +public: + explicit SDLButtonMotion(std::shared_ptr<SDLJoystick> joystick_, int button_) + : joystick(std::move(joystick_)), button(button_) {} + + Input::MotionStatus GetStatus() const override { + if (joystick->GetButton(button)) { + return joystick->GetMotion().GetRandomMotion(2, 6); + } + return joystick->GetMotion().GetRandomMotion(0, 0); + } + +private: + std::shared_ptr<SDLJoystick> joystick; + int button; }; /// A button device factory that creates button devices from SDL joystick @@ -457,14 +585,78 @@ public: 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); - const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); - + const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, 1.0f); + const float range = std::clamp(params.Get("range", 1.0f), 0.50f, 1.50f); 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<SDLAnalog>(joystick, axis_x, axis_y, deadzone); + return std::make_unique<SDLAnalog>(joystick, axis_x, axis_y, deadzone, range); + } + +private: + SDLState& state; +}; + +/// A motion device factory that creates motion devices from SDL joystick +class SDLMotionFactory final : public Input::Factory<Input::MotionDevice> { +public: + explicit SDLMotionFactory(SDLState& state_) : state(state_) {} + /** + * Creates motion 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 + */ + std::unique_ptr<Input::MotionDevice> 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<SDLDirectionMotion>(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<SDLAxisMotion>(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<SDLButtonMotion>(joystick, button); } private: @@ -473,8 +665,12 @@ private: SDLState::SDLState() { using namespace Input; - RegisterFactory<ButtonDevice>("sdl", std::make_shared<SDLButtonFactory>(*this)); - RegisterFactory<AnalogDevice>("sdl", std::make_shared<SDLAnalogFactory>(*this)); + analog_factory = std::make_shared<SDLAnalogFactory>(*this); + button_factory = std::make_shared<SDLButtonFactory>(*this); + motion_factory = std::make_shared<SDLMotionFactory>(*this); + RegisterFactory<AnalogDevice>("sdl", analog_factory); + RegisterFactory<ButtonDevice>("sdl", button_factory); + RegisterFactory<MotionDevice>("sdl", motion_factory); // If the frontend is going to manage the event loop, then we dont start one here start_thread = !SDL_WasInit(SDL_INIT_JOYSTICK); @@ -482,6 +678,7 @@ SDLState::SDLState() { LOG_CRITICAL(Input, "SDL_Init(SDL_INIT_JOYSTICK) failed with: {}", SDL_GetError()); return; } + has_gamecontroller = SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER); if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) { LOG_ERROR(Input, "Failed to set hint for background events with: {}", SDL_GetError()); } @@ -494,7 +691,7 @@ SDLState::SDLState() { using namespace std::chrono_literals; while (initialized) { SDL_PumpEvents(); - std::this_thread::sleep_for(10ms); + std::this_thread::sleep_for(5ms); } }); } @@ -509,6 +706,7 @@ SDLState::~SDLState() { using namespace Input; UnregisterFactory<ButtonDevice>("sdl"); UnregisterFactory<AnalogDevice>("sdl"); + UnregisterFactory<MotionDevice>("sdl"); CloseJoysticks(); SDL_DelEventWatch(&SDLEventWatcher, this); @@ -520,65 +718,251 @@ SDLState::~SDLState() { } } -static Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event) { +std::vector<Common::ParamPackage> SDLState::GetInputDevices() { + std::scoped_lock lock(joystick_map_mutex); + std::vector<Common::ParamPackage> devices; + for (const auto& [key, value] : joystick_map) { + for (const auto& joystick : value) { + auto joy = joystick->GetSDLJoystick(); + if (auto controller = joystick->GetSDLGameController()) { + std::string name = + fmt::format("{} {}", SDL_GameControllerName(controller), joystick->GetPort()); + devices.emplace_back(Common::ParamPackage{ + {"class", "sdl"}, + {"display", std::move(name)}, + {"guid", joystick->GetGUID()}, + {"port", std::to_string(joystick->GetPort())}, + }); + } else if (joy) { + std::string name = fmt::format("{} {}", SDL_JoystickName(joy), joystick->GetPort()); + devices.emplace_back(Common::ParamPackage{ + {"class", "sdl"}, + {"display", std::move(name)}, + {"guid", joystick->GetGUID()}, + {"port", std::to_string(joystick->GetPort())}, + }); + } + } + } + return devices; +} + +namespace { +Common::ParamPackage BuildAnalogParamPackageForButton(int port, std::string guid, u8 axis, + float value = 0.1f) { Common::ParamPackage params({{"engine", "sdl"}}); + params.Set("port", port); + params.Set("guid", std::move(guid)); + params.Set("axis", axis); + if (value > 0) { + params.Set("direction", "+"); + params.Set("threshold", "0.5"); + } else { + params.Set("direction", "-"); + params.Set("threshold", "-0.5"); + } + return params; +} + +Common::ParamPackage BuildButtonParamPackageForButton(int port, std::string guid, u8 button) { + Common::ParamPackage params({{"engine", "sdl"}}); + params.Set("port", port); + params.Set("guid", std::move(guid)); + params.Set("button", button); + return params; +} +Common::ParamPackage BuildHatParamPackageForButton(int port, std::string guid, u8 hat, u8 value) { + Common::ParamPackage params({{"engine", "sdl"}}); + + params.Set("port", port); + params.Set("guid", std::move(guid)); + params.Set("hat", hat); + switch (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 {}; + } + return params; +} + +Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event) { switch (event.type) { case SDL_JOYAXISMOTION: { const 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; + return BuildAnalogParamPackageForButton(joystick->GetPort(), joystick->GetGUID(), + event.jaxis.axis, event.jaxis.value); } case SDL_JOYBUTTONUP: { const auto joystick = state.GetSDLJoystickBySDLID(event.jbutton.which); - params.Set("port", joystick->GetPort()); - params.Set("guid", joystick->GetGUID()); - params.Set("button", event.jbutton.button); - break; + return BuildButtonParamPackageForButton(joystick->GetPort(), joystick->GetGUID(), + event.jbutton.button); } case SDL_JOYHATMOTION: { const 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 BuildHatParamPackageForButton(joystick->GetPort(), joystick->GetGUID(), + event.jhat.hat, event.jhat.value); + } + } + return {}; +} + +Common::ParamPackage SDLEventToMotionParamPackage(SDLState& state, const SDL_Event& event) { + switch (event.type) { + case SDL_JOYAXISMOTION: { + const auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); + return BuildAnalogParamPackageForButton(joystick->GetPort(), joystick->GetGUID(), + event.jaxis.axis, event.jaxis.value); } + case SDL_JOYBUTTONUP: { + const auto joystick = state.GetSDLJoystickBySDLID(event.jbutton.which); + return BuildButtonParamPackageForButton(joystick->GetPort(), joystick->GetGUID(), + event.jbutton.button); } + case SDL_JOYHATMOTION: { + const auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which); + return BuildHatParamPackageForButton(joystick->GetPort(), joystick->GetGUID(), + event.jhat.hat, event.jhat.value); + } + } + return {}; +} + +Common::ParamPackage BuildParamPackageForBinding(int port, const std::string& guid, + const SDL_GameControllerButtonBind& binding) { + switch (binding.bindType) { + case SDL_CONTROLLER_BINDTYPE_AXIS: + return BuildAnalogParamPackageForButton(port, guid, binding.value.axis); + case SDL_CONTROLLER_BINDTYPE_BUTTON: + return BuildButtonParamPackageForButton(port, guid, binding.value.button); + case SDL_CONTROLLER_BINDTYPE_HAT: + return BuildHatParamPackageForButton(port, guid, binding.value.hat.hat, + binding.value.hat.hat_mask); + } + return {}; +} + +Common::ParamPackage BuildParamPackageForAnalog(int port, const std::string& guid, int axis_x, + int axis_y) { + Common::ParamPackage params; + params.Set("engine", "sdl"); + params.Set("port", port); + params.Set("guid", guid); + params.Set("axis_x", axis_x); + params.Set("axis_y", axis_y); return params; } +} // Anonymous namespace -namespace Polling { +ButtonMapping SDLState::GetButtonMappingForDevice(const Common::ParamPackage& params) { + if (!params.Has("guid") || !params.Has("port")) { + return {}; + } + const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0)); + auto* controller = joystick->GetSDLGameController(); + if (controller == nullptr) { + return {}; + } + + // This list is missing ZL/ZR since those are not considered buttons in SDL GameController. + // We will add those afterwards + // This list also excludes Screenshot since theres not really a mapping for that + using ButtonBindings = + std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 17>; + static constexpr ButtonBindings switch_to_sdl_button{{ + {Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_B}, + {Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_A}, + {Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_Y}, + {Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_X}, + {Settings::NativeButton::LStick, SDL_CONTROLLER_BUTTON_LEFTSTICK}, + {Settings::NativeButton::RStick, SDL_CONTROLLER_BUTTON_RIGHTSTICK}, + {Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER}, + {Settings::NativeButton::R, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER}, + {Settings::NativeButton::Plus, SDL_CONTROLLER_BUTTON_START}, + {Settings::NativeButton::Minus, SDL_CONTROLLER_BUTTON_BACK}, + {Settings::NativeButton::DLeft, SDL_CONTROLLER_BUTTON_DPAD_LEFT}, + {Settings::NativeButton::DUp, SDL_CONTROLLER_BUTTON_DPAD_UP}, + {Settings::NativeButton::DRight, SDL_CONTROLLER_BUTTON_DPAD_RIGHT}, + {Settings::NativeButton::DDown, SDL_CONTROLLER_BUTTON_DPAD_DOWN}, + {Settings::NativeButton::SL, SDL_CONTROLLER_BUTTON_LEFTSHOULDER}, + {Settings::NativeButton::SR, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER}, + {Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE}, + }}; + + // Add the missing bindings for ZL/ZR + using ZBindings = + std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>; + static constexpr ZBindings switch_to_sdl_axis{{ + {Settings::NativeButton::ZL, SDL_CONTROLLER_AXIS_TRIGGERLEFT}, + {Settings::NativeButton::ZR, SDL_CONTROLLER_AXIS_TRIGGERRIGHT}, + }}; + + ButtonMapping mapping; + mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size()); + + for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) { + const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button); + mapping.insert_or_assign( + switch_button, + BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding)); + } + for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) { + const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis); + mapping.insert_or_assign( + switch_button, + BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding)); + } + + return mapping; +} + +AnalogMapping SDLState::GetAnalogMappingForDevice(const Common::ParamPackage& params) { + if (!params.Has("guid") || !params.Has("port")) { + return {}; + } + const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0)); + auto* controller = joystick->GetSDLGameController(); + if (controller == nullptr) { + return {}; + } + + AnalogMapping mapping = {}; + const auto& binding_left_x = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); + const auto& binding_left_y = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); + mapping.insert_or_assign(Settings::NativeAnalog::LStick, + BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(), + binding_left_x.value.axis, + binding_left_y.value.axis)); + const auto& binding_right_x = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX); + const auto& binding_right_y = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY); + mapping.insert_or_assign(Settings::NativeAnalog::RStick, + BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(), + binding_right_x.value.axis, + binding_right_y.value.axis)); + return mapping; +} +namespace Polling { class SDLPoller : public InputCommon::Polling::DevicePoller { public: explicit SDLPoller(SDLState& state_) : state(state_) {} - void Start() override { + void Start(const std::string& device_id) override { state.event_queue.Clear(); state.polling = true; } @@ -598,71 +982,135 @@ public: Common::ParamPackage GetNextInput() override { SDL_Event event; while (state.event_queue.Pop(event)) { - switch (event.type) { - case SDL_JOYAXISMOTION: - if (std::abs(event.jaxis.value / 32767.0) < 0.5) { - break; - } - [[fallthrough]]; - case SDL_JOYBUTTONUP: - case SDL_JOYHATMOTION: - return SDLEventToButtonParamPackage(state, event); + const auto package = FromEvent(event); + if (package) { + return *package; } } return {}; } + [[nodiscard]] std::optional<Common::ParamPackage> FromEvent(const SDL_Event& event) const { + switch (event.type) { + case SDL_JOYAXISMOTION: + if (std::abs(event.jaxis.value / 32767.0) < 0.5) { + break; + } + [[fallthrough]]; + case SDL_JOYBUTTONUP: + case SDL_JOYHATMOTION: + return {SDLEventToButtonParamPackage(state, event)}; + } + return std::nullopt; + } }; -class SDLAnalogPoller final : public SDLPoller { +class SDLMotionPoller final : public SDLPoller { public: - explicit SDLAnalogPoller(SDLState& state_) : SDLPoller(state_) {} + explicit SDLMotionPoller(SDLState& state_) : SDLPoller(state_) {} - void Start() override { - SDLPoller::Start(); + Common::ParamPackage GetNextInput() override { + SDL_Event event; + while (state.event_queue.Pop(event)) { + const auto package = FromEvent(event); + if (package) { + return *package; + } + } + return {}; + } + [[nodiscard]] std::optional<Common::ParamPackage> FromEvent(const SDL_Event& event) const { + switch (event.type) { + case SDL_JOYAXISMOTION: + if (std::abs(event.jaxis.value / 32767.0) < 0.5) { + break; + } + [[fallthrough]]; + case SDL_JOYBUTTONUP: + case SDL_JOYHATMOTION: + return {SDLEventToMotionParamPackage(state, event)}; + } + return std::nullopt; + } +}; + +/** + * Attempts to match the press to a controller joy axis (left/right stick) and if a match + * isn't found, checks if the event matches anything from SDLButtonPoller and uses that + * instead + */ +class SDLAnalogPreferredPoller final : public SDLPoller { +public: + explicit SDLAnalogPreferredPoller(SDLState& state_) + : SDLPoller(state_), button_poller(state_) {} + void Start(const std::string& device_id) override { + SDLPoller::Start(device_id); + // Load the game controller // Reset stored axes analog_x_axis = -1; analog_y_axis = -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) { + // Filter out axis events that are below a threshold + 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. - const int axis = event.jaxis.axis; - if (analog_x_axis == -1) { - analog_x_axis = axis; - analog_axes_joystick = event.jaxis.which; - } else if (analog_y_axis == -1 && analog_x_axis != axis && - analog_axes_joystick == event.jaxis.which) { - analog_y_axis = axis; + // Simplify controller config by testing if game controller support is enabled. + if (event.type == SDL_JOYAXISMOTION) { + const auto axis = event.jaxis.axis; + const auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); + const auto controller = joystick->GetSDLGameController(); + if (controller) { + const auto axis_left_x = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX) + .value.axis; + const auto axis_left_y = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY) + .value.axis; + const auto axis_right_x = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX) + .value.axis; + const auto axis_right_y = + SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY) + .value.axis; + + if (axis == axis_left_x || axis == axis_left_y) { + analog_x_axis = axis_left_x; + analog_y_axis = axis_left_y; + break; + } else if (axis == axis_right_x || axis == axis_right_y) { + analog_x_axis = axis_right_x; + analog_y_axis = axis_right_y; + break; + } + } + } + + // If the press wasn't accepted as a joy axis, check for a button press + auto button_press = button_poller.FromEvent(event); + if (button_press) { + return *button_press; } } - Common::ParamPackage params; + if (analog_x_axis != -1 && analog_y_axis != -1) { const 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_x_axis); - params.Set("axis_y", analog_y_axis); + auto params = BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(), + analog_x_axis, analog_y_axis); analog_x_axis = -1; analog_y_axis = -1; - analog_axes_joystick = -1; return params; } - return params; + return {}; } private: int analog_x_axis = -1; int analog_y_axis = -1; - SDL_JoystickID analog_axes_joystick = -1; + SDLButtonPoller button_poller; }; } // namespace Polling @@ -670,12 +1118,15 @@ SDLState::Pollers SDLState::GetPollers(InputCommon::Polling::DeviceType type) { Pollers pollers; switch (type) { - case InputCommon::Polling::DeviceType::Analog: - pollers.emplace_back(std::make_unique<Polling::SDLAnalogPoller>(*this)); + case InputCommon::Polling::DeviceType::AnalogPreferred: + pollers.emplace_back(std::make_unique<Polling::SDLAnalogPreferredPoller>(*this)); break; case InputCommon::Polling::DeviceType::Button: pollers.emplace_back(std::make_unique<Polling::SDLButtonPoller>(*this)); break; + case InputCommon::Polling::DeviceType::Motion: + pollers.emplace_back(std::make_unique<Polling::SDLMotionPoller>(*this)); + break; } return pollers; diff --git a/src/input_common/sdl/sdl_impl.h b/src/input_common/sdl/sdl_impl.h index 606a32c5b..b9bb4dc56 100644 --- a/src/input_common/sdl/sdl_impl.h +++ b/src/input_common/sdl/sdl_impl.h @@ -21,6 +21,7 @@ namespace InputCommon::SDL { class SDLAnalogFactory; class SDLButtonFactory; +class SDLMotionFactory; class SDLJoystick; class SDLState : public State { @@ -50,6 +51,11 @@ public: std::atomic<bool> polling = false; Common::SPSCQueue<SDL_Event> event_queue; + std::vector<Common::ParamPackage> GetInputDevices() override; + + ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override; + AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override; + private: void InitJoystick(int joystick_index); void CloseJoystick(SDL_Joystick* sdl_joystick); @@ -57,12 +63,16 @@ private: /// Needs to be called before SDL_QuitSubSystem. void CloseJoysticks(); + // Set to true if SDL supports game controller subsystem + bool has_gamecontroller = false; + /// Map of GUID of a list of corresponding virtual Joysticks std::unordered_map<std::string, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map; std::mutex joystick_map_mutex; std::shared_ptr<SDLButtonFactory> button_factory; std::shared_ptr<SDLAnalogFactory> analog_factory; + std::shared_ptr<SDLMotionFactory> motion_factory; bool start_thread = false; std::atomic<bool> initialized = false; diff --git a/src/input_common/settings.cpp b/src/input_common/settings.cpp new file mode 100644 index 000000000..b66c05856 --- /dev/null +++ b/src/input_common/settings.cpp @@ -0,0 +1,40 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "input_common/settings.h" + +namespace Settings { +namespace NativeButton { +const std::array<const char*, NumButtons> mapping = {{ + "button_a", "button_b", "button_x", "button_y", "button_lstick", + "button_rstick", "button_l", "button_r", "button_zl", "button_zr", + "button_plus", "button_minus", "button_dleft", "button_dup", "button_dright", + "button_ddown", "button_sl", "button_sr", "button_home", "button_screenshot", +}}; +} + +namespace NativeMotion { +const std::array<const char*, NumMotions> mapping = {{ + "motionleft", + "motionright", +}}; +} + +namespace NativeAnalog { +const std::array<const char*, NumAnalogs> mapping = {{ + "lstick", + "rstick", +}}; +} + +namespace NativeMouseButton { +const std::array<const char*, NumMouseButtons> mapping = {{ + "left", + "right", + "middle", + "forward", + "back", +}}; +} +} // namespace Settings diff --git a/src/input_common/settings.h b/src/input_common/settings.h new file mode 100644 index 000000000..ab0b95cf1 --- /dev/null +++ b/src/input_common/settings.h @@ -0,0 +1,352 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <string> +#include "common/common_types.h" + +namespace Settings { +namespace NativeButton { +enum Values : int { + A, + B, + X, + Y, + LStick, + RStick, + L, + R, + ZL, + ZR, + Plus, + Minus, + + DLeft, + DUp, + DRight, + DDown, + + SL, + SR, + + Home, + Screenshot, + + NumButtons, +}; + +constexpr int BUTTON_HID_BEGIN = A; +constexpr int BUTTON_NS_BEGIN = Home; + +constexpr int BUTTON_HID_END = BUTTON_NS_BEGIN; +constexpr int BUTTON_NS_END = NumButtons; + +constexpr int NUM_BUTTONS_HID = BUTTON_HID_END - BUTTON_HID_BEGIN; +constexpr int NUM_BUTTONS_NS = BUTTON_NS_END - BUTTON_NS_BEGIN; + +extern const std::array<const char*, NumButtons> mapping; + +} // namespace NativeButton + +namespace NativeAnalog { +enum Values : int { + LStick, + RStick, + + NumAnalogs, +}; + +constexpr int STICK_HID_BEGIN = LStick; +constexpr int STICK_HID_END = NumAnalogs; +constexpr int NUM_STICKS_HID = NumAnalogs; + +extern const std::array<const char*, NumAnalogs> mapping; +} // namespace NativeAnalog + +namespace NativeMotion { +enum Values : int { + MOTIONLEFT, + MOTIONRIGHT, + + NumMotions, +}; + +constexpr int MOTION_HID_BEGIN = MOTIONLEFT; +constexpr int MOTION_HID_END = NumMotions; +constexpr int NUM_MOTION_HID = NumMotions; + +extern const std::array<const char*, NumMotions> mapping; +} // namespace NativeMotion + +namespace NativeMouseButton { +enum Values { + Left, + Right, + Middle, + Forward, + Back, + + NumMouseButtons, +}; + +constexpr int MOUSE_HID_BEGIN = Left; +constexpr int MOUSE_HID_END = NumMouseButtons; +constexpr int NUM_MOUSE_HID = NumMouseButtons; + +extern const std::array<const char*, NumMouseButtons> mapping; +} // namespace NativeMouseButton + +namespace NativeKeyboard { +enum Keys { + None, + Error, + + A = 4, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + N1, + N2, + N3, + N4, + N5, + N6, + N7, + N8, + N9, + N0, + Enter, + Escape, + Backspace, + Tab, + Space, + Minus, + Equal, + LeftBrace, + RightBrace, + Backslash, + Tilde, + Semicolon, + Apostrophe, + Grave, + Comma, + Dot, + Slash, + CapsLockKey, + + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + + SystemRequest, + ScrollLockKey, + Pause, + Insert, + Home, + PageUp, + Delete, + End, + PageDown, + Right, + Left, + Down, + Up, + + NumLockKey, + KPSlash, + KPAsterisk, + KPMinus, + KPPlus, + KPEnter, + KP1, + KP2, + KP3, + KP4, + KP5, + KP6, + KP7, + KP8, + KP9, + KP0, + KPDot, + + Key102, + Compose, + Power, + KPEqual, + + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + + Open, + Help, + Properties, + Front, + Stop, + Repeat, + Undo, + Cut, + Copy, + Paste, + Find, + Mute, + VolumeUp, + VolumeDown, + CapsLockActive, + NumLockActive, + ScrollLockActive, + KPComma, + + KPLeftParenthesis, + KPRightParenthesis, + + LeftControlKey = 0xE0, + LeftShiftKey, + LeftAltKey, + LeftMetaKey, + RightControlKey, + RightShiftKey, + RightAltKey, + RightMetaKey, + + MediaPlayPause, + MediaStopCD, + MediaPrevious, + MediaNext, + MediaEject, + MediaVolumeUp, + MediaVolumeDown, + MediaMute, + MediaWebsite, + MediaBack, + MediaForward, + MediaStop, + MediaFind, + MediaScrollUp, + MediaScrollDown, + MediaEdit, + MediaSleep, + MediaCoffee, + MediaRefresh, + MediaCalculator, + + NumKeyboardKeys, +}; + +static_assert(NumKeyboardKeys == 0xFC, "Incorrect number of keyboard keys."); + +enum Modifiers { + LeftControl, + LeftShift, + LeftAlt, + LeftMeta, + RightControl, + RightShift, + RightAlt, + RightMeta, + CapsLock, + ScrollLock, + NumLock, + + NumKeyboardMods, +}; + +constexpr int KEYBOARD_KEYS_HID_BEGIN = None; +constexpr int KEYBOARD_KEYS_HID_END = NumKeyboardKeys; +constexpr int NUM_KEYBOARD_KEYS_HID = NumKeyboardKeys; + +constexpr int KEYBOARD_MODS_HID_BEGIN = LeftControl; +constexpr int KEYBOARD_MODS_HID_END = NumKeyboardMods; +constexpr int NUM_KEYBOARD_MODS_HID = NumKeyboardMods; + +} // namespace NativeKeyboard + +using ButtonsRaw = std::array<std::string, NativeButton::NumButtons>; +using AnalogsRaw = std::array<std::string, NativeAnalog::NumAnalogs>; +using MotionRaw = std::array<std::string, NativeMotion::NumMotions>; +using MouseButtonsRaw = std::array<std::string, NativeMouseButton::NumMouseButtons>; +using KeyboardKeysRaw = std::array<std::string, NativeKeyboard::NumKeyboardKeys>; +using KeyboardModsRaw = std::array<std::string, NativeKeyboard::NumKeyboardMods>; + +constexpr u32 JOYCON_BODY_NEON_RED = 0xFF3C28; +constexpr u32 JOYCON_BUTTONS_NEON_RED = 0x1E0A0A; +constexpr u32 JOYCON_BODY_NEON_BLUE = 0x0AB9E6; +constexpr u32 JOYCON_BUTTONS_NEON_BLUE = 0x001E1E; + +enum class ControllerType { + ProController, + DualJoyconDetached, + LeftJoycon, + RightJoycon, + Handheld, +}; + +struct PlayerInput { + bool connected; + ControllerType controller_type; + ButtonsRaw buttons; + AnalogsRaw analogs; + MotionRaw motions; + std::string lstick_mod; + std::string rstick_mod; + + u32 body_color_left; + u32 body_color_right; + u32 button_color_left; + u32 button_color_right; +}; + +struct TouchscreenInput { + bool enabled; + std::string device; + + u32 finger; + u32 diameter_x; + u32 diameter_y; + u32 rotation_angle; +}; +} // namespace Settings diff --git a/src/input_common/touch_from_button.cpp b/src/input_common/touch_from_button.cpp new file mode 100644 index 000000000..98da0ef1a --- /dev/null +++ b/src/input_common/touch_from_button.cpp @@ -0,0 +1,50 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/frontend/framebuffer_layout.h" +#include "core/settings.h" +#include "input_common/touch_from_button.h" + +namespace InputCommon { + +class TouchFromButtonDevice final : public Input::TouchDevice { +public: + TouchFromButtonDevice() { + for (const auto& config_entry : + Settings::values.touch_from_button_maps[Settings::values.touch_from_button_map_index] + .buttons) { + const Common::ParamPackage package{config_entry}; + map.emplace_back( + Input::CreateDevice<Input::ButtonDevice>(config_entry), + std::clamp(package.Get("x", 0), 0, static_cast<int>(Layout::ScreenUndocked::Width)), + std::clamp(package.Get("y", 0), 0, + static_cast<int>(Layout::ScreenUndocked::Height))); + } + } + + std::tuple<float, float, bool> GetStatus() const override { + for (const auto& m : map) { + const bool state = std::get<0>(m)->GetStatus(); + if (state) { + const float x = static_cast<float>(std::get<1>(m)) / + static_cast<int>(Layout::ScreenUndocked::Width); + const float y = static_cast<float>(std::get<2>(m)) / + static_cast<int>(Layout::ScreenUndocked::Height); + return {x, y, true}; + } + } + return {}; + } + +private: + // A vector of the mapped button, its x and its y-coordinate + std::vector<std::tuple<std::unique_ptr<Input::ButtonDevice>, int, int>> map; +}; + +std::unique_ptr<Input::TouchDevice> TouchFromButtonFactory::Create( + const Common::ParamPackage& params) { + return std::make_unique<TouchFromButtonDevice>(); +} + +} // namespace InputCommon diff --git a/src/input_common/touch_from_button.h b/src/input_common/touch_from_button.h new file mode 100644 index 000000000..8b4d1aa96 --- /dev/null +++ b/src/input_common/touch_from_button.h @@ -0,0 +1,23 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include "core/frontend/input.h" + +namespace InputCommon { + +/** + * A touch device factory that takes a list of button devices and combines them into a touch device. + */ +class TouchFromButtonFactory final : public Input::Factory<Input::TouchDevice> { +public: + /** + * Creates a touch device from a list of button devices + */ + std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage& params) override; +}; + +} // namespace InputCommon diff --git a/src/input_common/udp/client.cpp b/src/input_common/udp/client.cpp index e63c73c4f..9d0b9f31d 100644 --- a/src/input_common/udp/client.cpp +++ b/src/input_common/udp/client.cpp @@ -2,15 +2,13 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include <algorithm> -#include <array> #include <chrono> #include <cstring> #include <functional> #include <thread> #include <boost/asio.hpp> -#include <boost/bind.hpp> #include "common/logging/log.h" +#include "core/settings.h" #include "input_common/udp/client.h" #include "input_common/udp/protocol.h" @@ -132,21 +130,59 @@ static void SocketLoop(Socket* socket) { socket->Loop(); } -Client::Client(std::shared_ptr<DeviceStatus> status, const std::string& host, u16 port, - u8 pad_index, u32 client_id) - : status(std::move(status)) { - StartCommunication(host, port, pad_index, client_id); +Client::Client() { + LOG_INFO(Input, "Udp Initialization started"); + for (std::size_t client = 0; client < clients.size(); client++) { + u8 pad = client % 4; + StartCommunication(client, Settings::values.udp_input_address, + Settings::values.udp_input_port, pad, 24872); + // Set motion parameters + // SetGyroThreshold value should be dependent on GyroscopeZeroDriftMode + // Real HW values are unknown, 0.0001 is an approximate to Standard + clients[client].motion.SetGyroThreshold(0.0001f); + } } Client::~Client() { - socket->Stop(); - thread.join(); + Reset(); +} + +std::vector<Common::ParamPackage> Client::GetInputDevices() const { + std::vector<Common::ParamPackage> devices; + for (std::size_t client = 0; client < clients.size(); client++) { + if (!DeviceConnected(client)) { + continue; + } + std::string name = fmt::format("UDP Controller {}", client); + devices.emplace_back(Common::ParamPackage{ + {"class", "cemuhookudp"}, + {"display", std::move(name)}, + {"port", std::to_string(client)}, + }); + } + return devices; } +bool Client::DeviceConnected(std::size_t pad) const { + // Use last timestamp to detect if the socket has stopped sending data + const auto now = std::chrono::system_clock::now(); + u64 time_difference = + std::chrono::duration_cast<std::chrono::milliseconds>(now - clients[pad].last_motion_update) + .count(); + return time_difference < 1000 && clients[pad].active == 1; +} + +void Client::ReloadUDPClient() { + for (std::size_t client = 0; client < clients.size(); client++) { + ReloadSocket(Settings::values.udp_input_address, Settings::values.udp_input_port, client); + } +} void Client::ReloadSocket(const std::string& host, u16 port, u8 pad_index, u32 client_id) { - socket->Stop(); - thread.join(); - StartCommunication(host, port, pad_index, client_id); + // client number must be determined from host / port and pad index + std::size_t client = pad_index; + clients[client].socket->Stop(); + clients[client].thread.join(); + StartCommunication(client, host, port, pad_index, client_id); } void Client::OnVersion(Response::Version data) { @@ -158,23 +194,35 @@ void Client::OnPortInfo(Response::PortInfo data) { } void Client::OnPadData(Response::PadData data) { + // client number must be determined from host / port and pad index + std::size_t client = data.info.id; LOG_TRACE(Input, "PadData packet received"); - if (data.packet_counter <= packet_sequence) { + if (data.packet_counter == clients[client].packet_sequence) { LOG_WARNING( Input, "PadData packet dropped because its stale info. Current count: {} Packet count: {}", - packet_sequence, data.packet_counter); + clients[client].packet_sequence, data.packet_counter); return; } - packet_sequence = data.packet_counter; - // TODO: Check how the Switch handles motions and how the CemuhookUDP motion - // directions correspond to the ones of the Switch - Common::Vec3f accel = Common::MakeVec<float>(data.accel.x, data.accel.y, data.accel.z); - Common::Vec3f gyro = Common::MakeVec<float>(data.gyro.pitch, data.gyro.yaw, data.gyro.roll); - { - std::lock_guard guard(status->update_mutex); + clients[client].active = data.info.is_pad_active; + clients[client].packet_sequence = data.packet_counter; + const auto now = std::chrono::system_clock::now(); + u64 time_difference = std::chrono::duration_cast<std::chrono::microseconds>( + now - clients[client].last_motion_update) + .count(); + clients[client].last_motion_update = now; + Common::Vec3f raw_gyroscope = {data.gyro.pitch, data.gyro.roll, -data.gyro.yaw}; + clients[client].motion.SetAcceleration({data.accel.x, -data.accel.z, data.accel.y}); + // Gyroscope values are not it the correct scale from better joy. + // Dividing by 312 allows us to make one full turn = 1 turn + // This must be a configurable valued called sensitivity + clients[client].motion.SetGyroscope(raw_gyroscope / 312.0f); + clients[client].motion.UpdateRotation(time_difference); + clients[client].motion.UpdateOrientation(time_difference); - status->motion_status = {accel, gyro}; + { + std::lock_guard guard(clients[client].status.update_mutex); + clients[client].status.motion_status = clients[client].motion.GetMotion(); // TODO: add a setting for "click" touch. Click touch refers to a device that differentiates // between a simple "tap" and a hard press that causes the touch screen to click. @@ -183,11 +231,11 @@ void Client::OnPadData(Response::PadData data) { float x = 0; float y = 0; - if (is_active && status->touch_calibration) { - const u16 min_x = status->touch_calibration->min_x; - const u16 max_x = status->touch_calibration->max_x; - const u16 min_y = status->touch_calibration->min_y; - const u16 max_y = status->touch_calibration->max_y; + if (is_active && clients[client].status.touch_calibration) { + const u16 min_x = clients[client].status.touch_calibration->min_x; + const u16 max_x = clients[client].status.touch_calibration->max_x; + const u16 min_y = clients[client].status.touch_calibration->min_y; + const u16 max_y = clients[client].status.touch_calibration->max_y; x = (std::clamp(static_cast<u16>(data.touch_1.x), min_x, max_x) - min_x) / static_cast<float>(max_x - min_x); @@ -195,17 +243,86 @@ void Client::OnPadData(Response::PadData data) { static_cast<float>(max_y - min_y); } - status->touch_status = {x, y, is_active}; + clients[client].status.touch_status = {x, y, is_active}; + + if (configuring) { + const Common::Vec3f gyroscope = clients[client].motion.GetGyroscope(); + const Common::Vec3f accelerometer = clients[client].motion.GetAcceleration(); + UpdateYuzuSettings(client, accelerometer, gyroscope, is_active); + } } } -void Client::StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id) { +void Client::StartCommunication(std::size_t client, const std::string& host, u16 port, u8 pad_index, + u32 client_id) { SocketCallback callback{[this](Response::Version version) { OnVersion(version); }, [this](Response::PortInfo info) { OnPortInfo(info); }, [this](Response::PadData data) { OnPadData(data); }}; LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port); - socket = std::make_unique<Socket>(host, port, pad_index, client_id, callback); - thread = std::thread{SocketLoop, this->socket.get()}; + clients[client].socket = std::make_unique<Socket>(host, port, pad_index, client_id, callback); + clients[client].thread = std::thread{SocketLoop, clients[client].socket.get()}; +} + +void Client::Reset() { + for (std::size_t client = 0; client < clients.size(); client++) { + clients[client].socket->Stop(); + clients[client].thread.join(); + } +} + +void Client::UpdateYuzuSettings(std::size_t client, const Common::Vec3<float>& acc, + const Common::Vec3<float>& gyro, bool touch) { + if (gyro.Length() > 0.2f) { + LOG_DEBUG(Input, "UDP Controller {}: gyro=({}, {}, {}), accel=({}, {}, {}), touch={}", + client, gyro[0], gyro[1], gyro[2], acc[0], acc[1], acc[2], touch); + } + UDPPadStatus pad; + if (touch) { + pad.touch = PadTouch::Click; + pad_queue[client].Push(pad); + } + for (size_t i = 0; i < 3; ++i) { + if (gyro[i] > 5.0f || gyro[i] < -5.0f) { + pad.motion = static_cast<PadMotion>(i); + pad.motion_value = gyro[i]; + pad_queue[client].Push(pad); + } + if (acc[i] > 1.75f || acc[i] < -1.75f) { + pad.motion = static_cast<PadMotion>(i + 3); + pad.motion_value = acc[i]; + pad_queue[client].Push(pad); + } + } +} + +void Client::BeginConfiguration() { + for (auto& pq : pad_queue) { + pq.Clear(); + } + configuring = true; +} + +void Client::EndConfiguration() { + for (auto& pq : pad_queue) { + pq.Clear(); + } + configuring = false; +} + +DeviceStatus& Client::GetPadState(std::size_t pad) { + return clients[pad].status; +} + +const DeviceStatus& Client::GetPadState(std::size_t pad) const { + return clients[pad].status; +} + +std::array<Common::SPSCQueue<UDPPadStatus>, 4>& Client::GetPadQueue() { + return pad_queue; +} + +const std::array<Common::SPSCQueue<UDPPadStatus>, 4>& Client::GetPadQueue() const { + return pad_queue; } void TestCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id, @@ -225,8 +342,7 @@ void TestCommunication(const std::string& host, u16 port, u8 pad_index, u32 clie } else { failure_callback(); } - }) - .detach(); + }).detach(); } CalibrationConfigurationJob::CalibrationConfigurationJob( @@ -280,8 +396,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob( complete_event.Wait(); socket.Stop(); worker_thread.join(); - }) - .detach(); + }).detach(); } CalibrationConfigurationJob::~CalibrationConfigurationJob() { diff --git a/src/input_common/udp/client.h b/src/input_common/udp/client.h index b8c654755..523dc6a7a 100644 --- a/src/input_common/udp/client.h +++ b/src/input_common/udp/client.h @@ -12,8 +12,12 @@ #include <thread> #include <tuple> #include "common/common_types.h" +#include "common/param_package.h" #include "common/thread.h" +#include "common/threadsafe_queue.h" #include "common/vector_math.h" +#include "core/frontend/input.h" +#include "input_common/motion_input.h" namespace InputCommon::CemuhookUDP { @@ -28,9 +32,30 @@ struct PortInfo; struct Version; } // namespace Response +enum class PadMotion { + GyroX, + GyroY, + GyroZ, + AccX, + AccY, + AccZ, + Undefined, +}; + +enum class PadTouch { + Click, + Undefined, +}; + +struct UDPPadStatus { + PadTouch touch{PadTouch::Undefined}; + PadMotion motion{PadMotion::Undefined}; + f32 motion_value{0.0f}; +}; + struct DeviceStatus { std::mutex update_mutex; - std::tuple<Common::Vec3<float>, Common::Vec3<float>> motion_status; + Input::MotionStatus motion_status; std::tuple<float, float, bool> touch_status; // calibration data for scaling the device's touch area to 3ds @@ -45,22 +70,58 @@ struct DeviceStatus { class Client { public: - explicit Client(std::shared_ptr<DeviceStatus> status, const std::string& host = DEFAULT_ADDR, - u16 port = DEFAULT_PORT, u8 pad_index = 0, u32 client_id = 24872); + // Initialize the UDP client capture and read sequence + Client(); + + // Close and release the client ~Client(); + + // Used for polling + void BeginConfiguration(); + void EndConfiguration(); + + std::vector<Common::ParamPackage> GetInputDevices() const; + + bool DeviceConnected(std::size_t pad) const; + void ReloadUDPClient(); void ReloadSocket(const std::string& host = "127.0.0.1", u16 port = 26760, u8 pad_index = 0, u32 client_id = 24872); + std::array<Common::SPSCQueue<UDPPadStatus>, 4>& GetPadQueue(); + const std::array<Common::SPSCQueue<UDPPadStatus>, 4>& GetPadQueue() const; + + DeviceStatus& GetPadState(std::size_t pad); + const DeviceStatus& GetPadState(std::size_t pad) const; + private: + struct ClientData { + std::unique_ptr<Socket> socket; + DeviceStatus status; + std::thread thread; + u64 packet_sequence = 0; + u8 active; + + // Realtime values + // motion is initalized with PID values for drift correction on joycons + InputCommon::MotionInput motion{0.3f, 0.005f, 0.0f}; + std::chrono::time_point<std::chrono::system_clock> last_motion_update; + }; + + // For shutting down, clear all data, join all threads, release usb + void Reset(); + void OnVersion(Response::Version); void OnPortInfo(Response::PortInfo); void OnPadData(Response::PadData); - void StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id); + void StartCommunication(std::size_t client, const std::string& host, u16 port, u8 pad_index, + u32 client_id); + void UpdateYuzuSettings(std::size_t client, const Common::Vec3<float>& acc, + const Common::Vec3<float>& gyro, bool touch); + + bool configuring = false; - std::unique_ptr<Socket> socket; - std::shared_ptr<DeviceStatus> status; - std::thread thread; - u64 packet_sequence = 0; + std::array<ClientData, 4> clients; + std::array<Common::SPSCQueue<UDPPadStatus>, 4> pad_queue; }; /// An async job allowing configuration of the touchpad calibration. diff --git a/src/input_common/udp/udp.cpp b/src/input_common/udp/udp.cpp index 8c6ef1394..eba077a36 100644 --- a/src/input_common/udp/udp.cpp +++ b/src/input_common/udp/udp.cpp @@ -1,99 +1,144 @@ -// Copyright 2018 Citra Emulator Project +// Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <atomic> +#include <list> #include <mutex> -#include <optional> -#include <tuple> - -#include "common/param_package.h" -#include "core/frontend/input.h" -#include "core/settings.h" +#include <utility> +#include "common/assert.h" +#include "common/threadsafe_queue.h" #include "input_common/udp/client.h" #include "input_common/udp/udp.h" -namespace InputCommon::CemuhookUDP { +namespace InputCommon { -class UDPTouchDevice final : public Input::TouchDevice { +class UDPMotion final : public Input::MotionDevice { public: - explicit UDPTouchDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {} - std::tuple<float, float, bool> GetStatus() const override { - std::lock_guard guard(status->update_mutex); - return status->touch_status; + UDPMotion(std::string ip_, int port_, int pad_, CemuhookUDP::Client* client_) + : ip(ip_), port(port_), pad(pad_), client(client_) {} + + Input::MotionStatus GetStatus() const override { + return client->GetPadState(pad).motion_status; } private: - std::shared_ptr<DeviceStatus> status; + const std::string ip; + const int port; + const int pad; + CemuhookUDP::Client* client; + mutable std::mutex mutex; }; -class UDPMotionDevice final : public Input::MotionDevice { -public: - explicit UDPMotionDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {} - std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const override { - std::lock_guard guard(status->update_mutex); - return status->motion_status; - } +/// A motion device factory that creates motion devices from JC Adapter +UDPMotionFactory::UDPMotionFactory(std::shared_ptr<CemuhookUDP::Client> client_) + : client(std::move(client_)) {} + +/** + * Creates motion device + * @param params contains parameters for creating the device: + * - "port": the nth jcpad on the adapter + */ +std::unique_ptr<Input::MotionDevice> UDPMotionFactory::Create(const Common::ParamPackage& params) { + const std::string ip = params.Get("ip", "127.0.0.1"); + const int port = params.Get("port", 26760); + const int pad = params.Get("pad_index", 0); + + return std::make_unique<UDPMotion>(ip, port, pad, client.get()); +} -private: - std::shared_ptr<DeviceStatus> status; -}; +void UDPMotionFactory::BeginConfiguration() { + polling = true; + client->BeginConfiguration(); +} -class UDPTouchFactory final : public Input::Factory<Input::TouchDevice> { -public: - explicit UDPTouchFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {} - - std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage& params) override { - { - std::lock_guard guard(status->update_mutex); - status->touch_calibration = DeviceStatus::CalibrationData{}; - // These default values work well for DS4 but probably not other touch inputs - status->touch_calibration->min_x = params.Get("min_x", 100); - status->touch_calibration->min_y = params.Get("min_y", 50); - status->touch_calibration->max_x = params.Get("max_x", 1800); - status->touch_calibration->max_y = params.Get("max_y", 850); +void UDPMotionFactory::EndConfiguration() { + polling = false; + client->EndConfiguration(); +} + +Common::ParamPackage UDPMotionFactory::GetNextInput() { + Common::ParamPackage params; + CemuhookUDP::UDPPadStatus pad; + auto& queue = client->GetPadQueue(); + for (std::size_t pad_number = 0; pad_number < queue.size(); ++pad_number) { + while (queue[pad_number].Pop(pad)) { + if (pad.motion == CemuhookUDP::PadMotion::Undefined || std::abs(pad.motion_value) < 1) { + continue; + } + params.Set("engine", "cemuhookudp"); + params.Set("ip", "127.0.0.1"); + params.Set("port", 26760); + params.Set("pad_index", static_cast<int>(pad_number)); + params.Set("motion", static_cast<u16>(pad.motion)); + return params; } - return std::make_unique<UDPTouchDevice>(status); } + return params; +} -private: - std::shared_ptr<DeviceStatus> status; -}; - -class UDPMotionFactory final : public Input::Factory<Input::MotionDevice> { +class UDPTouch final : public Input::TouchDevice { public: - explicit UDPMotionFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {} + UDPTouch(std::string ip_, int port_, int pad_, CemuhookUDP::Client* client_) + : ip(std::move(ip_)), port(port_), pad(pad_), client(client_) {} - std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override { - return std::make_unique<UDPMotionDevice>(status); + std::tuple<float, float, bool> GetStatus() const override { + return client->GetPadState(pad).touch_status; } private: - std::shared_ptr<DeviceStatus> status; + const std::string ip; + const int port; + const int pad; + CemuhookUDP::Client* client; + mutable std::mutex mutex; }; -State::State() { - auto status = std::make_shared<DeviceStatus>(); - client = - std::make_unique<Client>(status, Settings::values.udp_input_address, - Settings::values.udp_input_port, Settings::values.udp_pad_index); - - Input::RegisterFactory<Input::TouchDevice>("cemuhookudp", - std::make_shared<UDPTouchFactory>(status)); - Input::RegisterFactory<Input::MotionDevice>("cemuhookudp", - std::make_shared<UDPMotionFactory>(status)); +/// A motion device factory that creates motion devices from JC Adapter +UDPTouchFactory::UDPTouchFactory(std::shared_ptr<CemuhookUDP::Client> client_) + : client(std::move(client_)) {} + +/** + * Creates motion device + * @param params contains parameters for creating the device: + * - "port": the nth jcpad on the adapter + */ +std::unique_ptr<Input::TouchDevice> UDPTouchFactory::Create(const Common::ParamPackage& params) { + const std::string ip = params.Get("ip", "127.0.0.1"); + const int port = params.Get("port", 26760); + const int pad = params.Get("pad_index", 0); + + return std::make_unique<UDPTouch>(ip, port, pad, client.get()); } -State::~State() { - Input::UnregisterFactory<Input::TouchDevice>("cemuhookudp"); - Input::UnregisterFactory<Input::MotionDevice>("cemuhookudp"); +void UDPTouchFactory::BeginConfiguration() { + polling = true; + client->BeginConfiguration(); } -void State::ReloadUDPClient() { - client->ReloadSocket(Settings::values.udp_input_address, Settings::values.udp_input_port, - Settings::values.udp_pad_index); +void UDPTouchFactory::EndConfiguration() { + polling = false; + client->EndConfiguration(); } -std::unique_ptr<State> Init() { - return std::make_unique<State>(); +Common::ParamPackage UDPTouchFactory::GetNextInput() { + Common::ParamPackage params; + CemuhookUDP::UDPPadStatus pad; + auto& queue = client->GetPadQueue(); + for (std::size_t pad_number = 0; pad_number < queue.size(); ++pad_number) { + while (queue[pad_number].Pop(pad)) { + if (pad.touch == CemuhookUDP::PadTouch::Undefined) { + continue; + } + params.Set("engine", "cemuhookudp"); + params.Set("ip", "127.0.0.1"); + params.Set("port", 26760); + params.Set("pad_index", static_cast<int>(pad_number)); + params.Set("touch", static_cast<u16>(pad.touch)); + return params; + } + } + return params; } -} // namespace InputCommon::CemuhookUDP + +} // namespace InputCommon diff --git a/src/input_common/udp/udp.h b/src/input_common/udp/udp.h index 4f83f0441..ea3fd4175 100644 --- a/src/input_common/udp/udp.h +++ b/src/input_common/udp/udp.h @@ -1,25 +1,57 @@ -// Copyright 2018 Citra Emulator Project +// Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include <memory> +#include "core/frontend/input.h" +#include "input_common/udp/client.h" -namespace InputCommon::CemuhookUDP { +namespace InputCommon { -class Client; - -class State { +/// A motion device factory that creates motion devices from udp clients +class UDPMotionFactory final : public Input::Factory<Input::MotionDevice> { public: - State(); - ~State(); - void ReloadUDPClient(); + explicit UDPMotionFactory(std::shared_ptr<CemuhookUDP::Client> client_); + + std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } private: - std::unique_ptr<Client> client; + std::shared_ptr<CemuhookUDP::Client> client; + bool polling = false; }; -std::unique_ptr<State> Init(); +/// A touch device factory that creates touch devices from udp clients +class UDPTouchFactory final : public Input::Factory<Input::TouchDevice> { +public: + explicit UDPTouchFactory(std::shared_ptr<CemuhookUDP::Client> client_); + + std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr<CemuhookUDP::Client> client; + bool polling = false; +}; -} // namespace InputCommon::CemuhookUDP +} // namespace InputCommon |