From 05e28a53e80d257e60ebcedb033feba22d2a3f60 Mon Sep 17 00:00:00 2001 From: ameerj <52414509+ameerj@users.noreply.github.com> Date: Mon, 8 Mar 2021 19:03:50 -0500 Subject: [PATCH] input_common: Add support for GameCube Adapter This is a port of the initial GameCube adapter input support i added into yuzu emulator. It requires the same setup as when it was first introduced in yuzu, requiring the Zadig driver be installed for the adapter to allow it to interface with libusb. --- CMakeLists.txt | 11 + .../configuration/configure_input.cpp | 36 +- src/input_common/CMakeLists.txt | 5 + src/input_common/gcadapter/gc_adapter.cpp | 329 ++++++++++++++++++ src/input_common/gcadapter/gc_adapter.h | 132 +++++++ src/input_common/gcadapter/gc_poller.cpp | 272 +++++++++++++++ src/input_common/gcadapter/gc_poller.h | 67 ++++ src/input_common/main.cpp | 24 ++ 8 files changed, 875 insertions(+), 1 deletion(-) create mode 100644 src/input_common/gcadapter/gc_adapter.cpp create mode 100644 src/input_common/gcadapter/gc_adapter.h create mode 100644 src/input_common/gcadapter/gc_poller.cpp create mode 100644 src/input_common/gcadapter/gc_poller.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fbbb0497..af8caa7fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,6 +193,17 @@ if (ENABLE_QT) endif() endif() +# Ensure libusb is properly configured (based on dolphin libusb include) +if(NOT APPLE) + include(FindPkgConfig) + find_package(LibUSB) +endif() +if (NOT LIBUSB_FOUND) + add_subdirectory(externals/libusb) + set(LIBUSB_INCLUDE_DIR "") + set(LIBUSB_LIBRARIES usb) +endif() + if (ENABLE_FFMPEG) if (CITRA_USE_BUNDLED_FFMPEG) if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1930) AND ARCHITECTURE_x86_64) diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp index 64fb7c840..6eb960c26 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/citra_qt/configuration/configure_input.cpp @@ -85,6 +85,20 @@ static QString ButtonToText(const Common::ParamPackage& param) { return {}; } + if (param.Get("engine", "") == "gcpad") { + if (param.Has("axis")) { + const QString axis_str = QString::fromStdString(param.Get("axis", "")); + const QString direction_str = QString::fromStdString(param.Get("direction", "")); + + return QObject::tr("GC Axis %1%2").arg(axis_str, direction_str); + } + if (param.Has("button")) { + const QString button_str = QString::number(int(std::log2(param.Get("button", 0)))); + return QObject::tr("GC Button %1").arg(button_str); + } + return GetKeyName(param.Get("code", 0)); + } + return QObject::tr("[unknown]"); } @@ -117,6 +131,25 @@ static QString AnalogToText(const Common::ParamPackage& param, const std::string return {}; } + if (param.Get("engine", "") == "gcpad") { + if (dir == "modifier") { + return QObject::tr("[unused]"); + } + + if (dir == "left" || dir == "right") { + const QString axis_x_str = QString::fromStdString(param.Get("axis_x", "")); + + return QObject::tr("GC Axis %1").arg(axis_x_str); + } + + if (dir == "up" || dir == "down") { + const QString axis_y_str = QString::fromStdString(param.Get("axis_y", "")); + + return QObject::tr("GC Axis %1").arg(axis_y_str); + } + + return {}; + } return QObject::tr("[unknown]"); } @@ -418,7 +451,8 @@ void ConfigureInput::UpdateButtonLabels() { analog_map_deadzone_and_modifier_slider_label[analog_id]; if (param.Has("engine")) { - if (param.Get("engine", "") == "sdl") { + const auto engine{param.Get("engine", "")}; + if (engine == "sdl" || engine == "gcpad") { if (!param.Has("deadzone")) { param.Set("deadzone", 0.1f); } diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 0a7ad1d2f..9c4a7517f 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -1,6 +1,10 @@ add_library(input_common STATIC analog_from_button.cpp analog_from_button.h + gcadapter/gc_adapter.cpp + gcadapter/gc_adapter.h + gcadapter/gc_poller.cpp + gcadapter/gc_poller.h keyboard.cpp keyboard.h main.cpp @@ -30,3 +34,4 @@ endif() create_target_directory_groups(input_common) target_link_libraries(input_common PUBLIC core PRIVATE common ${Boost_LIBRARIES}) +target_link_libraries(input_common PUBLIC ${LIBUSB_LIBRARIES}) diff --git a/src/input_common/gcadapter/gc_adapter.cpp b/src/input_common/gcadapter/gc_adapter.cpp new file mode 100644 index 000000000..74759ea7d --- /dev/null +++ b/src/input_common/gcadapter/gc_adapter.cpp @@ -0,0 +1,329 @@ +// Copyright 2014 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/logging/log.h" +#include "input_common/gcadapter/gc_adapter.h" + +namespace GCAdapter { + +/// Used to loop through and assign button in poller +constexpr std::array PadButtonArray{ + PadButton::PAD_BUTTON_LEFT, PadButton::PAD_BUTTON_RIGHT, PadButton::PAD_BUTTON_DOWN, + PadButton::PAD_BUTTON_UP, PadButton::PAD_TRIGGER_Z, PadButton::PAD_TRIGGER_R, + PadButton::PAD_TRIGGER_L, PadButton::PAD_BUTTON_A, PadButton::PAD_BUTTON_B, + PadButton::PAD_BUTTON_X, PadButton::PAD_BUTTON_Y, PadButton::PAD_BUTTON_START, +}; + +Adapter::Adapter() { + if (usb_adapter_handle != nullptr) { + return; + } + LOG_INFO(Input, "GC Adapter Initialization started"); + + const int init_res = libusb_init(&libusb_ctx); + if (init_res == LIBUSB_SUCCESS) { + Setup(); + } else { + LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res); + } +} + +GCPadStatus Adapter::GetPadStatus(std::size_t port, const std::array& adapter_payload) { + GCPadStatus pad = {}; + const std::size_t offset = 1 + (9 * port); + + adapter_controllers_status[port] = static_cast(adapter_payload[offset] >> 4); + + static constexpr std::array b1_buttons{ + PadButton::PAD_BUTTON_A, PadButton::PAD_BUTTON_B, PadButton::PAD_BUTTON_X, + PadButton::PAD_BUTTON_Y, PadButton::PAD_BUTTON_LEFT, PadButton::PAD_BUTTON_RIGHT, + PadButton::PAD_BUTTON_DOWN, PadButton::PAD_BUTTON_UP, + }; + + static constexpr std::array b2_buttons{ + PadButton::PAD_BUTTON_START, + PadButton::PAD_TRIGGER_Z, + PadButton::PAD_TRIGGER_R, + PadButton::PAD_TRIGGER_L, + }; + + static constexpr std::array axes{ + PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX, + PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight, + }; + + if (adapter_controllers_status[port] == ControllerTypes::None && !get_origin[port]) { + // Controller may have been disconnected, recalibrate if reconnected. + get_origin[port] = true; + } + + if (adapter_controllers_status[port] != ControllerTypes::None) { + const u8 b1 = adapter_payload[offset + 1]; + const u8 b2 = adapter_payload[offset + 2]; + + for (std::size_t i = 0; i < b1_buttons.size(); ++i) { + if ((b1 & (1U << i)) != 0) { + pad.button |= static_cast(b1_buttons[i]); + } + } + + for (std::size_t j = 0; j < b2_buttons.size(); ++j) { + if ((b2 & (1U << j)) != 0) { + pad.button |= static_cast(b2_buttons[j]); + } + } + for (PadAxes axis : axes) { + const std::size_t index = static_cast(axis); + pad.axis_values[index] = adapter_payload[offset + 3 + index]; + } + + if (get_origin[port]) { + origin_status[port].axis_values = pad.axis_values; + get_origin[port] = false; + } + } + return pad; +} + +void Adapter::PadToState(const GCPadStatus& pad, GCState& state) { + for (const auto& button : PadButtonArray) { + const u16 button_value = static_cast(button); + state.buttons.insert_or_assign(button_value, pad.button & button_value); + } + + for (size_t i = 0; i < pad.axis_values.size(); ++i) { + state.axes.insert_or_assign(static_cast(i), pad.axis_values[i]); + } +} + +void Adapter::Read() { + LOG_DEBUG(Input, "GC Adapter Read() thread started"); + + int payload_size; + std::array adapter_payload; + std::array pads; + + while (adapter_thread_running) { + libusb_interrupt_transfer(usb_adapter_handle, input_endpoint, adapter_payload.data(), + sizeof(adapter_payload), &payload_size, 16); + + if (payload_size != sizeof(adapter_payload) || adapter_payload[0] != LIBUSB_DT_HID) { + LOG_ERROR(Input, + "Error reading payload (size: {}, type: {:02x}) Is the adapter connected?", + payload_size, adapter_payload[0]); + adapter_thread_running = false; // error reading from adapter, stop reading. + break; + } + for (std::size_t port = 0; port < pads.size(); ++port) { + pads[port] = GetPadStatus(port, adapter_payload); + if (DeviceConnected(port) && configuring) { + if (pads[port].button != 0) { + pad_queue[port].Push(pads[port]); + } + + // Accounting for a threshold here to ensure an intentional press + for (size_t i = 0; i < pads[port].axis_values.size(); ++i) { + const u8 value = pads[port].axis_values[i]; + const u8 origin = origin_status[port].axis_values[i]; + + if (value > origin + pads[port].THRESHOLD || + value < origin - pads[port].THRESHOLD) { + pads[port].axis = static_cast(i); + pads[port].axis_value = pads[port].axis_values[i]; + pad_queue[port].Push(pads[port]); + } + } + } + PadToState(pads[port], state[port]); + } + std::this_thread::yield(); + } +} + +void Adapter::Setup() { + // Initialize all controllers as unplugged + adapter_controllers_status.fill(ControllerTypes::None); + // Initialize all ports to store axis origin values + get_origin.fill(true); + + // pointer to list of connected usb devices + libusb_device** devices{}; + + // populate the list of devices, get the count + const ssize_t device_count = libusb_get_device_list(libusb_ctx, &devices); + if (device_count < 0) { + LOG_ERROR(Input, "libusb_get_device_list failed with error: {}", device_count); + return; + } + + if (devices != nullptr) { + for (std::size_t index = 0; index < static_cast(device_count); ++index) { + if (CheckDeviceAccess(devices[index])) { + // GC Adapter found and accessible, registering it + GetGCEndpoint(devices[index]); + break; + } + } + libusb_free_device_list(devices, 1); + } +} + +bool Adapter::CheckDeviceAccess(libusb_device* device) { + libusb_device_descriptor desc; + const int get_descriptor_error = libusb_get_device_descriptor(device, &desc); + if (get_descriptor_error) { + // could not acquire the descriptor, no point in trying to use it. + LOG_ERROR(Input, "libusb_get_device_descriptor failed with error: {}", + get_descriptor_error); + return false; + } + + if (desc.idVendor != 0x057e || desc.idProduct != 0x0337) { + // This isn't the device we are looking for. + return false; + } + const int open_error = libusb_open(device, &usb_adapter_handle); + + if (open_error == LIBUSB_ERROR_ACCESS) { + LOG_ERROR(Input, "Yuzu can not gain access to this device: ID {:04X}:{:04X}.", + desc.idVendor, desc.idProduct); + return false; + } + if (open_error) { + LOG_ERROR(Input, "libusb_open failed to open device with error = {}", open_error); + return false; + } + + int kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle, 0); + if (kernel_driver_error == 1) { + kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle, 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) { + libusb_close(usb_adapter_handle); + usb_adapter_handle = nullptr; + return false; + } + + const int interface_claim_error = libusb_claim_interface(usb_adapter_handle, 0); + if (interface_claim_error) { + LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error); + libusb_close(usb_adapter_handle); + usb_adapter_handle = nullptr; + return false; + } + + return true; +} + +void Adapter::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; + } + + 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) { + 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, output_endpoint, &clear_payload, + sizeof(clear_payload), nullptr, 16); + + adapter_thread_running = true; + adapter_input_thread = std::thread(&Adapter::Read, this); +} + +Adapter::~Adapter() { + Reset(); +} + +void Adapter::Reset() { + if (adapter_thread_running) { + adapter_thread_running = false; + } + if (adapter_input_thread.joinable()) { + adapter_input_thread.join(); + } + + adapter_controllers_status.fill(ControllerTypes::None); + get_origin.fill(true); + + if (usb_adapter_handle) { + libusb_release_interface(usb_adapter_handle, 1); + libusb_close(usb_adapter_handle); + usb_adapter_handle = nullptr; + } + + if (libusb_ctx) { + libusb_exit(libusb_ctx); + } +} + +bool Adapter::DeviceConnected(std::size_t port) { + return adapter_controllers_status[port] != ControllerTypes::None; +} + +void Adapter::ResetDeviceType(std::size_t port) { + adapter_controllers_status[port] = ControllerTypes::None; +} + +void Adapter::BeginConfiguration() { + get_origin.fill(true); + for (auto& pq : pad_queue) { + pq.Clear(); + } + configuring = true; +} + +void Adapter::EndConfiguration() { + for (auto& pq : pad_queue) { + pq.Clear(); + } + configuring = false; +} + +std::array, 4>& Adapter::GetPadQueue() { + return pad_queue; +} + +const std::array, 4>& Adapter::GetPadQueue() const { + return pad_queue; +} + +std::array& Adapter::GetPadState() { + return state; +} + +const std::array& Adapter::GetPadState() const { + return state; +} + +int Adapter::GetOriginValue(int port, int axis) const { + return origin_status[port].axis_values[axis]; +} + +} // namespace GCAdapter diff --git a/src/input_common/gcadapter/gc_adapter.h b/src/input_common/gcadapter/gc_adapter.h new file mode 100644 index 000000000..dc1e3e398 --- /dev/null +++ b/src/input_common/gcadapter/gc_adapter.h @@ -0,0 +1,132 @@ +// Copyright 2014 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/common_types.h" +#include "common/threadsafe_queue.h" + +struct libusb_context; +struct libusb_device; +struct libusb_device_handle; + +namespace GCAdapter { + +enum class PadButton { + PAD_BUTTON_LEFT = 0x0001, + PAD_BUTTON_RIGHT = 0x0002, + PAD_BUTTON_DOWN = 0x0004, + PAD_BUTTON_UP = 0x0008, + PAD_TRIGGER_Z = 0x0010, + PAD_TRIGGER_R = 0x0020, + PAD_TRIGGER_L = 0x0040, + PAD_BUTTON_A = 0x0100, + PAD_BUTTON_B = 0x0200, + PAD_BUTTON_X = 0x0400, + PAD_BUTTON_Y = 0x0800, + PAD_BUTTON_START = 0x1000, + // Below is for compatibility with "AxisButton" type + PAD_STICK = 0x2000, +}; + +extern const std::array PadButtonArray; + +enum class PadAxes : u8 { + StickX, + StickY, + SubstickX, + SubstickY, + TriggerLeft, + TriggerRight, + Undefined, +}; + +struct GCPadStatus { + u16 button{}; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits + + std::array axis_values{}; // Triggers and sticks, following indices defined in PadAxes + static constexpr u8 THRESHOLD = 50; // Threshold for axis press for polling + + u8 port{}; + PadAxes axis{PadAxes::Undefined}; + u8 axis_value{255}; +}; + +struct GCState { + std::unordered_map buttons; + std::unordered_map axes; +}; + +enum class ControllerTypes { None, Wired, Wireless }; + +class Adapter { +public: + /// Initialize the GC Adapter capture and read sequence + Adapter(); + + /// Close the adapter read thread and release the adapter + ~Adapter(); + /// Used for polling + void BeginConfiguration(); + void EndConfiguration(); + + /// Returns true if there is a device connected to port + bool DeviceConnected(std::size_t port); + + std::array, 4>& GetPadQueue(); + const std::array, 4>& GetPadQueue() const; + + std::array& GetPadState(); + const std::array& GetPadState() const; + + int GetOriginValue(int port, int axis) const; + +private: + GCPadStatus GetPadStatus(std::size_t port, const std::array& adapter_payload); + + void PadToState(const GCPadStatus& pad, GCState& state); + + void Read(); + + /// Resets status of device connected to port + void ResetDeviceType(std::size_t port); + + /// Returns true if we successfully gain access to GC Adapter + bool CheckDeviceAccess(libusb_device* device); + + /// Captures GC Adapter endpoint address, + void GetGCEndpoint(libusb_device* device); + + /// For shutting down, clear all data, join all threads, release usb + void Reset(); + + /// For use in initialization, querying devices to find the adapter + void Setup(); + + libusb_device_handle* usb_adapter_handle = nullptr; + + std::thread adapter_input_thread; + bool adapter_thread_running; + + libusb_context* libusb_ctx; + + u8 input_endpoint = 0; + u8 output_endpoint = 0; + + bool configuring = false; + + std::array state; + std::array get_origin; + std::array origin_status; + std::array, 4> pad_queue; + std::array adapter_controllers_status{}; +}; + +} // namespace GCAdapter diff --git a/src/input_common/gcadapter/gc_poller.cpp b/src/input_common/gcadapter/gc_poller.cpp new file mode 100644 index 000000000..6fc141eda --- /dev/null +++ b/src/input_common/gcadapter/gc_poller.cpp @@ -0,0 +1,272 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "common/assert.h" +#include "common/threadsafe_queue.h" +#include "input_common/gcadapter/gc_adapter.h" +#include "input_common/gcadapter/gc_poller.h" + +namespace InputCommon { + +class GCButton final : public Input::ButtonDevice { +public: + explicit GCButton(int port_, int button_, GCAdapter::Adapter* adapter) + : port(port_), button(button_), gcadapter(adapter) {} + + ~GCButton() override; + + bool GetStatus() const override { + if (gcadapter->DeviceConnected(port)) { + return gcadapter->GetPadState()[port].buttons.at(button); + } + return false; + } + +private: + const int port; + const int button; + GCAdapter::Adapter* gcadapter; +}; + +class GCAxisButton final : public Input::ButtonDevice { +public: + explicit GCAxisButton(int port_, int axis_, float threshold_, bool trigger_if_greater_, + GCAdapter::Adapter* adapter) + : port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_), + gcadapter(adapter), + origin_value(static_cast(adapter->GetOriginValue(port_, axis_))) {} + + bool GetStatus() const override { + if (gcadapter->DeviceConnected(port)) { + const float current_axis_value = gcadapter->GetPadState()[port].axes.at(axis); + const float axis_value = (current_axis_value - origin_value) / 128.0f; + if (trigger_if_greater) { + // TODO: Might be worthwile to set a slider for the trigger threshold. It is + // currently always set to 0.5 in configure_input_player.cpp ZL/ZR HandleClick + return axis_value > threshold; + } + return axis_value < -threshold; + } + return false; + } + +private: + const int port; + const int axis; + float threshold; + bool trigger_if_greater; + GCAdapter::Adapter* gcadapter; + const float origin_value; +}; + +GCButtonFactory::GCButtonFactory(std::shared_ptr adapter_) + : adapter(std::move(adapter_)) {} + +GCButton::~GCButton() = default; + +std::unique_ptr GCButtonFactory::Create(const Common::ParamPackage& params) { + const int button_id = params.Get("button", 0); + const int port = params.Get("port", 0); + + constexpr int PAD_STICK_ID = static_cast(GCAdapter::PadButton::PAD_STICK); + + // button is not an axis/stick button + if (button_id != PAD_STICK_ID) { + return std::make_unique(port, button_id, adapter.get()); + } + + // For Axis buttons, used by the binary sticks. + if (button_id == PAD_STICK_ID) { + const int axis = params.Get("axis", 0); + const float threshold = params.Get("threshold", 0.25f); + const std::string direction_name = params.Get("direction", ""); + bool trigger_if_greater; + if (direction_name == "+") { + trigger_if_greater = true; + } else if (direction_name == "-") { + trigger_if_greater = false; + } else { + trigger_if_greater = true; + LOG_ERROR(Input, "Unknown direction {}", direction_name); + } + return std::make_unique(port, axis, threshold, trigger_if_greater, + adapter.get()); + } + + UNREACHABLE(); + return nullptr; +} + +Common::ParamPackage GCButtonFactory::GetNextInput() { + Common::ParamPackage params; + GCAdapter::GCPadStatus pad; + auto& queue = adapter->GetPadQueue(); + for (std::size_t port = 0; port < queue.size(); ++port) { + while (queue[port].Pop(pad)) { + // This while loop will break on the earliest detected button + params.Set("engine", "gcpad"); + params.Set("port", static_cast(port)); + for (const auto& button : GCAdapter::PadButtonArray) { + const u16 button_value = static_cast(button); + if (pad.button & button_value) { + params.Set("button", button_value); + break; + } + } + + // For Axis button implementation + if (pad.axis != GCAdapter::PadAxes::Undefined) { + params.Set("axis", static_cast(pad.axis)); + params.Set("button", static_cast(GCAdapter::PadButton::PAD_STICK)); + if (pad.axis_value > 128) { + params.Set("direction", "+"); + params.Set("threshold", "0.25"); + } else { + params.Set("direction", "-"); + params.Set("threshold", "-0.25"); + } + break; + } + } + } + return params; +} + +void GCButtonFactory::Start() { + polling = true; + adapter->BeginConfiguration(); +} + +void GCButtonFactory::Stop() { + polling = false; + adapter->EndConfiguration(); +} + +class GCAnalog final : public Input::AnalogDevice { +public: + GCAnalog(int port_, int axis_x_, int axis_y_, float deadzone_, GCAdapter::Adapter* adapter, + float range_) + : port(port_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), gcadapter(adapter), + origin_value_x(static_cast(adapter->GetOriginValue(port_, axis_x_))), + origin_value_y(static_cast(adapter->GetOriginValue(port_, axis_y_))), + range(range_) {} + + float GetAxis(int axis) const { + if (gcadapter->DeviceConnected(port)) { + std::lock_guard lock{mutex}; + const auto origin_value = axis % 2 == 0 ? origin_value_x : origin_value_y; + return (gcadapter->GetPadState()[port].axes.at(axis) - origin_value) / (100.0f * range); + } + return 0.0f; + } + + std::pair GetAnalog(int axis_x, int axis_y) const { + float x = GetAxis(axis_x); + float y = GetAxis(axis_y); + + // Make sure the coordinates are in the unit circle, + // otherwise normalize it. + float r = x * x + y * y; + if (r > 1.0f) { + r = std::sqrt(r); + x /= r; + y /= r; + } + + return {x, y}; + } + + std::tuple GetStatus() const override { + const auto [x, y] = GetAnalog(axis_x, axis_y); + const float r = std::sqrt((x * x) + (y * y)); + if (r > deadzone) { + return {x / r * (r - deadzone) / (1 - deadzone), + y / r * (r - deadzone) / (1 - deadzone)}; + } + return {0.0f, 0.0f}; + } + +private: + const int port; + const int axis_x; + const int axis_y; + const float deadzone; + GCAdapter::Adapter* gcadapter; + const float origin_value_x; + const float origin_value_y; + const float range; + mutable std::mutex mutex; +}; + +/// An analog device factory that creates analog devices from GC Adapter +GCAnalogFactory::GCAnalogFactory(std::shared_ptr adapter_) + : adapter(std::move(adapter_)) {} + +/** + * Creates analog device from joystick axes + * @param params contains parameters for creating the device: + * - "port": the nth gcpad on the adapter + * - "axis_x": the index of the axis to be bind as x-axis + * - "axis_y": the index of the axis to be bind as y-axis + */ +std::unique_ptr GCAnalogFactory::Create(const Common::ParamPackage& params) { + const int port = params.Get("port", 0); + const int axis_x = params.Get("axis_x", 0); + const int axis_y = params.Get("axis_y", 1); + const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); + const float range = std::clamp(params.Get("range", 1.0f), 0.50f, 1.50f); + + return std::make_unique(port, axis_x, axis_y, deadzone, adapter.get(), range); +} + +void GCAnalogFactory::Start() { + polling = true; + adapter->BeginConfiguration(); +} + +void GCAnalogFactory::Stop() { + polling = false; + adapter->EndConfiguration(); +} + +Common::ParamPackage GCAnalogFactory::GetNextInput() { + GCAdapter::GCPadStatus pad; + auto& queue = adapter->GetPadQueue(); + for (std::size_t port = 0; port < queue.size(); ++port) { + while (queue[port].Pop(pad)) { + if (pad.axis == GCAdapter::PadAxes::Undefined || + std::abs((pad.axis_value - 128.0f) / 128.0f) < 0.05) { + continue; + } + // An analog device needs two axes, so we need to store the axis for later and wait for + // a second input event. The axes also must be from the same joystick. + const u8 axis = static_cast(pad.axis); + if (analog_x_axis == -1) { + analog_x_axis = axis; + controller_number = static_cast(port); + } else if (analog_y_axis == -1 && analog_x_axis != axis && + controller_number == static_cast(port)) { + analog_y_axis = axis; + } + } + } + Common::ParamPackage params; + if (analog_x_axis != -1 && analog_y_axis != -1) { + params.Set("engine", "gcpad"); + params.Set("port", controller_number); + params.Set("axis_x", analog_x_axis); + params.Set("axis_y", analog_y_axis); + analog_x_axis = -1; + analog_y_axis = -1; + controller_number = -1; + return params; + } + return params; +} + +} // namespace InputCommon diff --git a/src/input_common/gcadapter/gc_poller.h b/src/input_common/gcadapter/gc_poller.h new file mode 100644 index 000000000..3e0382d30 --- /dev/null +++ b/src/input_common/gcadapter/gc_poller.h @@ -0,0 +1,67 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/frontend/input.h" +#include "input_common/gcadapter/gc_adapter.h" +#include "input_common/main.h" + +namespace InputCommon { + +/** + * A button device factory representing a gcpad. It receives gcpad events and forward them + * to all button devices it created. + */ +class GCButtonFactory final : public Input::Factory, + public Polling::DevicePoller { +public: +public: + explicit GCButtonFactory(std::shared_ptr adapter_); + + std::unique_ptr Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput() override; + + /// For device input configuration/polling + void Start() override; + void Stop() override; + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr adapter; + bool polling{false}; +}; + +/// An analog device factory that creates analog devices from GC Adapter +class GCAnalogFactory final : public Input::Factory, + public Polling::DevicePoller { +public: + explicit GCAnalogFactory(std::shared_ptr adapter_); + + std::unique_ptr Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput() override; + + /// For device input configuration/polling + void Start() override; + void Stop() override; + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr adapter; + int analog_x_axis{-1}; + int analog_y_axis{-1}; + int controller_number{-1}; + bool polling{false}; +}; + +} // namespace InputCommon diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index 89e441ddb..4d0671fb6 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -6,6 +6,8 @@ #include #include "common/param_package.h" #include "input_common/analog_from_button.h" +#include "input_common/gcadapter/gc_adapter.h" +#include "input_common/gcadapter/gc_poller.h" #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/motion_emu.h" @@ -16,12 +18,20 @@ namespace InputCommon { +std::shared_ptr gcbuttons; +std::shared_ptr gcanalog; +std::shared_ptr gcadapter; static std::shared_ptr keyboard; static std::shared_ptr motion_emu; static std::unique_ptr udp; static std::unique_ptr sdl; void Init() { + gcadapter = std::make_shared(); + gcbuttons = std::make_shared(gcadapter); + Input::RegisterFactory("gcpad", gcbuttons); + gcanalog = std::make_shared(gcadapter); + Input::RegisterFactory("gcpad", gcanalog); keyboard = std::make_shared(); Input::RegisterFactory("keyboard", keyboard); Input::RegisterFactory("analog_from_button", @@ -37,6 +47,10 @@ void Init() { } void Shutdown() { + Input::UnregisterFactory("gcpad"); + Input::UnregisterFactory("gcpad"); + gcbuttons.reset(); + gcanalog.reset(); Input::UnregisterFactory("keyboard"); keyboard.reset(); Input::UnregisterFactory("analog_from_button"); @@ -102,6 +116,16 @@ std::vector> GetPollers(DeviceType type) { #ifdef HAVE_SDL2 pollers = sdl->GetPollers(type); #endif + switch (type) { + case DeviceType::Analog: + pollers.push_back(std::make_unique(*gcanalog)); + break; + case DeviceType::Button: + pollers.push_back(std::make_unique(*gcbuttons)); + break; + default: + break; + } return pollers; }