summaryrefslogtreecommitdiffstats
path: root/src/input_common/drivers
diff options
context:
space:
mode:
Diffstat (limited to 'src/input_common/drivers')
-rw-r--r--src/input_common/drivers/gc_adapter.cpp527
-rw-r--r--src/input_common/drivers/gc_adapter.h135
-rw-r--r--src/input_common/drivers/keyboard.cpp112
-rw-r--r--src/input_common/drivers/keyboard.h56
-rw-r--r--src/input_common/drivers/mouse.cpp185
-rw-r--r--src/input_common/drivers/mouse.h81
-rw-r--r--src/input_common/drivers/sdl_driver.cpp924
-rw-r--r--src/input_common/drivers/sdl_driver.h117
-rw-r--r--src/input_common/drivers/tas_input.cpp315
-rw-r--r--src/input_common/drivers/tas_input.h194
-rw-r--r--src/input_common/drivers/touch_screen.cpp53
-rw-r--r--src/input_common/drivers/touch_screen.h44
-rw-r--r--src/input_common/drivers/udp_client.cpp591
-rw-r--r--src/input_common/drivers/udp_client.h185
14 files changed, 3519 insertions, 0 deletions
diff --git a/src/input_common/drivers/gc_adapter.cpp b/src/input_common/drivers/gc_adapter.cpp
new file mode 100644
index 000000000..8b6574223
--- /dev/null
+++ b/src/input_common/drivers/gc_adapter.cpp
@@ -0,0 +1,527 @@
+// Copyright 2014 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include <fmt/format.h>
+#include <libusb.h>
+
+#include "common/logging/log.h"
+#include "common/param_package.h"
+#include "common/settings_input.h"
+#include "common/thread.h"
+#include "input_common/drivers/gc_adapter.h"
+
+namespace InputCommon {
+
+class LibUSBContext {
+public:
+ explicit LibUSBContext() {
+ init_result = libusb_init(&ctx);
+ }
+
+ ~LibUSBContext() {
+ libusb_exit(ctx);
+ }
+
+ LibUSBContext& operator=(const LibUSBContext&) = delete;
+ LibUSBContext(const LibUSBContext&) = delete;
+
+ LibUSBContext& operator=(LibUSBContext&&) noexcept = delete;
+ LibUSBContext(LibUSBContext&&) noexcept = delete;
+
+ [[nodiscard]] int InitResult() const noexcept {
+ return init_result;
+ }
+
+ [[nodiscard]] libusb_context* get() noexcept {
+ return ctx;
+ }
+
+private:
+ libusb_context* ctx;
+ int init_result{};
+};
+
+class LibUSBDeviceHandle {
+public:
+ explicit LibUSBDeviceHandle(libusb_context* ctx, uint16_t vid, uint16_t pid) noexcept {
+ handle = libusb_open_device_with_vid_pid(ctx, vid, pid);
+ }
+
+ ~LibUSBDeviceHandle() noexcept {
+ if (handle) {
+ libusb_release_interface(handle, 1);
+ libusb_close(handle);
+ }
+ }
+
+ LibUSBDeviceHandle& operator=(const LibUSBDeviceHandle&) = delete;
+ LibUSBDeviceHandle(const LibUSBDeviceHandle&) = delete;
+
+ LibUSBDeviceHandle& operator=(LibUSBDeviceHandle&&) noexcept = delete;
+ LibUSBDeviceHandle(LibUSBDeviceHandle&&) noexcept = delete;
+
+ [[nodiscard]] libusb_device_handle* get() noexcept {
+ return handle;
+ }
+
+private:
+ libusb_device_handle* handle{};
+};
+
+GCAdapter::GCAdapter(const std::string& input_engine_) : InputEngine(input_engine_) {
+ if (usb_adapter_handle) {
+ return;
+ }
+ LOG_DEBUG(Input, "Initialization started");
+
+ libusb_ctx = std::make_unique<LibUSBContext>();
+ const int init_res = libusb_ctx->InitResult();
+ if (init_res == LIBUSB_SUCCESS) {
+ adapter_scan_thread =
+ std::jthread([this](std::stop_token stop_token) { AdapterScanThread(stop_token); });
+ } else {
+ LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res);
+ }
+}
+
+GCAdapter::~GCAdapter() {
+ Reset();
+}
+
+void GCAdapter::AdapterInputThread(std::stop_token stop_token) {
+ LOG_DEBUG(Input, "Input thread started");
+ Common::SetCurrentThreadName("yuzu:input:GCAdapter");
+ s32 payload_size{};
+ AdapterPayload adapter_payload{};
+
+ adapter_scan_thread = {};
+
+ while (!stop_token.stop_requested()) {
+ libusb_interrupt_transfer(usb_adapter_handle->get(), input_endpoint, adapter_payload.data(),
+ static_cast<s32>(adapter_payload.size()), &payload_size, 16);
+ if (IsPayloadCorrect(adapter_payload, payload_size)) {
+ UpdateControllers(adapter_payload);
+ UpdateVibrations();
+ }
+ std::this_thread::yield();
+ }
+
+ if (restart_scan_thread) {
+ adapter_scan_thread =
+ std::jthread([this](std::stop_token token) { AdapterScanThread(token); });
+ restart_scan_thread = false;
+ }
+}
+
+bool GCAdapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size) {
+ if (payload_size != static_cast<s32>(adapter_payload.size()) ||
+ adapter_payload[0] != LIBUSB_DT_HID) {
+ LOG_DEBUG(Input, "Error reading payload (size: {}, type: {:02x})", payload_size,
+ adapter_payload[0]);
+ if (input_error_counter++ > 20) {
+ LOG_ERROR(Input, "Timeout, Is the adapter connected?");
+ adapter_input_thread.request_stop();
+ restart_scan_thread = true;
+ }
+ return false;
+ }
+
+ input_error_counter = 0;
+ return true;
+}
+
+void GCAdapter::UpdateControllers(const AdapterPayload& adapter_payload) {
+ for (std::size_t port = 0; port < pads.size(); ++port) {
+ const std::size_t offset = 1 + (9 * port);
+ const auto type = static_cast<ControllerTypes>(adapter_payload[offset] >> 4);
+ UpdatePadType(port, type);
+ if (DeviceConnected(port)) {
+ const u8 b1 = adapter_payload[offset + 1];
+ const u8 b2 = adapter_payload[offset + 2];
+ UpdateStateButtons(port, b1, b2);
+ UpdateStateAxes(port, adapter_payload);
+ }
+ }
+}
+
+void GCAdapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) {
+ if (pads[port].type == pad_type) {
+ return;
+ }
+ // Device changed reset device and set new type
+ pads[port].axis_origin = {};
+ pads[port].reset_origin_counter = {};
+ pads[port].enable_vibration = {};
+ pads[port].rumble_amplitude = {};
+ pads[port].type = pad_type;
+}
+
+void GCAdapter::UpdateStateButtons(std::size_t port, [[maybe_unused]] u8 b1,
+ [[maybe_unused]] u8 b2) {
+ if (port >= pads.size()) {
+ return;
+ }
+
+ static constexpr std::array<PadButton, 8> b1_buttons{
+ PadButton::ButtonA, PadButton::ButtonB, PadButton::ButtonX, PadButton::ButtonY,
+ PadButton::ButtonLeft, PadButton::ButtonRight, PadButton::ButtonDown, PadButton::ButtonUp,
+ };
+
+ static constexpr std::array<PadButton, 4> b2_buttons{
+ PadButton::ButtonStart,
+ PadButton::TriggerZ,
+ PadButton::TriggerR,
+ PadButton::TriggerL,
+ };
+
+ for (std::size_t i = 0; i < b1_buttons.size(); ++i) {
+ const bool button_status = (b1 & (1U << i)) != 0;
+ const int button = static_cast<int>(b1_buttons[i]);
+ SetButton(pads[port].identifier, button, button_status);
+ }
+
+ for (std::size_t j = 0; j < b2_buttons.size(); ++j) {
+ const bool button_status = (b2 & (1U << j)) != 0;
+ const int button = static_cast<int>(b2_buttons[j]);
+ SetButton(pads[port].identifier, button, button_status);
+ }
+}
+
+void GCAdapter::UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload) {
+ if (port >= pads.size()) {
+ return;
+ }
+
+ const std::size_t offset = 1 + (9 * port);
+ static constexpr std::array<PadAxes, 6> axes{
+ PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX,
+ PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight,
+ };
+
+ for (const PadAxes axis : axes) {
+ const auto index = static_cast<std::size_t>(axis);
+ const u8 axis_value = adapter_payload[offset + 3 + index];
+ if (pads[port].reset_origin_counter <= 18) {
+ if (pads[port].axis_origin[index] != axis_value) {
+ pads[port].reset_origin_counter = 0;
+ }
+ pads[port].axis_origin[index] = axis_value;
+ pads[port].reset_origin_counter++;
+ }
+ const f32 axis_status = (axis_value - pads[port].axis_origin[index]) / 100.0f;
+ SetAxis(pads[port].identifier, static_cast<int>(index), axis_status);
+ }
+}
+
+void GCAdapter::AdapterScanThread(std::stop_token stop_token) {
+ Common::SetCurrentThreadName("yuzu:input:ScanGCAdapter");
+ usb_adapter_handle = nullptr;
+ pads = {};
+ while (!stop_token.stop_requested() && !Setup()) {
+ std::this_thread::sleep_for(std::chrono::seconds(2));
+ }
+}
+
+bool GCAdapter::Setup() {
+ constexpr u16 nintendo_vid = 0x057e;
+ constexpr u16 gc_adapter_pid = 0x0337;
+ usb_adapter_handle =
+ std::make_unique<LibUSBDeviceHandle>(libusb_ctx->get(), nintendo_vid, gc_adapter_pid);
+ if (!usb_adapter_handle->get()) {
+ return false;
+ }
+ if (!CheckDeviceAccess()) {
+ usb_adapter_handle = nullptr;
+ return false;
+ }
+
+ libusb_device* const device = libusb_get_device(usb_adapter_handle->get());
+
+ LOG_INFO(Input, "GC adapter is now connected");
+ // GC Adapter found and accessible, registering it
+ if (GetGCEndpoint(device)) {
+ rumble_enabled = true;
+ input_error_counter = 0;
+ output_error_counter = 0;
+
+ std::size_t port = 0;
+ for (GCController& pad : pads) {
+ pad.identifier = {
+ .guid = Common::UUID{Common::INVALID_UUID},
+ .port = port++,
+ .pad = 0,
+ };
+ PreSetController(pad.identifier);
+ }
+
+ adapter_input_thread =
+ std::jthread([this](std::stop_token stop_token) { AdapterInputThread(stop_token); });
+ return true;
+ }
+ return false;
+}
+
+bool GCAdapter::CheckDeviceAccess() {
+ s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle->get(), 0);
+ if (kernel_driver_error == 1) {
+ kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle->get(), 0);
+ if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
+ LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}",
+ kernel_driver_error);
+ }
+ }
+
+ if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
+ usb_adapter_handle = nullptr;
+ return false;
+ }
+
+ const int interface_claim_error = libusb_claim_interface(usb_adapter_handle->get(), 0);
+ if (interface_claim_error) {
+ LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error);
+ usb_adapter_handle = nullptr;
+ return false;
+ }
+
+ // This fixes payload problems from offbrand GCAdapters
+ const s32 control_transfer_error =
+ libusb_control_transfer(usb_adapter_handle->get(), 0x21, 11, 0x0001, 0, nullptr, 0, 1000);
+ if (control_transfer_error < 0) {
+ LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error);
+ }
+
+ return true;
+}
+
+bool GCAdapter::GetGCEndpoint(libusb_device* device) {
+ libusb_config_descriptor* config = nullptr;
+ const int config_descriptor_return = libusb_get_config_descriptor(device, 0, &config);
+ if (config_descriptor_return != LIBUSB_SUCCESS) {
+ LOG_ERROR(Input, "libusb_get_config_descriptor failed with error = {}",
+ config_descriptor_return);
+ return false;
+ }
+
+ for (u8 ic = 0; ic < config->bNumInterfaces; ic++) {
+ const libusb_interface* interfaceContainer = &config->interface[ic];
+ for (int i = 0; i < interfaceContainer->num_altsetting; i++) {
+ const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i];
+ for (u8 e = 0; e < interface->bNumEndpoints; e++) {
+ const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e];
+ if ((endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0) {
+ input_endpoint = endpoint->bEndpointAddress;
+ } else {
+ output_endpoint = endpoint->bEndpointAddress;
+ }
+ }
+ }
+ }
+ // This transfer seems to be responsible for clearing the state of the adapter
+ // Used to clear the "busy" state of when the device is unexpectedly unplugged
+ unsigned char clear_payload = 0x13;
+ libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, &clear_payload,
+ sizeof(clear_payload), nullptr, 16);
+ return true;
+}
+
+Common::Input::VibrationError GCAdapter::SetRumble(const PadIdentifier& identifier,
+ const Common::Input::VibrationStatus vibration) {
+ const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f;
+ const auto processed_amplitude =
+ static_cast<u8>((mean_amplitude + std::pow(mean_amplitude, 0.3f)) * 0.5f * 0x8);
+
+ pads[identifier.port].rumble_amplitude = processed_amplitude;
+
+ if (!rumble_enabled) {
+ return Common::Input::VibrationError::Disabled;
+ }
+ return Common::Input::VibrationError::None;
+}
+
+void GCAdapter::UpdateVibrations() {
+ // Use 8 states to keep the switching between on/off fast enough for
+ // a human to feel different vibration strenght
+ // More states == more rumble strengths == slower update time
+ constexpr u8 vibration_states = 8;
+
+ vibration_counter = (vibration_counter + 1) % vibration_states;
+
+ for (GCController& pad : pads) {
+ const bool vibrate = pad.rumble_amplitude > vibration_counter;
+ vibration_changed |= vibrate != pad.enable_vibration;
+ pad.enable_vibration = vibrate;
+ }
+ SendVibrations();
+}
+
+void GCAdapter::SendVibrations() {
+ if (!rumble_enabled || !vibration_changed) {
+ return;
+ }
+ s32 size{};
+ constexpr u8 rumble_command = 0x11;
+ const u8 p1 = pads[0].enable_vibration;
+ const u8 p2 = pads[1].enable_vibration;
+ const u8 p3 = pads[2].enable_vibration;
+ const u8 p4 = pads[3].enable_vibration;
+ std::array<u8, 5> payload = {rumble_command, p1, p2, p3, p4};
+ const int err =
+ libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, payload.data(),
+ static_cast<s32>(payload.size()), &size, 16);
+ if (err) {
+ LOG_DEBUG(Input, "Libusb write failed: {}", libusb_error_name(err));
+ if (output_error_counter++ > 5) {
+ LOG_ERROR(Input, "Output timeout, Rumble disabled");
+ rumble_enabled = false;
+ }
+ return;
+ }
+ output_error_counter = 0;
+ vibration_changed = false;
+}
+
+bool GCAdapter::DeviceConnected(std::size_t port) const {
+ return pads[port].type != ControllerTypes::None;
+}
+
+void GCAdapter::Reset() {
+ adapter_scan_thread = {};
+ adapter_input_thread = {};
+ usb_adapter_handle = nullptr;
+ pads = {};
+ libusb_ctx = nullptr;
+}
+
+std::vector<Common::ParamPackage> GCAdapter::GetInputDevices() const {
+ std::vector<Common::ParamPackage> devices;
+ for (std::size_t port = 0; port < pads.size(); ++port) {
+ if (!DeviceConnected(port)) {
+ continue;
+ }
+ Common::ParamPackage identifier{};
+ identifier.Set("engine", GetEngineName());
+ identifier.Set("display", fmt::format("Gamecube Controller {}", port + 1));
+ identifier.Set("port", static_cast<int>(port));
+ devices.emplace_back(identifier);
+ }
+ return devices;
+}
+
+ButtonMapping GCAdapter::GetButtonMappingForDevice(const Common::ParamPackage& params) {
+ // 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::ButtonA},
+ {Settings::NativeButton::B, PadButton::ButtonB},
+ {Settings::NativeButton::X, PadButton::ButtonX},
+ {Settings::NativeButton::Y, PadButton::ButtonY},
+ {Settings::NativeButton::Plus, PadButton::ButtonStart},
+ {Settings::NativeButton::DLeft, PadButton::ButtonLeft},
+ {Settings::NativeButton::DUp, PadButton::ButtonUp},
+ {Settings::NativeButton::DRight, PadButton::ButtonRight},
+ {Settings::NativeButton::DDown, PadButton::ButtonDown},
+ {Settings::NativeButton::SL, PadButton::TriggerL},
+ {Settings::NativeButton::SR, PadButton::TriggerR},
+ {Settings::NativeButton::R, PadButton::TriggerZ},
+ };
+ if (!params.Has("port")) {
+ return {};
+ }
+
+ ButtonMapping mapping{};
+ for (const auto& [switch_button, gcadapter_button] : switch_to_gcadapter_button) {
+ Common::ParamPackage button_params{};
+ button_params.Set("engine", GetEngineName());
+ 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::tuple<Settings::NativeButton::Values, PadButton, PadAxes>, 2>
+ switch_to_gcadapter_axis = {
+ std::tuple{Settings::NativeButton::ZL, PadButton::TriggerL, PadAxes::TriggerLeft},
+ {Settings::NativeButton::ZR, PadButton::TriggerR, PadAxes::TriggerRight},
+ };
+ for (const auto& [switch_button, gcadapter_buton, gcadapter_axis] : switch_to_gcadapter_axis) {
+ Common::ParamPackage button_params{};
+ button_params.Set("engine", GetEngineName());
+ button_params.Set("port", params.Get("port", 0));
+ button_params.Set("button", static_cast<s32>(gcadapter_buton));
+ button_params.Set("axis", static_cast<s32>(gcadapter_axis));
+ button_params.Set("threshold", 0.5f);
+ button_params.Set("range", 1.9f);
+ button_params.Set("direction", "+");
+ mapping.insert_or_assign(switch_button, std::move(button_params));
+ }
+ return mapping;
+}
+
+AnalogMapping GCAdapter::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
+ if (!params.Has("port")) {
+ return {};
+ }
+
+ AnalogMapping mapping = {};
+ Common::ParamPackage left_analog_params;
+ left_analog_params.Set("engine", GetEngineName());
+ 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", GetEngineName());
+ 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;
+}
+
+Common::Input::ButtonNames GCAdapter::GetUIButtonName(const Common::ParamPackage& params) const {
+ PadButton button = static_cast<PadButton>(params.Get("button", 0));
+ switch (button) {
+ case PadButton::ButtonLeft:
+ return Common::Input::ButtonNames::ButtonLeft;
+ case PadButton::ButtonRight:
+ return Common::Input::ButtonNames::ButtonRight;
+ case PadButton::ButtonDown:
+ return Common::Input::ButtonNames::ButtonDown;
+ case PadButton::ButtonUp:
+ return Common::Input::ButtonNames::ButtonUp;
+ case PadButton::TriggerZ:
+ return Common::Input::ButtonNames::TriggerZ;
+ case PadButton::TriggerR:
+ return Common::Input::ButtonNames::TriggerR;
+ case PadButton::TriggerL:
+ return Common::Input::ButtonNames::TriggerL;
+ case PadButton::ButtonA:
+ return Common::Input::ButtonNames::ButtonA;
+ case PadButton::ButtonB:
+ return Common::Input::ButtonNames::ButtonB;
+ case PadButton::ButtonX:
+ return Common::Input::ButtonNames::ButtonX;
+ case PadButton::ButtonY:
+ return Common::Input::ButtonNames::ButtonY;
+ case PadButton::ButtonStart:
+ return Common::Input::ButtonNames::ButtonStart;
+ default:
+ return Common::Input::ButtonNames::Undefined;
+ }
+}
+
+Common::Input::ButtonNames GCAdapter::GetUIName(const Common::ParamPackage& params) const {
+ if (params.Has("button")) {
+ return GetUIButtonName(params);
+ }
+ if (params.Has("axis")) {
+ return Common::Input::ButtonNames::Value;
+ }
+
+ return Common::Input::ButtonNames::Invalid;
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/gc_adapter.h b/src/input_common/drivers/gc_adapter.h
new file mode 100644
index 000000000..8dc51d2e5
--- /dev/null
+++ b/src/input_common/drivers/gc_adapter.h
@@ -0,0 +1,135 @@
+// Copyright 2014 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <memory>
+#include <mutex>
+#include <stop_token>
+#include <string>
+#include <thread>
+
+#include "input_common/input_engine.h"
+
+struct libusb_context;
+struct libusb_device;
+struct libusb_device_handle;
+
+namespace InputCommon {
+
+class LibUSBContext;
+class LibUSBDeviceHandle;
+
+class GCAdapter : public InputCommon::InputEngine {
+public:
+ explicit GCAdapter(const std::string& input_engine_);
+ ~GCAdapter();
+
+ Common::Input::VibrationError SetRumble(
+ const PadIdentifier& identifier, const Common::Input::VibrationStatus vibration) override;
+
+ /// Used for automapping features
+ std::vector<Common::ParamPackage> GetInputDevices() const override;
+ ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
+ AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
+ Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
+
+private:
+ enum class PadButton {
+ Undefined = 0x0000,
+ ButtonLeft = 0x0001,
+ ButtonRight = 0x0002,
+ ButtonDown = 0x0004,
+ ButtonUp = 0x0008,
+ TriggerZ = 0x0010,
+ TriggerR = 0x0020,
+ TriggerL = 0x0040,
+ ButtonA = 0x0100,
+ ButtonB = 0x0200,
+ ButtonX = 0x0400,
+ ButtonY = 0x0800,
+ ButtonStart = 0x1000,
+ };
+
+ enum class PadAxes : u8 {
+ StickX,
+ StickY,
+ SubstickX,
+ SubstickY,
+ TriggerLeft,
+ TriggerRight,
+ Undefined,
+ };
+
+ enum class ControllerTypes {
+ None,
+ Wired,
+ Wireless,
+ };
+
+ struct GCController {
+ ControllerTypes type = ControllerTypes::None;
+ PadIdentifier identifier{};
+ bool enable_vibration = false;
+ u8 rumble_amplitude{};
+ std::array<u8, 6> axis_origin{};
+ u8 reset_origin_counter{};
+ };
+
+ using AdapterPayload = std::array<u8, 37>;
+
+ void UpdatePadType(std::size_t port, ControllerTypes pad_type);
+ void UpdateControllers(const AdapterPayload& adapter_payload);
+ void UpdateStateButtons(std::size_t port, u8 b1, u8 b2);
+ void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload);
+
+ void AdapterInputThread(std::stop_token stop_token);
+
+ void AdapterScanThread(std::stop_token stop_token);
+
+ bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size);
+
+ /// For use in initialization, querying devices to find the adapter
+ bool Setup();
+
+ /// Returns true if we successfully gain access to GC Adapter
+ bool CheckDeviceAccess();
+
+ /// Captures GC Adapter endpoint address
+ /// Returns true if the endpoint was set correctly
+ bool GetGCEndpoint(libusb_device* device);
+
+ /// Returns true if there is a device connected to port
+ bool DeviceConnected(std::size_t port) const;
+
+ /// For shutting down, clear all data, join all threads, release usb
+ void Reset();
+
+ void UpdateVibrations();
+
+ /// Updates vibration state of all controllers
+ void SendVibrations();
+
+ Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
+
+ std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle;
+ std::array<GCController, 4> pads;
+
+ std::jthread adapter_input_thread;
+ std::jthread adapter_scan_thread;
+ bool restart_scan_thread{};
+
+ std::unique_ptr<LibUSBContext> libusb_ctx;
+
+ u8 input_endpoint{0};
+ u8 output_endpoint{0};
+ u8 input_error_counter{0};
+ u8 output_error_counter{0};
+ int vibration_counter{0};
+
+ bool rumble_enabled{true};
+ bool vibration_changed{true};
+};
+} // namespace InputCommon
diff --git a/src/input_common/drivers/keyboard.cpp b/src/input_common/drivers/keyboard.cpp
new file mode 100644
index 000000000..23b0c0ccf
--- /dev/null
+++ b/src/input_common/drivers/keyboard.cpp
@@ -0,0 +1,112 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included
+
+#include "common/param_package.h"
+#include "common/settings_input.h"
+#include "input_common/drivers/keyboard.h"
+
+namespace InputCommon {
+
+constexpr PadIdentifier key_identifier = {
+ .guid = Common::UUID{Common::INVALID_UUID},
+ .port = 0,
+ .pad = 0,
+};
+constexpr PadIdentifier keyboard_key_identifier = {
+ .guid = Common::UUID{Common::INVALID_UUID},
+ .port = 1,
+ .pad = 0,
+};
+constexpr PadIdentifier keyboard_modifier_identifier = {
+ .guid = Common::UUID{Common::INVALID_UUID},
+ .port = 1,
+ .pad = 1,
+};
+
+Keyboard::Keyboard(const std::string& input_engine_) : InputEngine(input_engine_) {
+ // Keyboard is broken into 3 diferent sets:
+ // key: Unfiltered intended for controllers.
+ // keyboard_key: Allows only Settings::NativeKeyboard::Keys intended for keyboard emulation.
+ // keyboard_modifier: Allows only Settings::NativeKeyboard::Modifiers intended for keyboard
+ // emulation.
+ PreSetController(key_identifier);
+ PreSetController(keyboard_key_identifier);
+ PreSetController(keyboard_modifier_identifier);
+}
+
+void Keyboard::PressKey(int key_code) {
+ SetButton(key_identifier, key_code, true);
+}
+
+void Keyboard::ReleaseKey(int key_code) {
+ SetButton(key_identifier, key_code, false);
+}
+
+void Keyboard::PressKeyboardKey(int key_index) {
+ if (key_index == Settings::NativeKeyboard::None) {
+ return;
+ }
+ SetButton(keyboard_key_identifier, key_index, true);
+}
+
+void Keyboard::ReleaseKeyboardKey(int key_index) {
+ if (key_index == Settings::NativeKeyboard::None) {
+ return;
+ }
+ SetButton(keyboard_key_identifier, key_index, false);
+}
+
+void Keyboard::SetKeyboardModifiers(int key_modifiers) {
+ for (int i = 0; i < 32; ++i) {
+ bool key_value = ((key_modifiers >> i) & 0x1) != 0;
+ SetButton(keyboard_modifier_identifier, i, key_value);
+ // Use the modifier to press the key button equivalent
+ switch (i) {
+ case Settings::NativeKeyboard::LeftControl:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftControlKey, key_value);
+ break;
+ case Settings::NativeKeyboard::LeftShift:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftShiftKey, key_value);
+ break;
+ case Settings::NativeKeyboard::LeftAlt:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftAltKey, key_value);
+ break;
+ case Settings::NativeKeyboard::LeftMeta:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftMetaKey, key_value);
+ break;
+ case Settings::NativeKeyboard::RightControl:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightControlKey,
+ key_value);
+ break;
+ case Settings::NativeKeyboard::RightShift:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightShiftKey, key_value);
+ break;
+ case Settings::NativeKeyboard::RightAlt:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightAltKey, key_value);
+ break;
+ case Settings::NativeKeyboard::RightMeta:
+ SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightMetaKey, key_value);
+ break;
+ default:
+ // Other modifier keys should be pressed with PressKey since they stay enabled until
+ // next press
+ break;
+ }
+ }
+}
+
+void Keyboard::ReleaseAllKeys() {
+ ResetButtonState();
+}
+
+std::vector<Common::ParamPackage> Keyboard::GetInputDevices() const {
+ std::vector<Common::ParamPackage> devices;
+ devices.emplace_back(Common::ParamPackage{
+ {"engine", GetEngineName()},
+ {"display", "Keyboard Only"},
+ });
+ return devices;
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/keyboard.h b/src/input_common/drivers/keyboard.h
new file mode 100644
index 000000000..ad123b136
--- /dev/null
+++ b/src/input_common/drivers/keyboard.h
@@ -0,0 +1,56 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included
+
+#pragma once
+
+#include "input_common/input_engine.h"
+
+namespace InputCommon {
+
+/**
+ * A button device factory representing a keyboard. It receives keyboard events and forward them
+ * to all button devices it created.
+ */
+class Keyboard final : public InputCommon::InputEngine {
+public:
+ explicit Keyboard(const std::string& input_engine_);
+
+ /**
+ * Sets the status of all buttons bound with the key to pressed
+ * @param key_code the code of the key to press
+ */
+ void PressKey(int key_code);
+
+ /**
+ * Sets the status of all buttons bound with the key to released
+ * @param key_code the code of the key to release
+ */
+ void ReleaseKey(int key_code);
+
+ /**
+ * Sets the status of the keyboard key to pressed
+ * @param key_index index of the key to press
+ */
+ void PressKeyboardKey(int key_index);
+
+ /**
+ * Sets the status of the keyboard key to released
+ * @param key_index index of the key to release
+ */
+ void ReleaseKeyboardKey(int key_index);
+
+ /**
+ * Sets the status of all keyboard modifier keys
+ * @param key_modifiers the code of the key to release
+ */
+ void SetKeyboardModifiers(int key_modifiers);
+
+ /// Sets all keys to the non pressed state
+ void ReleaseAllKeys();
+
+ /// Used for automapping features
+ std::vector<Common::ParamPackage> GetInputDevices() const override;
+};
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/mouse.cpp b/src/input_common/drivers/mouse.cpp
new file mode 100644
index 000000000..752118e97
--- /dev/null
+++ b/src/input_common/drivers/mouse.cpp
@@ -0,0 +1,185 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included
+
+#include <stop_token>
+#include <thread>
+#include <fmt/format.h>
+
+#include "common/param_package.h"
+#include "common/settings.h"
+#include "common/thread.h"
+#include "input_common/drivers/mouse.h"
+
+namespace InputCommon {
+constexpr int mouse_axis_x = 0;
+constexpr int mouse_axis_y = 1;
+constexpr int wheel_axis_x = 2;
+constexpr int wheel_axis_y = 3;
+constexpr int touch_axis_x = 10;
+constexpr int touch_axis_y = 11;
+constexpr PadIdentifier identifier = {
+ .guid = Common::UUID{Common::INVALID_UUID},
+ .port = 0,
+ .pad = 0,
+};
+
+Mouse::Mouse(const std::string& input_engine_) : InputEngine(input_engine_) {
+ PreSetController(identifier);
+ PreSetAxis(identifier, mouse_axis_x);
+ PreSetAxis(identifier, mouse_axis_y);
+ PreSetAxis(identifier, wheel_axis_x);
+ PreSetAxis(identifier, wheel_axis_y);
+ PreSetAxis(identifier, touch_axis_x);
+ PreSetAxis(identifier, touch_axis_x);
+ update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
+}
+
+void Mouse::UpdateThread(std::stop_token stop_token) {
+ Common::SetCurrentThreadName("yuzu:input:Mouse");
+ constexpr int update_time = 10;
+ while (!stop_token.stop_requested()) {
+ if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
+ // Slow movement by 4%
+ last_mouse_change *= 0.96f;
+ const float sensitivity =
+ Settings::values.mouse_panning_sensitivity.GetValue() * 0.022f;
+ SetAxis(identifier, mouse_axis_x, last_mouse_change.x * sensitivity);
+ SetAxis(identifier, mouse_axis_y, -last_mouse_change.y * sensitivity);
+ }
+
+ if (mouse_panning_timout++ > 20) {
+ StopPanning();
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
+ }
+}
+
+void Mouse::MouseMove(int x, int y, f32 touch_x, f32 touch_y, int center_x, int center_y) {
+ // If native mouse is enabled just set the screen coordinates
+ if (Settings::values.mouse_enabled) {
+ SetAxis(identifier, mouse_axis_x, touch_x);
+ SetAxis(identifier, mouse_axis_y, touch_y);
+ return;
+ }
+
+ SetAxis(identifier, touch_axis_x, touch_x);
+ SetAxis(identifier, touch_axis_y, touch_y);
+
+ if (Settings::values.mouse_panning) {
+ auto mouse_change =
+ (Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
+ mouse_panning_timout = 0;
+
+ const auto move_distance = mouse_change.Length();
+ if (move_distance == 0) {
+ return;
+ }
+
+ // Make slow movements at least 3 units on lenght
+ if (move_distance < 3.0f) {
+ // Normalize value
+ mouse_change /= move_distance;
+ mouse_change *= 3.0f;
+ }
+
+ // Average mouse movements
+ last_mouse_change = (last_mouse_change * 0.91f) + (mouse_change * 0.09f);
+
+ const auto last_move_distance = last_mouse_change.Length();
+
+ // Make fast movements clamp to 8 units on lenght
+ if (last_move_distance > 8.0f) {
+ // Normalize value
+ last_mouse_change /= last_move_distance;
+ last_mouse_change *= 8.0f;
+ }
+
+ // Ignore average if it's less than 1 unit and use current movement value
+ if (last_move_distance < 1.0f) {
+ last_mouse_change = mouse_change / mouse_change.Length();
+ }
+
+ return;
+ }
+
+ if (button_pressed) {
+ const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
+ const float sensitivity = Settings::values.mouse_panning_sensitivity.GetValue() * 0.0012f;
+ SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * sensitivity);
+ SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * sensitivity);
+ }
+}
+
+void Mouse::PressButton(int x, int y, f32 touch_x, f32 touch_y, MouseButton button) {
+ SetAxis(identifier, touch_axis_x, touch_x);
+ SetAxis(identifier, touch_axis_y, touch_y);
+ SetButton(identifier, static_cast<int>(button), true);
+ // Set initial analog parameters
+ mouse_origin = {x, y};
+ last_mouse_position = {x, y};
+ button_pressed = true;
+}
+
+void Mouse::ReleaseButton(MouseButton button) {
+ SetButton(identifier, static_cast<int>(button), false);
+
+ if (!Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
+ SetAxis(identifier, mouse_axis_x, 0);
+ SetAxis(identifier, mouse_axis_y, 0);
+ }
+ button_pressed = false;
+}
+
+void Mouse::MouseWheelChange(int x, int y) {
+ wheel_position.x += x;
+ wheel_position.y += y;
+ SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
+ SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
+}
+
+void Mouse::ReleaseAllButtons() {
+ ResetButtonState();
+ button_pressed = false;
+}
+
+void Mouse::StopPanning() {
+ last_mouse_change = {};
+}
+
+std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
+ std::vector<Common::ParamPackage> devices;
+ devices.emplace_back(Common::ParamPackage{
+ {"engine", GetEngineName()},
+ {"display", "Keyboard/Mouse"},
+ });
+ return devices;
+}
+
+AnalogMapping Mouse::GetAnalogMappingForDevice(
+ [[maybe_unused]] const Common::ParamPackage& params) {
+ // Only overwrite different buttons from default
+ AnalogMapping mapping = {};
+ Common::ParamPackage right_analog_params;
+ right_analog_params.Set("engine", GetEngineName());
+ right_analog_params.Set("axis_x", 0);
+ right_analog_params.Set("axis_y", 1);
+ right_analog_params.Set("threshold", 0.5f);
+ right_analog_params.Set("range", 1.0f);
+ right_analog_params.Set("deadzone", 0.0f);
+ mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
+ return mapping;
+}
+
+Common::Input::ButtonNames Mouse::GetUIName(const Common::ParamPackage& params) const {
+ if (params.Has("button")) {
+ return Common::Input::ButtonNames::Value;
+ }
+ if (params.Has("axis")) {
+ return Common::Input::ButtonNames::Value;
+ }
+
+ return Common::Input::ButtonNames::Invalid;
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/mouse.h b/src/input_common/drivers/mouse.h
new file mode 100644
index 000000000..4a1fd2fd9
--- /dev/null
+++ b/src/input_common/drivers/mouse.h
@@ -0,0 +1,81 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included
+
+#pragma once
+
+#include <stop_token>
+#include <thread>
+
+#include "common/vector_math.h"
+#include "input_common/input_engine.h"
+
+namespace InputCommon {
+
+enum class MouseButton {
+ Left,
+ Right,
+ Wheel,
+ Backward,
+ Forward,
+ Task,
+ Extra,
+ Undefined,
+};
+
+/**
+ * A button device factory representing a keyboard. It receives keyboard events and forward them
+ * to all button devices it created.
+ */
+class Mouse final : public InputCommon::InputEngine {
+public:
+ explicit Mouse(const std::string& input_engine_);
+
+ /**
+ * Signals that mouse has moved.
+ * @param x the x-coordinate of the cursor
+ * @param y the y-coordinate of the cursor
+ * @param center_x the x-coordinate of the middle of the screen
+ * @param center_y the y-coordinate of the middle of the screen
+ */
+ void MouseMove(int x, int y, f32 touch_x, f32 touch_y, int center_x, int center_y);
+
+ /**
+ * Sets the status of all buttons bound with the key to pressed
+ * @param key_code the code of the key to press
+ */
+ void PressButton(int x, int y, f32 touch_x, f32 touch_y, MouseButton button);
+
+ /**
+ * Sets the status of all buttons bound with the key to released
+ * @param key_code the code of the key to release
+ */
+ void ReleaseButton(MouseButton button);
+
+ /**
+ * Sets the status of the mouse wheel
+ * @param x delta movement in the x direction
+ * @param y delta movement in the y direction
+ */
+ void MouseWheelChange(int x, int y);
+
+ void ReleaseAllButtons();
+
+ std::vector<Common::ParamPackage> GetInputDevices() const override;
+ AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
+ Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
+
+private:
+ void UpdateThread(std::stop_token stop_token);
+ void StopPanning();
+
+ Common::Vec2<int> mouse_origin;
+ Common::Vec2<int> last_mouse_position;
+ Common::Vec2<float> last_mouse_change;
+ Common::Vec2<int> wheel_position;
+ bool button_pressed;
+ int mouse_panning_timout{};
+ std::jthread update_thread;
+};
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp
new file mode 100644
index 000000000..90128b6cf
--- /dev/null
+++ b/src/input_common/drivers/sdl_driver.cpp
@@ -0,0 +1,924 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/logging/log.h"
+#include "common/math_util.h"
+#include "common/param_package.h"
+#include "common/settings.h"
+#include "common/thread.h"
+#include "common/vector_math.h"
+#include "input_common/drivers/sdl_driver.h"
+
+namespace InputCommon {
+
+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));
+ return guid_str;
+}
+} // Anonymous namespace
+
+static int SDLEventWatcher(void* user_data, SDL_Event* event) {
+ auto* const sdl_state = static_cast<SDLDriver*>(user_data);
+
+ sdl_state->HandleGameControllerEvent(*event);
+
+ return 0;
+}
+
+class SDLJoystick {
+public:
+ SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick,
+ SDL_GameController* game_controller)
+ : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose},
+ sdl_controller{game_controller, &SDL_GameControllerClose} {
+ EnableMotion();
+ }
+
+ void EnableMotion() {
+ if (sdl_controller) {
+ SDL_GameController* controller = sdl_controller.get();
+ if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) && !has_accel) {
+ SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
+ has_accel = true;
+ }
+ if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) && !has_gyro) {
+ SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
+ has_gyro = true;
+ }
+ }
+ }
+
+ bool HasGyro() const {
+ return has_gyro;
+ }
+
+ bool HasAccel() const {
+ return has_accel;
+ }
+
+ bool UpdateMotion(SDL_ControllerSensorEvent event) {
+ constexpr float gravity_constant = 9.80665f;
+ std::lock_guard lock{mutex};
+ const u64 time_difference = event.timestamp - last_motion_update;
+ last_motion_update = event.timestamp;
+ switch (event.sensor) {
+ case SDL_SENSOR_ACCEL: {
+ motion.accel_x = -event.data[0] / gravity_constant;
+ motion.accel_y = event.data[2] / gravity_constant;
+ motion.accel_z = -event.data[1] / gravity_constant;
+ break;
+ }
+ case SDL_SENSOR_GYRO: {
+ motion.gyro_x = event.data[0] / (Common::PI * 2);
+ motion.gyro_y = -event.data[2] / (Common::PI * 2);
+ motion.gyro_z = event.data[1] / (Common::PI * 2);
+ break;
+ }
+ }
+
+ // Ignore duplicated timestamps
+ if (time_difference == 0) {
+ return false;
+ }
+ motion.delta_timestamp = time_difference * 1000;
+ return true;
+ }
+
+ BasicMotion GetMotion() {
+ return motion;
+ }
+
+ bool RumblePlay(const Common::Input::VibrationStatus vibration) {
+ constexpr u32 rumble_max_duration_ms = 1000;
+ if (sdl_controller) {
+ return SDL_GameControllerRumble(
+ sdl_controller.get(), static_cast<u16>(vibration.low_amplitude),
+ static_cast<u16>(vibration.high_amplitude), rumble_max_duration_ms) != -1;
+ } else if (sdl_joystick) {
+ return SDL_JoystickRumble(sdl_joystick.get(), static_cast<u16>(vibration.low_amplitude),
+ static_cast<u16>(vibration.high_amplitude),
+ rumble_max_duration_ms) != -1;
+ }
+
+ return false;
+ }
+
+ bool HasHDRumble() const {
+ if (sdl_controller) {
+ return (SDL_GameControllerGetType(sdl_controller.get()) ==
+ SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO);
+ }
+ return false;
+ }
+ /**
+ * The Pad identifier of the joystick
+ */
+ const PadIdentifier GetPadIdentifier() const {
+ return {
+ .guid = Common::UUID{guid},
+ .port = static_cast<std::size_t>(port),
+ .pad = 0,
+ };
+ }
+
+ /**
+ * The guid of the joystick
+ */
+ const std::string& GetGUID() const {
+ return guid;
+ }
+
+ /**
+ * The number of joystick from the same type that were connected before this joystick
+ */
+ int GetPort() const {
+ return port;
+ }
+
+ SDL_Joystick* GetSDLJoystick() const {
+ return sdl_joystick.get();
+ }
+
+ SDL_GameController* GetSDLGameController() const {
+ return sdl_controller.get();
+ }
+
+ void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) {
+ sdl_joystick.reset(joystick);
+ sdl_controller.reset(controller);
+ }
+
+ bool IsJoyconLeft() const {
+ const std::string controller_name = GetControllerName();
+ if (std::strstr(controller_name.c_str(), "Joy-Con Left") != nullptr) {
+ return true;
+ }
+ if (std::strstr(controller_name.c_str(), "Joy-Con (L)") != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
+ bool IsJoyconRight() const {
+ const std::string controller_name = GetControllerName();
+ if (std::strstr(controller_name.c_str(), "Joy-Con Right") != nullptr) {
+ return true;
+ }
+ if (std::strstr(controller_name.c_str(), "Joy-Con (R)") != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
+ BatteryLevel GetBatteryLevel() {
+ const auto level = SDL_JoystickCurrentPowerLevel(sdl_joystick.get());
+ switch (level) {
+ case SDL_JOYSTICK_POWER_EMPTY:
+ return BatteryLevel::Empty;
+ case SDL_JOYSTICK_POWER_LOW:
+ return BatteryLevel::Critical;
+ case SDL_JOYSTICK_POWER_MEDIUM:
+ return BatteryLevel::Low;
+ case SDL_JOYSTICK_POWER_FULL:
+ return BatteryLevel::Medium;
+ case SDL_JOYSTICK_POWER_MAX:
+ return BatteryLevel::Full;
+ case SDL_JOYSTICK_POWER_UNKNOWN:
+ case SDL_JOYSTICK_POWER_WIRED:
+ default:
+ return BatteryLevel::Charging;
+ }
+ }
+
+ std::string GetControllerName() const {
+ if (sdl_controller) {
+ switch (SDL_GameControllerGetType(sdl_controller.get())) {
+ case SDL_CONTROLLER_TYPE_XBOX360:
+ return "XBox 360 Controller";
+ case SDL_CONTROLLER_TYPE_XBOXONE:
+ return "XBox One Controller";
+ default:
+ break;
+ }
+ const auto name = SDL_GameControllerName(sdl_controller.get());
+ if (name) {
+ return name;
+ }
+ }
+
+ if (sdl_joystick) {
+ const auto name = SDL_JoystickName(sdl_joystick.get());
+ if (name) {
+ return name;
+ }
+ }
+
+ return "Unknown";
+ }
+
+private:
+ std::string guid;
+ int port;
+ std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)> sdl_joystick;
+ std::unique_ptr<SDL_GameController, decltype(&SDL_GameControllerClose)> sdl_controller;
+ mutable std::mutex mutex;
+
+ u64 last_motion_update{};
+ bool has_gyro{false};
+ bool has_accel{false};
+ BasicMotion motion;
+};
+
+std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGUID(const std::string& guid, int port) {
+ std::lock_guard lock{joystick_map_mutex};
+ const auto it = joystick_map.find(guid);
+
+ if (it != joystick_map.end()) {
+ while (it->second.size() <= static_cast<std::size_t>(port)) {
+ 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[static_cast<std::size_t>(port)];
+ }
+
+ auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr, nullptr);
+
+ return joystick_map[guid].emplace_back(std::move(joystick));
+}
+
+std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) {
+ auto sdl_joystick = SDL_JoystickFromInstanceID(sdl_id);
+ const std::string guid = GetGUID(sdl_joystick);
+
+ std::lock_guard lock{joystick_map_mutex};
+ const auto map_it = joystick_map.find(guid);
+
+ if (map_it == joystick_map.end()) {
+ return nullptr;
+ }
+
+ const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(),
+ [&sdl_joystick](const auto& joystick) {
+ return joystick->GetSDLJoystick() == sdl_joystick;
+ });
+
+ if (vec_it == map_it->second.end()) {
+ return nullptr;
+ }
+
+ return *vec_it;
+}
+
+void SDLDriver::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);
+ 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, sdl_gamecontroller);
+ PreSetController(joystick->GetPadIdentifier());
+ SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
+ joystick_map[guid].emplace_back(std::move(joystick));
+ return;
+ }
+
+ auto& joystick_guid_list = joystick_map[guid];
+ const auto joystick_it =
+ std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
+ [](const auto& joystick) { return !joystick->GetSDLJoystick(); });
+
+ if (joystick_it != joystick_guid_list.end()) {
+ (*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, sdl_gamecontroller);
+ PreSetController(joystick->GetPadIdentifier());
+ SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
+ joystick_guid_list.emplace_back(std::move(joystick));
+}
+
+void SDLDriver::CloseJoystick(SDL_Joystick* sdl_joystick) {
+ const std::string guid = GetGUID(sdl_joystick);
+
+ std::lock_guard lock{joystick_map_mutex};
+ // This call to guid is safe since the joystick is guaranteed to be in the map
+ const auto& joystick_guid_list = joystick_map[guid];
+ const auto joystick_it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
+ [&sdl_joystick](const auto& joystick) {
+ return joystick->GetSDLJoystick() == sdl_joystick;
+ });
+
+ if (joystick_it != joystick_guid_list.end()) {
+ (*joystick_it)->SetSDLJoystick(nullptr, nullptr);
+ }
+}
+
+void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
+ switch (event.type) {
+ case SDL_JOYBUTTONUP: {
+ if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
+ const PadIdentifier identifier = joystick->GetPadIdentifier();
+ SetButton(identifier, event.jbutton.button, false);
+ }
+ break;
+ }
+ case SDL_JOYBUTTONDOWN: {
+ if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
+ const PadIdentifier identifier = joystick->GetPadIdentifier();
+ SetButton(identifier, event.jbutton.button, true);
+ }
+ break;
+ }
+ case SDL_JOYHATMOTION: {
+ if (const auto joystick = GetSDLJoystickBySDLID(event.jhat.which)) {
+ const PadIdentifier identifier = joystick->GetPadIdentifier();
+ SetHatButton(identifier, event.jhat.hat, event.jhat.value);
+ }
+ break;
+ }
+ case SDL_JOYAXISMOTION: {
+ if (const auto joystick = GetSDLJoystickBySDLID(event.jaxis.which)) {
+ const PadIdentifier identifier = joystick->GetPadIdentifier();
+ SetAxis(identifier, event.jaxis.axis, event.jaxis.value / 32767.0f);
+ }
+ break;
+ }
+ case SDL_CONTROLLERSENSORUPDATE: {
+ if (auto joystick = GetSDLJoystickBySDLID(event.csensor.which)) {
+ if (joystick->UpdateMotion(event.csensor)) {
+ const PadIdentifier identifier = joystick->GetPadIdentifier();
+ SetMotion(identifier, 0, joystick->GetMotion());
+ };
+ }
+ break;
+ }
+ case SDL_JOYDEVICEREMOVED:
+ LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which);
+ CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which));
+ break;
+ case SDL_JOYDEVICEADDED:
+ LOG_DEBUG(Input, "Controller connected with device index {}", event.jdevice.which);
+ InitJoystick(event.jdevice.which);
+ break;
+ }
+}
+
+void SDLDriver::CloseJoysticks() {
+ std::lock_guard lock{joystick_map_mutex};
+ joystick_map.clear();
+}
+
+SDLDriver::SDLDriver(const std::string& input_engine_) : InputEngine(input_engine_) {
+ Common::SetCurrentThreadName("yuzu:input:SDL");
+
+ if (!Settings::values.enable_raw_input) {
+ // Disable raw input. When enabled this setting causes SDL to die when a web applet opens
+ SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
+ }
+
+ // Prevent SDL from adding undesired axis
+ SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
+
+ // Enable HIDAPI rumble. This prevents SDL from disabling motion on PS4 and PS5 controllers
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
+ SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
+
+ // Use hidapi driver for joycons. This will allow joycons to be detected as a GameController and
+ // not a generic one
+ SDL_SetHint("SDL_JOYSTICK_HIDAPI_JOY_CONS", "1");
+
+ // Turn off Pro controller home led
+ SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED", "0");
+
+ // If the frontend is going to manage the event loop, then we don't start one here
+ start_thread = SDL_WasInit(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) == 0;
+ if (start_thread && SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) {
+ LOG_CRITICAL(Input, "SDL_Init failed with: {}", SDL_GetError());
+ return;
+ }
+
+ SDL_AddEventWatch(&SDLEventWatcher, this);
+
+ initialized = true;
+ if (start_thread) {
+ poll_thread = std::thread([this] {
+ using namespace std::chrono_literals;
+ while (initialized) {
+ SDL_PumpEvents();
+ std::this_thread::sleep_for(1ms);
+ }
+ });
+ }
+ // Because the events for joystick connection happens before we have our event watcher added, we
+ // can just open all the joysticks right here
+ for (int i = 0; i < SDL_NumJoysticks(); ++i) {
+ InitJoystick(i);
+ }
+}
+
+SDLDriver::~SDLDriver() {
+ CloseJoysticks();
+ SDL_DelEventWatch(&SDLEventWatcher, this);
+
+ initialized = false;
+ if (start_thread) {
+ poll_thread.join();
+ SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
+ }
+}
+
+std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
+ std::vector<Common::ParamPackage> devices;
+ std::unordered_map<int, std::shared_ptr<SDLJoystick>> joycon_pairs;
+ for (const auto& [key, value] : joystick_map) {
+ for (const auto& joystick : value) {
+ if (!joystick->GetSDLJoystick()) {
+ continue;
+ }
+ const std::string name =
+ fmt::format("{} {}", joystick->GetControllerName(), joystick->GetPort());
+ devices.emplace_back(Common::ParamPackage{
+ {"engine", GetEngineName()},
+ {"display", std::move(name)},
+ {"guid", joystick->GetGUID()},
+ {"port", std::to_string(joystick->GetPort())},
+ });
+ if (joystick->IsJoyconLeft()) {
+ joycon_pairs.insert_or_assign(joystick->GetPort(), joystick);
+ }
+ }
+ }
+
+ // Add dual controllers
+ for (const auto& [key, value] : joystick_map) {
+ for (const auto& joystick : value) {
+ if (joystick->IsJoyconRight()) {
+ if (!joycon_pairs.contains(joystick->GetPort())) {
+ continue;
+ }
+ const auto joystick2 = joycon_pairs.at(joystick->GetPort());
+
+ const std::string name =
+ fmt::format("{} {}", "Nintendo Dual Joy-Con", joystick->GetPort());
+ devices.emplace_back(Common::ParamPackage{
+ {"engine", GetEngineName()},
+ {"display", std::move(name)},
+ {"guid", joystick->GetGUID()},
+ {"guid2", joystick2->GetGUID()},
+ {"port", std::to_string(joystick->GetPort())},
+ });
+ }
+ }
+ }
+ return devices;
+}
+Common::Input::VibrationError SDLDriver::SetRumble(const PadIdentifier& identifier,
+ const Common::Input::VibrationStatus vibration) {
+ const auto joystick =
+ GetSDLJoystickByGUID(identifier.guid.Format(), static_cast<int>(identifier.port));
+ const auto process_amplitude_exp = [](f32 amplitude, f32 factor) {
+ return (amplitude + std::pow(amplitude, factor)) * 0.5f * 0xFFFF;
+ };
+
+ // Default exponential curve for rumble
+ f32 factor = 0.35f;
+
+ // If vibration is set as a linear output use a flatter value
+ if (vibration.type == Common::Input::VibrationAmplificationType::Linear) {
+ factor = 0.5f;
+ }
+
+ // Amplitude for HD rumble needs no modification
+ if (joystick->HasHDRumble()) {
+ factor = 1.0f;
+ }
+
+ const Common::Input::VibrationStatus new_vibration{
+ .low_amplitude = process_amplitude_exp(vibration.low_amplitude, factor),
+ .low_frequency = vibration.low_frequency,
+ .high_amplitude = process_amplitude_exp(vibration.high_amplitude, factor),
+ .high_frequency = vibration.high_frequency,
+ .type = Common::Input::VibrationAmplificationType::Exponential,
+ };
+
+ if (!joystick->RumblePlay(new_vibration)) {
+ return Common::Input::VibrationError::Unknown;
+ }
+
+ return Common::Input::VibrationError::None;
+}
+Common::ParamPackage SDLDriver::BuildAnalogParamPackageForButton(int port, std::string guid,
+ s32 axis, float value) const {
+ Common::ParamPackage params{};
+ params.Set("engine", GetEngineName());
+ params.Set("port", port);
+ params.Set("guid", std::move(guid));
+ params.Set("axis", axis);
+ params.Set("threshold", "0.5");
+ params.Set("invert", value < 0 ? "-" : "+");
+ return params;
+}
+
+Common::ParamPackage SDLDriver::BuildButtonParamPackageForButton(int port, std::string guid,
+ s32 button) const {
+ Common::ParamPackage params{};
+ params.Set("engine", GetEngineName());
+ params.Set("port", port);
+ params.Set("guid", std::move(guid));
+ params.Set("button", button);
+ return params;
+}
+
+Common::ParamPackage SDLDriver::BuildHatParamPackageForButton(int port, std::string guid, s32 hat,
+ u8 value) const {
+ Common::ParamPackage params{};
+ params.Set("engine", GetEngineName());
+ params.Set("port", port);
+ params.Set("guid", std::move(guid));
+ params.Set("hat", hat);
+ params.Set("direction", GetHatButtonName(value));
+ return params;
+}
+
+Common::ParamPackage SDLDriver::BuildMotionParam(int port, std::string guid) const {
+ Common::ParamPackage params{};
+ params.Set("engine", GetEngineName());
+ params.Set("motion", 0);
+ params.Set("port", port);
+ params.Set("guid", std::move(guid));
+ return params;
+}
+
+Common::ParamPackage SDLDriver::BuildParamPackageForBinding(
+ int port, const std::string& guid, const SDL_GameControllerButtonBind& binding) const {
+ switch (binding.bindType) {
+ case SDL_CONTROLLER_BINDTYPE_NONE:
+ break;
+ 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,
+ static_cast<u8>(binding.value.hat.hat_mask));
+ }
+ return {};
+}
+
+Common::ParamPackage SDLDriver::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
+ int axis_y, float offset_x,
+ float offset_y) const {
+ Common::ParamPackage params;
+ params.Set("engine", GetEngineName());
+ params.Set("port", static_cast<int>(identifier.port));
+ params.Set("guid", identifier.guid.Format());
+ params.Set("axis_x", axis_x);
+ params.Set("axis_y", axis_y);
+ params.Set("offset_x", offset_x);
+ params.Set("offset_y", offset_y);
+ params.Set("invert_x", "+");
+ params.Set("invert_y", "+");
+ return params;
+}
+
+ButtonMapping SDLDriver::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
+ ButtonBindings switch_to_sdl_button;
+
+ if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) {
+ switch_to_sdl_button = GetNintendoButtonBinding(joystick);
+ } else {
+ switch_to_sdl_button = GetDefaultButtonBinding();
+ }
+
+ // Add the missing bindings for ZL/ZR
+ static constexpr ZButtonBindings switch_to_sdl_axis{{
+ {Settings::NativeButton::ZL, SDL_CONTROLLER_AXIS_TRIGGERLEFT},
+ {Settings::NativeButton::ZR, SDL_CONTROLLER_AXIS_TRIGGERRIGHT},
+ }};
+
+ // Parameters contain two joysticks return dual
+ if (params.Has("guid2")) {
+ const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
+
+ if (joystick2->GetSDLGameController() != nullptr) {
+ return GetDualControllerMapping(joystick, joystick2, switch_to_sdl_button,
+ switch_to_sdl_axis);
+ }
+ }
+
+ return GetSingleControllerMapping(joystick, switch_to_sdl_button, switch_to_sdl_axis);
+}
+
+ButtonBindings SDLDriver::GetDefaultButtonBinding() const {
+ return {
+ std::pair{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},
+ };
+}
+
+ButtonBindings SDLDriver::GetNintendoButtonBinding(
+ const std::shared_ptr<SDLJoystick>& joystick) const {
+ // Default SL/SR mapping for pro controllers
+ auto sl_button = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
+ auto sr_button = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
+
+ if (joystick->IsJoyconLeft()) {
+ sl_button = SDL_CONTROLLER_BUTTON_PADDLE2;
+ sr_button = SDL_CONTROLLER_BUTTON_PADDLE4;
+ }
+ if (joystick->IsJoyconRight()) {
+ sl_button = SDL_CONTROLLER_BUTTON_PADDLE3;
+ sr_button = SDL_CONTROLLER_BUTTON_PADDLE1;
+ }
+
+ return {
+ std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_A},
+ {Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_B},
+ {Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_X},
+ {Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_Y},
+ {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, sl_button},
+ {Settings::NativeButton::SR, sr_button},
+ {Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
+ };
+}
+
+ButtonMapping SDLDriver::GetSingleControllerMapping(
+ const std::shared_ptr<SDLJoystick>& joystick, const ButtonBindings& switch_to_sdl_button,
+ const ZButtonBindings& switch_to_sdl_axis) const {
+ ButtonMapping mapping;
+ mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
+ auto* controller = joystick->GetSDLGameController();
+
+ 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;
+}
+
+ButtonMapping SDLDriver::GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
+ const std::shared_ptr<SDLJoystick>& joystick2,
+ const ButtonBindings& switch_to_sdl_button,
+ const ZButtonBindings& switch_to_sdl_axis) const {
+ ButtonMapping mapping;
+ mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
+ auto* controller = joystick->GetSDLGameController();
+ auto* controller2 = joystick2->GetSDLGameController();
+
+ for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
+ if (IsButtonOnLeftSide(switch_button)) {
+ const auto& binding = SDL_GameControllerGetBindForButton(controller2, sdl_button);
+ mapping.insert_or_assign(
+ switch_button,
+ BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
+ continue;
+ }
+ 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) {
+ if (IsButtonOnLeftSide(switch_button)) {
+ const auto& binding = SDL_GameControllerGetBindForAxis(controller2, sdl_axis);
+ mapping.insert_or_assign(
+ switch_button,
+ BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
+ continue;
+ }
+ const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis);
+ mapping.insert_or_assign(
+ switch_button,
+ BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
+ }
+
+ return mapping;
+}
+
+bool SDLDriver::IsButtonOnLeftSide(Settings::NativeButton::Values button) const {
+ switch (button) {
+ case Settings::NativeButton::DDown:
+ case Settings::NativeButton::DLeft:
+ case Settings::NativeButton::DRight:
+ case Settings::NativeButton::DUp:
+ case Settings::NativeButton::L:
+ case Settings::NativeButton::LStick:
+ case Settings::NativeButton::Minus:
+ case Settings::NativeButton::Screenshot:
+ case Settings::NativeButton::ZL:
+ return true;
+ default:
+ return false;
+ }
+}
+
+AnalogMapping SDLDriver::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
+ if (!params.Has("guid") || !params.Has("port")) {
+ return {};
+ }
+ const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
+ const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), 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);
+ if (params.Has("guid2")) {
+ const auto identifier = joystick2->GetPadIdentifier();
+ PreSetController(identifier);
+ PreSetAxis(identifier, binding_left_x.value.axis);
+ PreSetAxis(identifier, binding_left_y.value.axis);
+ const auto left_offset_x = -GetAxis(identifier, binding_left_x.value.axis);
+ const auto left_offset_y = -GetAxis(identifier, binding_left_y.value.axis);
+ mapping.insert_or_assign(Settings::NativeAnalog::LStick,
+ BuildParamPackageForAnalog(identifier, binding_left_x.value.axis,
+ binding_left_y.value.axis,
+ left_offset_x, left_offset_y));
+ } else {
+ const auto identifier = joystick->GetPadIdentifier();
+ PreSetController(identifier);
+ PreSetAxis(identifier, binding_left_x.value.axis);
+ PreSetAxis(identifier, binding_left_y.value.axis);
+ const auto left_offset_x = -GetAxis(identifier, binding_left_x.value.axis);
+ const auto left_offset_y = -GetAxis(identifier, binding_left_y.value.axis);
+ mapping.insert_or_assign(Settings::NativeAnalog::LStick,
+ BuildParamPackageForAnalog(identifier, binding_left_x.value.axis,
+ binding_left_y.value.axis,
+ left_offset_x, left_offset_y));
+ }
+ const auto& binding_right_x =
+ SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
+ const auto& binding_right_y =
+ SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
+ const auto identifier = joystick->GetPadIdentifier();
+ PreSetController(identifier);
+ PreSetAxis(identifier, binding_right_x.value.axis);
+ PreSetAxis(identifier, binding_right_y.value.axis);
+ const auto right_offset_x = -GetAxis(identifier, binding_right_x.value.axis);
+ const auto right_offset_y = -GetAxis(identifier, binding_right_y.value.axis);
+ mapping.insert_or_assign(Settings::NativeAnalog::RStick,
+ BuildParamPackageForAnalog(identifier, binding_right_x.value.axis,
+ binding_right_y.value.axis, right_offset_x,
+ right_offset_y));
+ return mapping;
+}
+
+MotionMapping SDLDriver::GetMotionMappingForDevice(const Common::ParamPackage& params) {
+ if (!params.Has("guid") || !params.Has("port")) {
+ return {};
+ }
+ const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
+ const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
+ auto* controller = joystick->GetSDLGameController();
+ if (controller == nullptr) {
+ return {};
+ }
+
+ MotionMapping mapping = {};
+ joystick->EnableMotion();
+
+ if (joystick->HasGyro() || joystick->HasAccel()) {
+ mapping.insert_or_assign(Settings::NativeMotion::MotionRight,
+ BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
+ }
+ if (params.Has("guid2")) {
+ joystick2->EnableMotion();
+ if (joystick2->HasGyro() || joystick2->HasAccel()) {
+ mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
+ BuildMotionParam(joystick2->GetPort(), joystick2->GetGUID()));
+ }
+ } else {
+ if (joystick->HasGyro() || joystick->HasAccel()) {
+ mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
+ BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
+ }
+ }
+
+ return mapping;
+}
+
+Common::Input::ButtonNames SDLDriver::GetUIName(const Common::ParamPackage& params) const {
+ if (params.Has("button")) {
+ // TODO(German77): Find how to substitue the values for real button names
+ return Common::Input::ButtonNames::Value;
+ }
+ if (params.Has("hat")) {
+ return Common::Input::ButtonNames::Value;
+ }
+ if (params.Has("axis")) {
+ return Common::Input::ButtonNames::Value;
+ }
+ if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
+ return Common::Input::ButtonNames::Value;
+ }
+ if (params.Has("motion")) {
+ return Common::Input::ButtonNames::Engine;
+ }
+
+ return Common::Input::ButtonNames::Invalid;
+}
+
+std::string SDLDriver::GetHatButtonName(u8 direction_value) const {
+ switch (direction_value) {
+ case SDL_HAT_UP:
+ return "up";
+ case SDL_HAT_DOWN:
+ return "down";
+ case SDL_HAT_LEFT:
+ return "left";
+ case SDL_HAT_RIGHT:
+ return "right";
+ default:
+ return {};
+ }
+}
+
+u8 SDLDriver::GetHatButtonId(const std::string& direction_name) const {
+ 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;
+ }
+ return direction;
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/sdl_driver.h b/src/input_common/drivers/sdl_driver.h
new file mode 100644
index 000000000..d03ff4b84
--- /dev/null
+++ b/src/input_common/drivers/sdl_driver.h
@@ -0,0 +1,117 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <atomic>
+#include <mutex>
+#include <thread>
+#include <unordered_map>
+
+#include <SDL.h>
+
+#include "common/common_types.h"
+#include "input_common/input_engine.h"
+
+union SDL_Event;
+using SDL_GameController = struct _SDL_GameController;
+using SDL_Joystick = struct _SDL_Joystick;
+using SDL_JoystickID = s32;
+
+using ButtonBindings =
+ std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 17>;
+using ZButtonBindings =
+ std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
+
+namespace InputCommon {
+
+class SDLJoystick;
+
+class SDLDriver : public InputCommon::InputEngine {
+public:
+ /// Initializes and registers SDL device factories
+ SDLDriver(const std::string& input_engine_);
+
+ /// Unregisters SDL device factories and shut them down.
+ ~SDLDriver() override;
+
+ /// Handle SDL_Events for joysticks from SDL_PollEvent
+ void HandleGameControllerEvent(const SDL_Event& event);
+
+ /// Get the nth joystick with the corresponding GUID
+ std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
+
+ /**
+ * Check how many identical joysticks (by guid) were connected before the one with sdl_id and so
+ * tie it to a SDLJoystick with the same guid and that port
+ */
+ std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const std::string& guid, int port);
+
+ std::vector<Common::ParamPackage> GetInputDevices() const override;
+
+ ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
+ AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
+ MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
+ Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
+
+ std::string GetHatButtonName(u8 direction_value) const override;
+ u8 GetHatButtonId(const std::string& direction_name) const override;
+
+ Common::Input::VibrationError SetRumble(
+ const PadIdentifier& identifier, const Common::Input::VibrationStatus vibration) override;
+
+private:
+ void InitJoystick(int joystick_index);
+ void CloseJoystick(SDL_Joystick* sdl_joystick);
+
+ /// Needs to be called before SDL_QuitSubSystem.
+ void CloseJoysticks();
+
+ Common::ParamPackage BuildAnalogParamPackageForButton(int port, std::string guid, s32 axis,
+ float value = 0.1f) const;
+ Common::ParamPackage BuildButtonParamPackageForButton(int port, std::string guid,
+ s32 button) const;
+
+ Common::ParamPackage BuildHatParamPackageForButton(int port, std::string guid, s32 hat,
+ u8 value) const;
+
+ Common::ParamPackage BuildMotionParam(int port, std::string guid) const;
+
+ Common::ParamPackage BuildParamPackageForBinding(
+ int port, const std::string& guid, const SDL_GameControllerButtonBind& binding) const;
+
+ Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
+ int axis_y, float offset_x,
+ float offset_y) const;
+
+ /// Returns the default button bindings list for generic controllers
+ ButtonBindings GetDefaultButtonBinding() const;
+
+ /// Returns the default button bindings list for nintendo controllers
+ ButtonBindings GetNintendoButtonBinding(const std::shared_ptr<SDLJoystick>& joystick) const;
+
+ /// Returns the button mappings from a single controller
+ ButtonMapping GetSingleControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
+ const ButtonBindings& switch_to_sdl_button,
+ const ZButtonBindings& switch_to_sdl_axis) const;
+
+ /// Returns the button mappings from two different controllers
+ ButtonMapping GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
+ const std::shared_ptr<SDLJoystick>& joystick2,
+ const ButtonBindings& switch_to_sdl_button,
+ const ZButtonBindings& switch_to_sdl_axis) const;
+
+ /// Returns true if the button is on the left joycon
+ bool IsButtonOnLeftSide(Settings::NativeButton::Values button) const;
+
+ /// 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;
+
+ bool start_thread = false;
+ std::atomic<bool> initialized = false;
+
+ std::thread poll_thread;
+};
+} // namespace InputCommon
diff --git a/src/input_common/drivers/tas_input.cpp b/src/input_common/drivers/tas_input.cpp
new file mode 100644
index 000000000..0e01fb0d9
--- /dev/null
+++ b/src/input_common/drivers/tas_input.cpp
@@ -0,0 +1,315 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include <cstring>
+#include <regex>
+#include <fmt/format.h>
+
+#include "common/fs/file.h"
+#include "common/fs/fs_types.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+#include "input_common/drivers/tas_input.h"
+
+namespace InputCommon::TasInput {
+
+enum TasAxes : u8 {
+ StickX,
+ StickY,
+ SubstickX,
+ SubstickY,
+ Undefined,
+};
+
+// Supported keywords and buttons from a TAS file
+constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_button = {
+ std::pair{"KEY_A", TasButton::BUTTON_A},
+ {"KEY_B", TasButton::BUTTON_B},
+ {"KEY_X", TasButton::BUTTON_X},
+ {"KEY_Y", TasButton::BUTTON_Y},
+ {"KEY_LSTICK", TasButton::STICK_L},
+ {"KEY_RSTICK", TasButton::STICK_R},
+ {"KEY_L", TasButton::TRIGGER_L},
+ {"KEY_R", TasButton::TRIGGER_R},
+ {"KEY_PLUS", TasButton::BUTTON_PLUS},
+ {"KEY_MINUS", TasButton::BUTTON_MINUS},
+ {"KEY_DLEFT", TasButton::BUTTON_LEFT},
+ {"KEY_DUP", TasButton::BUTTON_UP},
+ {"KEY_DRIGHT", TasButton::BUTTON_RIGHT},
+ {"KEY_DDOWN", TasButton::BUTTON_DOWN},
+ {"KEY_SL", TasButton::BUTTON_SL},
+ {"KEY_SR", TasButton::BUTTON_SR},
+ {"KEY_CAPTURE", TasButton::BUTTON_CAPTURE},
+ {"KEY_HOME", TasButton::BUTTON_HOME},
+ {"KEY_ZL", TasButton::TRIGGER_ZL},
+ {"KEY_ZR", TasButton::TRIGGER_ZR},
+};
+
+Tas::Tas(const std::string& input_engine_) : InputCommon::InputEngine(input_engine_) {
+ for (size_t player_index = 0; player_index < PLAYER_NUMBER; player_index++) {
+ PadIdentifier identifier{
+ .guid = Common::UUID{},
+ .port = player_index,
+ .pad = 0,
+ };
+ PreSetController(identifier);
+ }
+ ClearInput();
+ if (!Settings::values.tas_enable) {
+ needs_reset = true;
+ return;
+ }
+ LoadTasFiles();
+}
+
+Tas::~Tas() {
+ Stop();
+};
+
+void Tas::LoadTasFiles() {
+ script_length = 0;
+ for (size_t i = 0; i < commands.size(); i++) {
+ LoadTasFile(i, 0);
+ if (commands[i].size() > script_length) {
+ script_length = commands[i].size();
+ }
+ }
+}
+
+void Tas::LoadTasFile(size_t player_index, size_t file_index) {
+ if (!commands[player_index].empty()) {
+ commands[player_index].clear();
+ }
+ std::string file = Common::FS::ReadStringFromFile(
+ Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) /
+ fmt::format("script{}-{}.txt", file_index, player_index + 1),
+ Common::FS::FileType::BinaryFile);
+ std::stringstream command_line(file);
+ std::string line;
+ int frame_no = 0;
+ while (std::getline(command_line, line, '\n')) {
+ if (line.empty()) {
+ continue;
+ }
+ std::smatch m;
+
+ std::stringstream linestream(line);
+ std::string segment;
+ std::vector<std::string> seglist;
+
+ while (std::getline(linestream, segment, ' ')) {
+ seglist.push_back(segment);
+ }
+
+ if (seglist.size() < 4) {
+ continue;
+ }
+
+ while (frame_no < std::stoi(seglist.at(0))) {
+ commands[player_index].push_back({});
+ frame_no++;
+ }
+
+ TASCommand command = {
+ .buttons = ReadCommandButtons(seglist.at(1)),
+ .l_axis = ReadCommandAxis(seglist.at(2)),
+ .r_axis = ReadCommandAxis(seglist.at(3)),
+ };
+ commands[player_index].push_back(command);
+ frame_no++;
+ }
+ LOG_INFO(Input, "TAS file loaded! {} frames", frame_no);
+}
+
+void Tas::WriteTasFile(std::u8string file_name) {
+ std::string output_text;
+ for (size_t frame = 0; frame < record_commands.size(); frame++) {
+ const TASCommand& line = record_commands[frame];
+ output_text += fmt::format("{} {} {} {}\n", frame, WriteCommandButtons(line.buttons),
+ WriteCommandAxis(line.l_axis), WriteCommandAxis(line.r_axis));
+ }
+ const auto bytes_written = Common::FS::WriteStringToFile(
+ Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name,
+ Common::FS::FileType::TextFile, output_text);
+ if (bytes_written == output_text.size()) {
+ LOG_INFO(Input, "TAS file written to file!");
+ } else {
+ LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written,
+ output_text.size());
+ }
+}
+
+void Tas::RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis) {
+ last_input = {
+ .buttons = buttons,
+ .l_axis = left_axis,
+ .r_axis = right_axis,
+ };
+}
+
+std::tuple<TasState, size_t, size_t> Tas::GetStatus() const {
+ TasState state;
+ if (is_recording) {
+ return {TasState::Recording, 0, record_commands.size()};
+ }
+
+ if (is_running) {
+ state = TasState::Running;
+ } else {
+ state = TasState::Stopped;
+ }
+
+ return {state, current_command, script_length};
+}
+
+void Tas::UpdateThread() {
+ if (!Settings::values.tas_enable) {
+ if (is_running) {
+ Stop();
+ }
+ return;
+ }
+
+ if (is_recording) {
+ record_commands.push_back(last_input);
+ }
+ if (needs_reset) {
+ current_command = 0;
+ needs_reset = false;
+ LoadTasFiles();
+ LOG_DEBUG(Input, "tas_reset done");
+ }
+
+ if (!is_running) {
+ ClearInput();
+ return;
+ }
+ if (current_command < script_length) {
+ LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length);
+ const size_t frame = current_command++;
+ for (size_t player_index = 0; player_index < commands.size(); player_index++) {
+ TASCommand command{};
+ if (frame < commands[player_index].size()) {
+ command = commands[player_index][frame];
+ }
+
+ PadIdentifier identifier{
+ .guid = Common::UUID{},
+ .port = player_index,
+ .pad = 0,
+ };
+ for (std::size_t i = 0; i < sizeof(command.buttons) * 8; ++i) {
+ const bool button_status = (command.buttons & (1LLU << i)) != 0;
+ const int button = static_cast<int>(i);
+ SetButton(identifier, button, button_status);
+ }
+ SetAxis(identifier, TasAxes::StickX, command.l_axis.x);
+ SetAxis(identifier, TasAxes::StickY, command.l_axis.y);
+ SetAxis(identifier, TasAxes::SubstickX, command.r_axis.x);
+ SetAxis(identifier, TasAxes::SubstickY, command.r_axis.y);
+ }
+ } else {
+ is_running = Settings::values.tas_loop.GetValue();
+ LoadTasFiles();
+ current_command = 0;
+ ClearInput();
+ }
+}
+
+void Tas::ClearInput() {
+ ResetButtonState();
+ ResetAnalogState();
+}
+
+TasAnalog Tas::ReadCommandAxis(const std::string& line) const {
+ std::stringstream linestream(line);
+ std::string segment;
+ std::vector<std::string> seglist;
+
+ while (std::getline(linestream, segment, ';')) {
+ seglist.push_back(segment);
+ }
+
+ const float x = std::stof(seglist.at(0)) / 32767.0f;
+ const float y = std::stof(seglist.at(1)) / 32767.0f;
+
+ return {x, y};
+}
+
+u64 Tas::ReadCommandButtons(const std::string& data) const {
+ std::stringstream button_text(data);
+ std::string line;
+ u64 buttons = 0;
+ while (std::getline(button_text, line, ';')) {
+ for (auto [text, tas_button] : text_to_tas_button) {
+ if (text == line) {
+ buttons |= static_cast<u64>(tas_button);
+ break;
+ }
+ }
+ }
+ return buttons;
+}
+
+std::string Tas::WriteCommandButtons(u64 buttons) const {
+ std::string returns = "";
+ for (auto [text_button, tas_button] : text_to_tas_button) {
+ if ((buttons & static_cast<u64>(tas_button)) != 0) {
+ returns += fmt::format("{};", text_button);
+ }
+ }
+ return returns.empty() ? "NONE" : returns;
+}
+
+std::string Tas::WriteCommandAxis(TasAnalog analog) const {
+ return fmt::format("{};{}", analog.x * 32767, analog.y * 32767);
+}
+
+void Tas::StartStop() {
+ if (!Settings::values.tas_enable) {
+ return;
+ }
+ if (is_running) {
+ Stop();
+ } else {
+ is_running = true;
+ }
+}
+
+void Tas::Stop() {
+ is_running = false;
+}
+
+void Tas::Reset() {
+ if (!Settings::values.tas_enable) {
+ return;
+ }
+ needs_reset = true;
+}
+
+bool Tas::Record() {
+ if (!Settings::values.tas_enable) {
+ return true;
+ }
+ is_recording = !is_recording;
+ return is_recording;
+}
+
+void Tas::SaveRecording(bool overwrite_file) {
+ if (is_recording) {
+ return;
+ }
+ if (record_commands.empty()) {
+ return;
+ }
+ WriteTasFile(u8"record.txt");
+ if (overwrite_file) {
+ WriteTasFile(u8"script0-1.txt");
+ }
+ needs_reset = true;
+ record_commands.clear();
+}
+
+} // namespace InputCommon::TasInput
diff --git a/src/input_common/drivers/tas_input.h b/src/input_common/drivers/tas_input.h
new file mode 100644
index 000000000..c95a130fc
--- /dev/null
+++ b/src/input_common/drivers/tas_input.h
@@ -0,0 +1,194 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+
+#include "common/common_types.h"
+#include "common/settings_input.h"
+#include "input_common/input_engine.h"
+#include "input_common/main.h"
+
+/*
+To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below
+Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt
+for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players).
+
+A script file has the same format as TAS-nx uses, so final files will look like this:
+
+1 KEY_B 0;0 0;0
+6 KEY_ZL 0;0 0;0
+41 KEY_ZL;KEY_Y 0;0 0;0
+43 KEY_X;KEY_A 32767;0 0;0
+44 KEY_A 32767;0 0;0
+45 KEY_A 32767;0 0;0
+46 KEY_A 32767;0 0;0
+47 KEY_A 32767;0 0;0
+
+After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey
+CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file
+has. Playback can be started or stopped using CTRL+F5.
+
+However, for playback to actually work, the correct input device has to be selected: In the Controls
+menu, select TAS from the device list for the controller that the script should be played on.
+
+Recording a new script file is really simple: Just make sure that the proper device (not TAS) is
+connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke
+again (CTRL+F7). The new script will be saved at the location previously selected, as the filename
+record.txt.
+
+For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller
+P1).
+*/
+
+namespace InputCommon::TasInput {
+
+constexpr size_t PLAYER_NUMBER = 10;
+
+enum class TasButton : u64 {
+ BUTTON_A = 1U << 0,
+ BUTTON_B = 1U << 1,
+ BUTTON_X = 1U << 2,
+ BUTTON_Y = 1U << 3,
+ STICK_L = 1U << 4,
+ STICK_R = 1U << 5,
+ TRIGGER_L = 1U << 6,
+ TRIGGER_R = 1U << 7,
+ TRIGGER_ZL = 1U << 8,
+ TRIGGER_ZR = 1U << 9,
+ BUTTON_PLUS = 1U << 10,
+ BUTTON_MINUS = 1U << 11,
+ BUTTON_LEFT = 1U << 12,
+ BUTTON_UP = 1U << 13,
+ BUTTON_RIGHT = 1U << 14,
+ BUTTON_DOWN = 1U << 15,
+ BUTTON_SL = 1U << 16,
+ BUTTON_SR = 1U << 17,
+ BUTTON_HOME = 1U << 18,
+ BUTTON_CAPTURE = 1U << 19,
+};
+
+struct TasAnalog {
+ float x{};
+ float y{};
+};
+
+enum class TasState {
+ Running,
+ Recording,
+ Stopped,
+};
+
+class Tas final : public InputCommon::InputEngine {
+public:
+ explicit Tas(const std::string& input_engine_);
+ ~Tas();
+
+ /**
+ * Changes the input status that will be stored in each frame
+ * @param buttons: bitfield with the status of the buttons
+ * @param left_axis: value of the left axis
+ * @param right_axis: value of the right axis
+ */
+ void RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis);
+
+ // Main loop that records or executes input
+ void UpdateThread();
+
+ // Sets the flag to start or stop the TAS command excecution and swaps controllers profiles
+ void StartStop();
+
+ // Stop the TAS and reverts any controller profile
+ void Stop();
+
+ // Sets the flag to reload the file and start from the begining in the next update
+ void Reset();
+
+ /**
+ * Sets the flag to enable or disable recording of inputs
+ * @return Returns true if the current recording status is enabled
+ */
+ bool Record();
+
+ /**
+ * Saves contents of record_commands on a file
+ * @param overwrite_file: Indicates if player 1 should be overwritten
+ */
+ void SaveRecording(bool overwrite_file);
+
+ /**
+ * Returns the current status values of TAS playback/recording
+ * @return Tuple of
+ * TasState indicating the current state out of Running ;
+ * Current playback progress ;
+ * Total length of script file currently loaded or being recorded
+ */
+ std::tuple<TasState, size_t, size_t> GetStatus() const;
+
+private:
+ struct TASCommand {
+ u64 buttons{};
+ TasAnalog l_axis{};
+ TasAnalog r_axis{};
+ };
+
+ /// Loads TAS files from all players
+ void LoadTasFiles();
+
+ /** Loads TAS file from the specified player
+ * @param player_index: player number to save the script
+ * @param file_index: script number of the file
+ */
+ void LoadTasFile(size_t player_index, size_t file_index);
+
+ /** Writes a TAS file from the recorded commands
+ * @param file_name: name of the file to be written
+ */
+ void WriteTasFile(std::u8string file_name);
+
+ /**
+ * Parses a string containing the axis values. X and Y have a range from -32767 to 32767
+ * @param line: string containing axis values with the following format "x;y"
+ * @return Returns a TAS analog object with axis values with range from -1.0 to 1.0
+ */
+ TasAnalog ReadCommandAxis(const std::string& line) const;
+
+ /**
+ * Parses a string containing the button values. Each button is represented by it's text format
+ * specified in text_to_tas_button array
+ * @param line: string containing button name with the following format "a;b;c;d..."
+ * @return Returns a u64 with each bit representing the status of a button
+ */
+ u64 ReadCommandButtons(const std::string& line) const;
+
+ /**
+ * Reset state of all players
+ */
+ void ClearInput();
+
+ /**
+ * Converts an u64 containing the button status into the text equivalent
+ * @param buttons: bitfield with the status of the buttons
+ * @return Returns a string with the name of the buttons to be written to the file
+ */
+ std::string WriteCommandButtons(u64 buttons) const;
+
+ /**
+ * Converts an TAS analog object containing the axis status into the text equivalent
+ * @param data: value of the axis
+ * @return A string with the value of the axis to be written to the file
+ */
+ std::string WriteCommandAxis(TasAnalog data) const;
+
+ size_t script_length{0};
+ bool is_recording{false};
+ bool is_running{false};
+ bool needs_reset{false};
+ std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
+ std::vector<TASCommand> record_commands{};
+ size_t current_command{0};
+ TASCommand last_input{}; // only used for recording
+};
+} // namespace InputCommon::TasInput
diff --git a/src/input_common/drivers/touch_screen.cpp b/src/input_common/drivers/touch_screen.cpp
new file mode 100644
index 000000000..45b3086f6
--- /dev/null
+++ b/src/input_common/drivers/touch_screen.cpp
@@ -0,0 +1,53 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included
+
+#include "common/param_package.h"
+#include "input_common/drivers/touch_screen.h"
+
+namespace InputCommon {
+
+constexpr PadIdentifier identifier = {
+ .guid = Common::UUID{Common::INVALID_UUID},
+ .port = 0,
+ .pad = 0,
+};
+
+TouchScreen::TouchScreen(const std::string& input_engine_) : InputEngine(input_engine_) {
+ PreSetController(identifier);
+}
+
+void TouchScreen::TouchMoved(float x, float y, std::size_t finger) {
+ if (finger >= 16) {
+ return;
+ }
+ TouchPressed(x, y, finger);
+}
+
+void TouchScreen::TouchPressed(float x, float y, std::size_t finger) {
+ if (finger >= 16) {
+ return;
+ }
+ SetButton(identifier, static_cast<int>(finger), true);
+ SetAxis(identifier, static_cast<int>(finger * 2), x);
+ SetAxis(identifier, static_cast<int>(finger * 2 + 1), y);
+}
+
+void TouchScreen::TouchReleased(std::size_t finger) {
+ if (finger >= 16) {
+ return;
+ }
+ SetButton(identifier, static_cast<int>(finger), false);
+ SetAxis(identifier, static_cast<int>(finger * 2), 0.0f);
+ SetAxis(identifier, static_cast<int>(finger * 2 + 1), 0.0f);
+}
+
+void TouchScreen::ReleaseAllTouch() {
+ for (int index = 0; index < 16; ++index) {
+ SetButton(identifier, index, false);
+ SetAxis(identifier, index * 2, 0.0f);
+ SetAxis(identifier, index * 2 + 1, 0.0f);
+ }
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/touch_screen.h b/src/input_common/drivers/touch_screen.h
new file mode 100644
index 000000000..25c11e8bf
--- /dev/null
+++ b/src/input_common/drivers/touch_screen.h
@@ -0,0 +1,44 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included
+
+#pragma once
+
+#include "input_common/input_engine.h"
+
+namespace InputCommon {
+
+/**
+ * A button device factory representing a keyboard. It receives keyboard events and forward them
+ * to all button devices it created.
+ */
+class TouchScreen final : public InputCommon::InputEngine {
+public:
+ explicit TouchScreen(const std::string& input_engine_);
+
+ /**
+ * Signals that mouse has moved.
+ * @param x the x-coordinate of the cursor
+ * @param y the y-coordinate of the cursor
+ * @param center_x the x-coordinate of the middle of the screen
+ * @param center_y the y-coordinate of the middle of the screen
+ */
+ void TouchMoved(float x, float y, std::size_t finger);
+
+ /**
+ * Sets the status of all buttons bound with the key to pressed
+ * @param key_code the code of the key to press
+ */
+ void TouchPressed(float x, float y, std::size_t finger);
+
+ /**
+ * Sets the status of all buttons bound with the key to released
+ * @param key_code the code of the key to release
+ */
+ void TouchReleased(std::size_t finger);
+
+ /// Resets all inputs to their initial value
+ void ReleaseAllTouch();
+};
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp
new file mode 100644
index 000000000..fdee0f2d5
--- /dev/null
+++ b/src/input_common/drivers/udp_client.cpp
@@ -0,0 +1,591 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <random>
+#include <boost/asio.hpp>
+#include <fmt/format.h>
+
+#include "common/logging/log.h"
+#include "common/param_package.h"
+#include "common/settings.h"
+#include "input_common/drivers/udp_client.h"
+#include "input_common/helpers/udp_protocol.h"
+
+using boost::asio::ip::udp;
+
+namespace InputCommon::CemuhookUDP {
+
+struct SocketCallback {
+ std::function<void(Response::Version)> version;
+ std::function<void(Response::PortInfo)> port_info;
+ std::function<void(Response::PadData)> pad_data;
+};
+
+class Socket {
+public:
+ using clock = std::chrono::system_clock;
+
+ explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
+ : callback(std::move(callback_)), timer(io_service),
+ socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
+ boost::system::error_code ec{};
+ auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
+ if (ec.value() != boost::system::errc::success) {
+ LOG_ERROR(Input, "Invalid IPv4 address \"{}\" provided to socket", host);
+ ipv4 = boost::asio::ip::address_v4{};
+ }
+
+ send_endpoint = {udp::endpoint(ipv4, port)};
+ }
+
+ void Stop() {
+ io_service.stop();
+ }
+
+ void Loop() {
+ io_service.run();
+ }
+
+ void StartSend(const clock::time_point& from) {
+ timer.expires_at(from + std::chrono::seconds(3));
+ timer.async_wait([this](const boost::system::error_code& error) { HandleSend(error); });
+ }
+
+ void StartReceive() {
+ socket.async_receive_from(
+ boost::asio::buffer(receive_buffer), receive_endpoint,
+ [this](const boost::system::error_code& error, std::size_t bytes_transferred) {
+ HandleReceive(error, bytes_transferred);
+ });
+ }
+
+private:
+ u32 GenerateRandomClientId() const {
+ std::random_device device;
+ return device();
+ }
+
+ void HandleReceive(const boost::system::error_code&, std::size_t bytes_transferred) {
+ if (auto type = Response::Validate(receive_buffer.data(), bytes_transferred)) {
+ switch (*type) {
+ case Type::Version: {
+ Response::Version version;
+ std::memcpy(&version, &receive_buffer[sizeof(Header)], sizeof(Response::Version));
+ callback.version(std::move(version));
+ break;
+ }
+ case Type::PortInfo: {
+ Response::PortInfo port_info;
+ std::memcpy(&port_info, &receive_buffer[sizeof(Header)],
+ sizeof(Response::PortInfo));
+ callback.port_info(std::move(port_info));
+ break;
+ }
+ case Type::PadData: {
+ Response::PadData pad_data;
+ std::memcpy(&pad_data, &receive_buffer[sizeof(Header)], sizeof(Response::PadData));
+ callback.pad_data(std::move(pad_data));
+ break;
+ }
+ }
+ }
+ StartReceive();
+ }
+
+ void HandleSend(const boost::system::error_code&) {
+ boost::system::error_code _ignored{};
+ // Send a request for getting port info for the pad
+ const Request::PortInfo port_info{4, {0, 1, 2, 3}};
+ const auto port_message = Request::Create(port_info, client_id);
+ std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
+ socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint, {}, _ignored);
+
+ // Send a request for getting pad data for the pad
+ const Request::PadData pad_data{
+ Request::RegisterFlags::AllPads,
+ 0,
+ EMPTY_MAC_ADDRESS,
+ };
+ const auto pad_message = Request::Create(pad_data, client_id);
+ std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
+ socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint, {}, _ignored);
+ StartSend(timer.expiry());
+ }
+
+ SocketCallback callback;
+ boost::asio::io_service io_service;
+ boost::asio::basic_waitable_timer<clock> timer;
+ udp::socket socket;
+
+ const u32 client_id;
+
+ static constexpr std::size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>);
+ static constexpr std::size_t PAD_DATA_SIZE = sizeof(Message<Request::PadData>);
+ std::array<u8, PORT_INFO_SIZE> send_buffer1;
+ std::array<u8, PAD_DATA_SIZE> send_buffer2;
+ udp::endpoint send_endpoint;
+
+ std::array<u8, MAX_PACKET_SIZE> receive_buffer;
+ udp::endpoint receive_endpoint;
+};
+
+static void SocketLoop(Socket* socket) {
+ socket->StartReceive();
+ socket->StartSend(Socket::clock::now());
+ socket->Loop();
+}
+
+UDPClient::UDPClient(const std::string& input_engine_) : InputEngine(input_engine_) {
+ LOG_INFO(Input, "Udp Initialization started");
+ ReloadSockets();
+}
+
+UDPClient::~UDPClient() {
+ Reset();
+}
+
+UDPClient::ClientConnection::ClientConnection() = default;
+
+UDPClient::ClientConnection::~ClientConnection() = default;
+
+void UDPClient::ReloadSockets() {
+ Reset();
+
+ std::stringstream servers_ss(Settings::values.udp_input_servers.GetValue());
+ std::string server_token;
+ std::size_t client = 0;
+ while (std::getline(servers_ss, server_token, ',')) {
+ if (client == MAX_UDP_CLIENTS) {
+ break;
+ }
+ std::stringstream server_ss(server_token);
+ std::string token;
+ std::getline(server_ss, token, ':');
+ std::string udp_input_address = token;
+ std::getline(server_ss, token, ':');
+ char* temp;
+ const u16 udp_input_port = static_cast<u16>(std::strtol(token.c_str(), &temp, 0));
+ if (*temp != '\0') {
+ LOG_ERROR(Input, "Port number is not valid {}", token);
+ continue;
+ }
+
+ const std::size_t client_number = GetClientNumber(udp_input_address, udp_input_port);
+ if (client_number != MAX_UDP_CLIENTS) {
+ LOG_ERROR(Input, "Duplicated UDP servers found");
+ continue;
+ }
+ StartCommunication(client++, udp_input_address, udp_input_port);
+ }
+}
+
+std::size_t UDPClient::GetClientNumber(std::string_view host, u16 port) const {
+ for (std::size_t client = 0; client < clients.size(); client++) {
+ if (clients[client].active == -1) {
+ continue;
+ }
+ if (clients[client].host == host && clients[client].port == port) {
+ return client;
+ }
+ }
+ return MAX_UDP_CLIENTS;
+}
+
+void UDPClient::OnVersion([[maybe_unused]] Response::Version data) {
+ LOG_TRACE(Input, "Version packet received: {}", data.version);
+}
+
+void UDPClient::OnPortInfo([[maybe_unused]] Response::PortInfo data) {
+ LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
+}
+
+void UDPClient::OnPadData(Response::PadData data, std::size_t client) {
+ const std::size_t pad_index = (client * PADS_PER_CLIENT) + data.info.id;
+
+ if (pad_index >= pads.size()) {
+ LOG_ERROR(Input, "Invalid pad id {}", data.info.id);
+ return;
+ }
+
+ LOG_TRACE(Input, "PadData packet received");
+ if (data.packet_counter == pads[pad_index].packet_sequence) {
+ LOG_WARNING(
+ Input,
+ "PadData packet dropped because its stale info. Current count: {} Packet count: {}",
+ pads[pad_index].packet_sequence, data.packet_counter);
+ pads[pad_index].connected = false;
+ return;
+ }
+
+ clients[client].active = 1;
+ pads[pad_index].connected = true;
+ pads[pad_index].packet_sequence = data.packet_counter;
+
+ const auto now = std::chrono::steady_clock::now();
+ const auto time_difference = static_cast<u64>(
+ std::chrono::duration_cast<std::chrono::microseconds>(now - pads[pad_index].last_update)
+ .count());
+ pads[pad_index].last_update = now;
+
+ // 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
+ const float gyro_scale = 1.0f / 312.0f;
+
+ const BasicMotion motion{
+ .gyro_x = data.gyro.pitch * gyro_scale,
+ .gyro_y = data.gyro.roll * gyro_scale,
+ .gyro_z = -data.gyro.yaw * gyro_scale,
+ .accel_x = data.accel.x,
+ .accel_y = -data.accel.z,
+ .accel_z = data.accel.y,
+ .delta_timestamp = time_difference,
+ };
+ const PadIdentifier identifier = GetPadIdentifier(pad_index);
+ SetMotion(identifier, 0, motion);
+
+ for (std::size_t id = 0; id < data.touch.size(); ++id) {
+ const auto touch_pad = data.touch[id];
+ const auto touch_axis_x_id =
+ static_cast<int>(id == 0 ? PadAxes::Touch1X : PadAxes::Touch2X);
+ const auto touch_axis_y_id =
+ static_cast<int>(id == 0 ? PadAxes::Touch1Y : PadAxes::Touch2Y);
+ const auto touch_button_id =
+ static_cast<int>(id == 0 ? PadButton::Touch1 : PadButton::touch2);
+
+ // TODO: Use custom calibration per device
+ const Common::ParamPackage touch_param(Settings::values.touch_device.GetValue());
+ const u16 min_x = static_cast<u16>(touch_param.Get("min_x", 100));
+ const u16 min_y = static_cast<u16>(touch_param.Get("min_y", 50));
+ const u16 max_x = static_cast<u16>(touch_param.Get("max_x", 1800));
+ const u16 max_y = static_cast<u16>(touch_param.Get("max_y", 850));
+
+ const f32 x =
+ static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.x), min_x, max_x) - min_x) /
+ static_cast<f32>(max_x - min_x);
+ const f32 y =
+ static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.y), min_y, max_y) - min_y) /
+ static_cast<f32>(max_y - min_y);
+
+ if (touch_pad.is_active) {
+ SetAxis(identifier, touch_axis_x_id, x);
+ SetAxis(identifier, touch_axis_y_id, y);
+ SetButton(identifier, touch_button_id, true);
+ continue;
+ }
+ SetAxis(identifier, touch_axis_x_id, 0);
+ SetAxis(identifier, touch_axis_y_id, 0);
+ SetButton(identifier, touch_button_id, false);
+ }
+
+ SetAxis(identifier, static_cast<int>(PadAxes::LeftStickX),
+ (data.left_stick_x - 127.0f) / 127.0f);
+ SetAxis(identifier, static_cast<int>(PadAxes::LeftStickY),
+ (data.left_stick_y - 127.0f) / 127.0f);
+ SetAxis(identifier, static_cast<int>(PadAxes::RightStickX),
+ (data.right_stick_x - 127.0f) / 127.0f);
+ SetAxis(identifier, static_cast<int>(PadAxes::RightStickY),
+ (data.right_stick_y - 127.0f) / 127.0f);
+
+ static constexpr std::array<PadButton, 16> buttons{
+ PadButton::Share, PadButton::L3, PadButton::R3, PadButton::Options,
+ PadButton::Up, PadButton::Right, PadButton::Down, PadButton::Left,
+ PadButton::L2, PadButton::R2, PadButton::L1, PadButton::R1,
+ PadButton::Triangle, PadButton::Circle, PadButton::Cross, PadButton::Square};
+
+ for (std::size_t i = 0; i < buttons.size(); ++i) {
+ const bool button_status = (data.digital_button & (1U << i)) != 0;
+ const int button = static_cast<int>(buttons[i]);
+ SetButton(identifier, button, button_status);
+ }
+}
+
+void UDPClient::StartCommunication(std::size_t client, const std::string& host, u16 port) {
+ SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
+ [this](Response::PortInfo info) { OnPortInfo(info); },
+ [this, client](Response::PadData data) { OnPadData(data, client); }};
+ LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
+ clients[client].uuid = GetHostUUID(host);
+ clients[client].host = host;
+ clients[client].port = port;
+ clients[client].active = 0;
+ clients[client].socket = std::make_unique<Socket>(host, port, callback);
+ clients[client].thread = std::thread{SocketLoop, clients[client].socket.get()};
+ for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
+ const PadIdentifier identifier = GetPadIdentifier(client * PADS_PER_CLIENT + index);
+ PreSetController(identifier);
+ }
+}
+
+const PadIdentifier UDPClient::GetPadIdentifier(std::size_t pad_index) const {
+ const std::size_t client = pad_index / PADS_PER_CLIENT;
+ return {
+ .guid = clients[client].uuid,
+ .port = static_cast<std::size_t>(clients[client].port),
+ .pad = pad_index,
+ };
+}
+
+const Common::UUID UDPClient::GetHostUUID(const std::string host) const {
+ const auto ip = boost::asio::ip::address_v4::from_string(host);
+ const auto hex_host = fmt::format("{:06x}", ip.to_ulong());
+ return Common::UUID{hex_host};
+}
+
+void UDPClient::Reset() {
+ for (auto& client : clients) {
+ if (client.thread.joinable()) {
+ client.active = -1;
+ client.socket->Stop();
+ client.thread.join();
+ }
+ }
+}
+
+std::vector<Common::ParamPackage> UDPClient::GetInputDevices() const {
+ std::vector<Common::ParamPackage> devices;
+ if (!Settings::values.enable_udp_controller) {
+ return devices;
+ }
+ for (std::size_t client = 0; client < clients.size(); client++) {
+ if (clients[client].active != 1) {
+ continue;
+ }
+ for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
+ const std::size_t pad_index = client * PADS_PER_CLIENT + index;
+ if (!pads[pad_index].connected) {
+ continue;
+ }
+ const auto pad_identifier = GetPadIdentifier(pad_index);
+ Common::ParamPackage identifier{};
+ identifier.Set("engine", GetEngineName());
+ identifier.Set("display", fmt::format("UDP Controller {}", pad_identifier.pad));
+ identifier.Set("guid", pad_identifier.guid.Format());
+ identifier.Set("port", static_cast<int>(pad_identifier.port));
+ identifier.Set("pad", static_cast<int>(pad_identifier.pad));
+ devices.emplace_back(identifier);
+ }
+ }
+ return devices;
+}
+
+ButtonMapping UDPClient::GetButtonMappingForDevice(const Common::ParamPackage& params) {
+ // This list excludes any button that can't be really mapped
+ static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 18>
+ switch_to_dsu_button = {
+ std::pair{Settings::NativeButton::A, PadButton::Circle},
+ {Settings::NativeButton::B, PadButton::Cross},
+ {Settings::NativeButton::X, PadButton::Triangle},
+ {Settings::NativeButton::Y, PadButton::Square},
+ {Settings::NativeButton::Plus, PadButton::Options},
+ {Settings::NativeButton::Minus, PadButton::Share},
+ {Settings::NativeButton::DLeft, PadButton::Left},
+ {Settings::NativeButton::DUp, PadButton::Up},
+ {Settings::NativeButton::DRight, PadButton::Right},
+ {Settings::NativeButton::DDown, PadButton::Down},
+ {Settings::NativeButton::L, PadButton::L1},
+ {Settings::NativeButton::R, PadButton::R1},
+ {Settings::NativeButton::ZL, PadButton::L2},
+ {Settings::NativeButton::ZR, PadButton::R2},
+ {Settings::NativeButton::SL, PadButton::L2},
+ {Settings::NativeButton::SR, PadButton::R2},
+ {Settings::NativeButton::LStick, PadButton::L3},
+ {Settings::NativeButton::RStick, PadButton::R3},
+ };
+ if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
+ return {};
+ }
+
+ ButtonMapping mapping{};
+ for (const auto& [switch_button, dsu_button] : switch_to_dsu_button) {
+ Common::ParamPackage button_params{};
+ button_params.Set("engine", GetEngineName());
+ button_params.Set("guid", params.Get("guid", ""));
+ button_params.Set("port", params.Get("port", 0));
+ button_params.Set("pad", params.Get("pad", 0));
+ button_params.Set("button", static_cast<int>(dsu_button));
+ mapping.insert_or_assign(switch_button, std::move(button_params));
+ }
+
+ return mapping;
+}
+
+AnalogMapping UDPClient::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
+ if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
+ return {};
+ }
+
+ AnalogMapping mapping = {};
+ Common::ParamPackage left_analog_params;
+ left_analog_params.Set("engine", GetEngineName());
+ left_analog_params.Set("guid", params.Get("guid", ""));
+ left_analog_params.Set("port", params.Get("port", 0));
+ left_analog_params.Set("pad", params.Get("pad", 0));
+ left_analog_params.Set("axis_x", static_cast<int>(PadAxes::LeftStickX));
+ left_analog_params.Set("axis_y", static_cast<int>(PadAxes::LeftStickY));
+ mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
+ Common::ParamPackage right_analog_params;
+ right_analog_params.Set("engine", GetEngineName());
+ right_analog_params.Set("guid", params.Get("guid", ""));
+ right_analog_params.Set("port", params.Get("port", 0));
+ right_analog_params.Set("pad", params.Get("pad", 0));
+ right_analog_params.Set("axis_x", static_cast<int>(PadAxes::RightStickX));
+ right_analog_params.Set("axis_y", static_cast<int>(PadAxes::RightStickY));
+ mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
+ return mapping;
+}
+
+MotionMapping UDPClient::GetMotionMappingForDevice(const Common::ParamPackage& params) {
+ if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
+ return {};
+ }
+
+ MotionMapping mapping = {};
+ Common::ParamPackage motion_params;
+ motion_params.Set("engine", GetEngineName());
+ motion_params.Set("guid", params.Get("guid", ""));
+ motion_params.Set("port", params.Get("port", 0));
+ motion_params.Set("pad", params.Get("pad", 0));
+ motion_params.Set("motion", 0);
+ mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(motion_params));
+ mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(motion_params));
+ return mapping;
+}
+
+Common::Input::ButtonNames UDPClient::GetUIButtonName(const Common::ParamPackage& params) const {
+ PadButton button = static_cast<PadButton>(params.Get("button", 0));
+ switch (button) {
+ case PadButton::Left:
+ return Common::Input::ButtonNames::ButtonLeft;
+ case PadButton::Right:
+ return Common::Input::ButtonNames::ButtonRight;
+ case PadButton::Down:
+ return Common::Input::ButtonNames::ButtonDown;
+ case PadButton::Up:
+ return Common::Input::ButtonNames::ButtonUp;
+ case PadButton::L1:
+ return Common::Input::ButtonNames::L1;
+ case PadButton::L2:
+ return Common::Input::ButtonNames::L2;
+ case PadButton::L3:
+ return Common::Input::ButtonNames::L3;
+ case PadButton::R1:
+ return Common::Input::ButtonNames::R1;
+ case PadButton::R2:
+ return Common::Input::ButtonNames::R2;
+ case PadButton::R3:
+ return Common::Input::ButtonNames::R3;
+ case PadButton::Circle:
+ return Common::Input::ButtonNames::Circle;
+ case PadButton::Cross:
+ return Common::Input::ButtonNames::Cross;
+ case PadButton::Square:
+ return Common::Input::ButtonNames::Square;
+ case PadButton::Triangle:
+ return Common::Input::ButtonNames::Triangle;
+ case PadButton::Share:
+ return Common::Input::ButtonNames::Share;
+ case PadButton::Options:
+ return Common::Input::ButtonNames::Options;
+ default:
+ return Common::Input::ButtonNames::Undefined;
+ }
+}
+
+Common::Input::ButtonNames UDPClient::GetUIName(const Common::ParamPackage& params) const {
+ if (params.Has("button")) {
+ return GetUIButtonName(params);
+ }
+ if (params.Has("axis")) {
+ return Common::Input::ButtonNames::Value;
+ }
+ if (params.Has("motion")) {
+ return Common::Input::ButtonNames::Engine;
+ }
+
+ return Common::Input::ButtonNames::Invalid;
+}
+
+void TestCommunication(const std::string& host, u16 port,
+ const std::function<void()>& success_callback,
+ const std::function<void()>& failure_callback) {
+ std::thread([=] {
+ Common::Event success_event;
+ SocketCallback callback{
+ .version = [](Response::Version) {},
+ .port_info = [](Response::PortInfo) {},
+ .pad_data = [&](Response::PadData) { success_event.Set(); },
+ };
+ Socket socket{host, port, std::move(callback)};
+ std::thread worker_thread{SocketLoop, &socket};
+ const bool result =
+ success_event.WaitUntil(std::chrono::steady_clock::now() + std::chrono::seconds(10));
+ socket.Stop();
+ worker_thread.join();
+ if (result) {
+ success_callback();
+ } else {
+ failure_callback();
+ }
+ }).detach();
+}
+
+CalibrationConfigurationJob::CalibrationConfigurationJob(
+ const std::string& host, u16 port, std::function<void(Status)> status_callback,
+ std::function<void(u16, u16, u16, u16)> data_callback) {
+
+ std::thread([=, this] {
+ Status current_status{Status::Initialized};
+ SocketCallback callback{
+ [](Response::Version) {}, [](Response::PortInfo) {},
+ [&](Response::PadData data) {
+ static constexpr u16 CALIBRATION_THRESHOLD = 100;
+ static constexpr u16 MAX_VALUE = UINT16_MAX;
+
+ if (current_status == Status::Initialized) {
+ // Receiving data means the communication is ready now
+ current_status = Status::Ready;
+ status_callback(current_status);
+ }
+ const auto& touchpad_0 = data.touch[0];
+ if (touchpad_0.is_active == 0) {
+ return;
+ }
+ LOG_DEBUG(Input, "Current touch: {} {}", touchpad_0.x, touchpad_0.y);
+ const u16 min_x = std::min(MAX_VALUE, static_cast<u16>(touchpad_0.x));
+ const u16 min_y = std::min(MAX_VALUE, static_cast<u16>(touchpad_0.y));
+ if (current_status == Status::Ready) {
+ // First touch - min data (min_x/min_y)
+ current_status = Status::Stage1Completed;
+ status_callback(current_status);
+ }
+ if (touchpad_0.x - min_x > CALIBRATION_THRESHOLD &&
+ touchpad_0.y - min_y > CALIBRATION_THRESHOLD) {
+ // Set the current position as max value and finishes configuration
+ const u16 max_x = touchpad_0.x;
+ const u16 max_y = touchpad_0.y;
+ current_status = Status::Completed;
+ data_callback(min_x, min_y, max_x, max_y);
+ status_callback(current_status);
+
+ complete_event.Set();
+ }
+ }};
+ Socket socket{host, port, std::move(callback)};
+ std::thread worker_thread{SocketLoop, &socket};
+ complete_event.Wait();
+ socket.Stop();
+ worker_thread.join();
+ }).detach();
+}
+
+CalibrationConfigurationJob::~CalibrationConfigurationJob() {
+ Stop();
+}
+
+void CalibrationConfigurationJob::Stop() {
+ complete_event.Set();
+}
+
+} // namespace InputCommon::CemuhookUDP
diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h
new file mode 100644
index 000000000..5d483f26b
--- /dev/null
+++ b/src/input_common/drivers/udp_client.h
@@ -0,0 +1,185 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <optional>
+
+#include "common/common_types.h"
+#include "common/thread.h"
+#include "input_common/input_engine.h"
+
+namespace InputCommon::CemuhookUDP {
+
+class Socket;
+
+namespace Response {
+struct PadData;
+struct PortInfo;
+struct TouchPad;
+struct Version;
+} // namespace Response
+
+enum class PadTouch {
+ Click,
+ Undefined,
+};
+
+struct UDPPadStatus {
+ std::string host{"127.0.0.1"};
+ u16 port{26760};
+ std::size_t pad_index{};
+};
+
+struct DeviceStatus {
+ std::mutex update_mutex;
+
+ // calibration data for scaling the device's touch area to 3ds
+ struct CalibrationData {
+ u16 min_x{};
+ u16 min_y{};
+ u16 max_x{};
+ u16 max_y{};
+ };
+ std::optional<CalibrationData> touch_calibration;
+};
+
+/**
+ * A button device factory representing a keyboard. It receives keyboard events and forward them
+ * to all button devices it created.
+ */
+class UDPClient final : public InputCommon::InputEngine {
+public:
+ explicit UDPClient(const std::string& input_engine_);
+ ~UDPClient();
+
+ void ReloadSockets();
+
+ /// Used for automapping features
+ std::vector<Common::ParamPackage> GetInputDevices() const override;
+ ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
+ AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
+ MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
+ Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
+
+private:
+ enum class PadButton {
+ Undefined = 0x0000,
+ Share = 0x0001,
+ L3 = 0x0002,
+ R3 = 0x0004,
+ Options = 0x0008,
+ Up = 0x0010,
+ Right = 0x0020,
+ Down = 0x0040,
+ Left = 0x0080,
+ L2 = 0x0100,
+ R2 = 0x0200,
+ L1 = 0x0400,
+ R1 = 0x0800,
+ Triangle = 0x1000,
+ Circle = 0x2000,
+ Cross = 0x4000,
+ Square = 0x8000,
+ Touch1 = 0x10000,
+ touch2 = 0x20000,
+ };
+
+ enum class PadAxes : u8 {
+ LeftStickX,
+ LeftStickY,
+ RightStickX,
+ RightStickY,
+ AnalogLeft,
+ AnalogDown,
+ AnalogRight,
+ AnalogUp,
+ AnalogSquare,
+ AnalogCross,
+ AnalogCircle,
+ AnalogTriangle,
+ AnalogR1,
+ AnalogL1,
+ AnalogR2,
+ AnalogL3,
+ AnalogR3,
+ Touch1X,
+ Touch1Y,
+ Touch2X,
+ Touch2Y,
+ Undefined,
+ };
+
+ struct PadData {
+ std::size_t pad_index{};
+ bool connected{};
+ DeviceStatus status;
+ u64 packet_sequence{};
+
+ std::chrono::time_point<std::chrono::steady_clock> last_update;
+ };
+
+ struct ClientConnection {
+ ClientConnection();
+ ~ClientConnection();
+ Common::UUID uuid{"7F000001"};
+ std::string host{"127.0.0.1"};
+ u16 port{26760};
+ s8 active{-1};
+ std::unique_ptr<Socket> socket;
+ std::thread thread;
+ };
+
+ // For shutting down, clear all data, join all threads, release usb
+ void Reset();
+
+ // Translates configuration to client number
+ std::size_t GetClientNumber(std::string_view host, u16 port) const;
+
+ void OnVersion(Response::Version);
+ void OnPortInfo(Response::PortInfo);
+ void OnPadData(Response::PadData, std::size_t client);
+ void StartCommunication(std::size_t client, const std::string& host, u16 port);
+ const PadIdentifier GetPadIdentifier(std::size_t pad_index) const;
+ const Common::UUID GetHostUUID(const std::string host) const;
+
+ Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
+
+ // Allocate clients for 8 udp servers
+ static constexpr std::size_t MAX_UDP_CLIENTS = 8;
+ static constexpr std::size_t PADS_PER_CLIENT = 4;
+ std::array<PadData, MAX_UDP_CLIENTS * PADS_PER_CLIENT> pads{};
+ std::array<ClientConnection, MAX_UDP_CLIENTS> clients{};
+};
+
+/// An async job allowing configuration of the touchpad calibration.
+class CalibrationConfigurationJob {
+public:
+ enum class Status {
+ Initialized,
+ Ready,
+ Stage1Completed,
+ Completed,
+ };
+ /**
+ * Constructs and starts the job with the specified parameter.
+ *
+ * @param status_callback Callback for job status updates
+ * @param data_callback Called when calibration data is ready
+ */
+ explicit CalibrationConfigurationJob(const std::string& host, u16 port,
+ std::function<void(Status)> status_callback,
+ std::function<void(u16, u16, u16, u16)> data_callback);
+ ~CalibrationConfigurationJob();
+ void Stop();
+
+private:
+ Common::Event complete_event;
+};
+
+void TestCommunication(const std::string& host, u16 port,
+ const std::function<void()>& success_callback,
+ const std::function<void()>& failure_callback);
+
+} // namespace InputCommon::CemuhookUDP