diff --git a/.gitmodules b/.gitmodules index 6fa823c1ce..79028bbb5e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,6 +34,9 @@ [submodule "xbyak"] path = externals/xbyak url = https://github.com/herumi/xbyak.git +[submodule "externals/libusb"] + path = externals/libusb + url = https://github.com/ameerj/libusb [submodule "opus"] path = externals/opus/opus url = https://github.com/xiph/opus.git diff --git a/CMakeLists.txt b/CMakeLists.txt index d0af994da2..27383bce83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -329,6 +329,12 @@ elseif(SDL2_FOUND) target_link_libraries(SDL2 INTERFACE "${SDL2_LIBRARIES}") endif() +# Ensure libusb is properly configured (based on dolphin libusb include) +find_package(LibUSB) +add_subdirectory(externals/libusb) +set(LIBUSB_LIBRARIES usb) + + # Prefer the -pthread flag on Linux. set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) diff --git a/externals/libusb b/externals/libusb new file mode 160000 index 0000000000..3406d72cda --- /dev/null +++ b/externals/libusb @@ -0,0 +1 @@ +Subproject commit 3406d72cda879f8792a88bf5f6bd0b7a65636f72 diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index a9c2392b15..3bd76dd238 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -7,6 +7,10 @@ add_library(input_common STATIC main.h motion_emu.cpp motion_emu.h + gcadapter/gc_adapter.cpp + gcadapter/gc_adapter.h + gcadapter/gc_poller.cpp + gcadapter/gc_poller.h sdl/sdl.cpp sdl/sdl.h udp/client.cpp @@ -26,5 +30,7 @@ if(SDL2_FOUND) target_compile_definitions(input_common PRIVATE HAVE_SDL2) endif() +target_link_libraries(input_common PUBLIC ${LIBUSB_LIBRARIES}) + create_target_directory_groups(input_common) target_link_libraries(input_common PUBLIC core PRIVATE common Boost::boost) diff --git a/src/input_common/gcadapter/gc_adapter.cpp b/src/input_common/gcadapter/gc_adapter.cpp new file mode 100644 index 0000000000..b39d2a3fb4 --- /dev/null +++ b/src/input_common/gcadapter/gc_adapter.cpp @@ -0,0 +1,379 @@ +// Copyright 2014 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#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"); + + current_status = NO_ADAPTER_DETECTED; + libusb_init(&libusb_ctx); + + StartScanThread(); +} + +GCPadStatus Adapter::GetPadStatus(int port, const std::array& adapter_payload) { + GCPadStatus pad = {}; + bool get_origin = false; + + ControllerTypes type = ControllerTypes(adapter_payload[1 + (9 * port)] >> 4); + if (type != ControllerTypes::None) { + get_origin = true; + } + + adapter_controllers_status[port] = type; + + 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, + }; + + if (adapter_controllers_status[port] != ControllerTypes::None) { + const u8 b1 = adapter_payload[1 + (9 * port) + 1]; + const u8 b2 = adapter_payload[1 + (9 * port) + 2]; + + 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]); + } + } + + if (get_origin) { + pad.button |= PAD_GET_ORIGIN; + } + + pad.stick_x = adapter_payload[1 + (9 * port) + 3]; + pad.stick_y = adapter_payload[1 + (9 * port) + 4]; + pad.substick_x = adapter_payload[1 + (9 * port) + 5]; + pad.substick_y = adapter_payload[1 + (9 * port) + 6]; + pad.trigger_left = adapter_payload[1 + (9 * port) + 7]; + pad.trigger_right = adapter_payload[1 + (9 * port) + 8]; + } + 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); + } + + state.axes.insert_or_assign(static_cast(PadAxes::StickX), pad.stick_x); + state.axes.insert_or_assign(static_cast(PadAxes::StickY), pad.stick_y); + state.axes.insert_or_assign(static_cast(PadAxes::SubstickX), pad.substick_x); + state.axes.insert_or_assign(static_cast(PadAxes::SubstickY), pad.substick_y); + state.axes.insert_or_assign(static_cast(PadAxes::TriggerLeft), pad.trigger_left); + state.axes.insert_or_assign(static_cast(PadAxes::TriggerRight), pad.trigger_right); +} + +void Adapter::Read() { + LOG_DEBUG(Input, "GC Adapter Read() thread started"); + + int payload_size_in, payload_size_copy; + std::array adapter_payload; + std::array adapter_payload_copy; + std::array pads; + + while (adapter_thread_running) { + libusb_interrupt_transfer(usb_adapter_handle, input_endpoint, adapter_payload.data(), + sizeof(adapter_payload), &payload_size_in, 16); + payload_size_copy = 0; + // this mutex might be redundant? + { + std::lock_guard lk(s_mutex); + std::copy(std::begin(adapter_payload), std::end(adapter_payload), + std::begin(adapter_payload_copy)); + payload_size_copy = payload_size_in; + } + + if (payload_size_copy != sizeof(adapter_payload_copy) || + adapter_payload_copy[0] != LIBUSB_DT_HID) { + LOG_ERROR(Input, "error reading payload (size: {}, type: {:02x})", payload_size_copy, + adapter_payload_copy[0]); + adapter_thread_running = false; // error reading from adapter, stop reading. + break; + } + for (std::size_t port = 0; port < pads.size(); ++port) { + pads[port] = GetPadStatus(port, adapter_payload_copy); + if (DeviceConnected(port) && configuring) { + if (pads[port].button != PAD_GET_ORIGIN) { + pad_queue[port].Push(pads[port]); + } + + // Accounting for a threshold here because of some controller variance + if (pads[port].stick_x > pads[port].MAIN_STICK_CENTER_X + pads[port].THRESHOLD || + pads[port].stick_x < pads[port].MAIN_STICK_CENTER_X - pads[port].THRESHOLD) { + pads[port].axis = GCAdapter::PadAxes::StickX; + pads[port].axis_value = pads[port].stick_x; + pad_queue[port].Push(pads[port]); + } + if (pads[port].stick_y > pads[port].MAIN_STICK_CENTER_Y + pads[port].THRESHOLD || + pads[port].stick_y < pads[port].MAIN_STICK_CENTER_Y - pads[port].THRESHOLD) { + pads[port].axis = GCAdapter::PadAxes::StickY; + pads[port].axis_value = pads[port].stick_y; + pad_queue[port].Push(pads[port]); + } + if (pads[port].substick_x > pads[port].C_STICK_CENTER_X + pads[port].THRESHOLD || + pads[port].substick_x < pads[port].C_STICK_CENTER_X - pads[port].THRESHOLD) { + pads[port].axis = GCAdapter::PadAxes::SubstickX; + pads[port].axis_value = pads[port].substick_x; + pad_queue[port].Push(pads[port]); + } + if (pads[port].substick_y > pads[port].C_STICK_CENTER_Y + pads[port].THRESHOLD || + pads[port].substick_y < pads[port].C_STICK_CENTER_Y - pads[port].THRESHOLD) { + pads[port].axis = GCAdapter::PadAxes::SubstickY; + pads[port].axis_value = pads[port].substick_y; + pad_queue[port].Push(pads[port]); + } + if (pads[port].trigger_left > pads[port].TRIGGER_THRESHOLD) { + pads[port].axis = GCAdapter::PadAxes::TriggerLeft; + pads[port].axis_value = pads[port].trigger_left; + pad_queue[port].Push(pads[port]); + } + if (pads[port].trigger_right > pads[port].TRIGGER_THRESHOLD) { + pads[port].axis = GCAdapter::PadAxes::TriggerRight; + pads[port].axis_value = pads[port].trigger_right; + pad_queue[port].Push(pads[port]); + } + } + PadToState(pads[port], state[port]); + } + std::this_thread::yield(); + } +} + +void Adapter::ScanThreadFunc() { + LOG_INFO(Input, "GC Adapter scanning thread started"); + + while (detect_thread_running) { + if (usb_adapter_handle == nullptr) { + std::lock_guard lk(initialization_mutex); + Setup(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } +} + +void Adapter::StartScanThread() { + if (detect_thread_running) { + return; + } + if (!libusb_ctx) { + return; + } + + detect_thread_running = true; + detect_thread = std::thread([=] { ScanThreadFunc(); }); +} + +void Adapter::StopScanThread() { + detect_thread_running = false; + detect_thread.join(); +} + +void Adapter::Setup() { + // Reset the error status in case the adapter gets unplugged + if (current_status < 0) { + current_status = NO_ADAPTER_DETECTED; + } + + adapter_controllers_status.fill(ControllerTypes::None); + + // pointer to list of connected usb devices + libusb_device** devices; + + // populate the list of devices, get the count + const std::size_t device_count = libusb_get_device_list(libusb_ctx, &devices); + + for (std::size_t index = 0; index < device_count; ++index) { + if (CheckDeviceAccess(devices[index])) { + // GC Adapter found and accessible, registering it + GetGCEndpoint(devices[index]); + break; + } + } +} + +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; + libusb_get_config_descriptor(device, 0, &config); + 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; + current_status = ADAPTER_DETECTED; + adapter_input_thread = std::thread([=] { Read(); }); // Read input +} + +Adapter::~Adapter() { + StopScanThread(); + Reset(); +} + +void Adapter::Reset() { + std::unique_lock lock(initialization_mutex, std::defer_lock); + if (!lock.try_lock()) { + return; + } + if (current_status != ADAPTER_DETECTED) { + return; + } + + if (adapter_thread_running) { + adapter_thread_running = false; + } + adapter_input_thread.join(); + + adapter_controllers_status.fill(ControllerTypes::None); + current_status = NO_ADAPTER_DETECTED; + + 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(int port) { + return adapter_controllers_status[port] != ControllerTypes::None; +} + +void Adapter::ResetDeviceType(int port) { + adapter_controllers_status[port] = ControllerTypes::None; +} + +void Adapter::BeginConfiguration() { + 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; +} + +} // 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 0000000000..0ea6263ebd --- /dev/null +++ b/src/input_common/gcadapter/gc_adapter.h @@ -0,0 +1,160 @@ +// Copyright 2014 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once +#include +#include +#include +#include +#include +#include "common/common_types.h" +#include "common/threadsafe_queue.h" + +namespace GCAdapter { + +enum { + PAD_USE_ORIGIN = 0x0080, + PAD_GET_ORIGIN = 0x2000, + PAD_ERR_STATUS = 0x8000, +}; + +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 + u8 stick_x{}; // 0 <= stick_x <= 255 + u8 stick_y{}; // 0 <= stick_y <= 255 + u8 substick_x{}; // 0 <= substick_x <= 255 + u8 substick_y{}; // 0 <= substick_y <= 255 + u8 trigger_left{}; // 0 <= trigger_left <= 255 + u8 trigger_right{}; // 0 <= trigger_right <= 255 + + static constexpr u8 MAIN_STICK_CENTER_X = 0x80; + static constexpr u8 MAIN_STICK_CENTER_Y = 0x80; + static constexpr u8 MAIN_STICK_RADIUS = 0x7f; + static constexpr u8 C_STICK_CENTER_X = 0x80; + static constexpr u8 C_STICK_CENTER_Y = 0x80; + static constexpr u8 C_STICK_RADIUS = 0x7f; + static constexpr u8 THRESHOLD = 10; + + // 256/4, at least a quarter press to count as a press. For polling mostly + static constexpr u8 TRIGGER_THRESHOLD = 64; + + 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 }; + +enum { + NO_ADAPTER_DETECTED = 0, + ADAPTER_DETECTED = 1, +}; + +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(); + + std::array, 4>& GetPadQueue(); + const std::array, 4>& GetPadQueue() const; + + std::array& GetPadState(); + const std::array& GetPadState() const; + +private: + GCPadStatus GetPadStatus(int port, const std::array& adapter_payload); + + void PadToState(const GCPadStatus& pad, GCState& state); + + void Read(); + void ScanThreadFunc(); + /// Begin scanning for the GC Adapter. + void StartScanThread(); + + /// Stop scanning for the adapter + void StopScanThread(); + + /// Returns true if there is a device connected to port + bool DeviceConnected(int port); + + /// Resets status of device connected to port + void ResetDeviceType(int 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(); + + int current_status = NO_ADAPTER_DETECTED; + libusb_device_handle* usb_adapter_handle = nullptr; + std::array adapter_controllers_status{}; + + std::mutex s_mutex; + + std::thread adapter_input_thread; + bool adapter_thread_running; + + std::mutex initialization_mutex; + std::thread detect_thread; + bool detect_thread_running = false; + + libusb_context* libusb_ctx; + + u8 input_endpoint = 0; + u8 output_endpoint = 0; + + bool configuring = false; + + std::array, 4> pad_queue; + std::array state; +}; + +} // 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 0000000000..385ce84301 --- /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/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 { + return gcadapter->GetPadState()[port].buttons.at(button); + } + +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) { + // L/R triggers range is only in positive direction beginning near 0 + // 0.0 threshold equates to near half trigger press, but threshold accounts for variability. + if (axis > 3) { + threshold *= -0.5; + } + } + + bool GetStatus() const override { + const float axis_value = (gcadapter->GetPadState()[port].axes.at(axis) - 128.0f) / 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; + } + +private: + const int port; + const int axis; + float threshold; + bool trigger_if_greater; + GCAdapter::Adapter* gcadapter; +}; + +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) { + auto button = std::make_unique(port, button_id, adapter.get()); + return std::move(button); + } + + // 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()); + } +} + +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::BeginConfiguration() { + polling = true; + adapter->BeginConfiguration(); +} + +void GCButtonFactory::EndConfiguration() { + 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) + : port(port_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), gcadapter(adapter) {} + + float GetAxis(int axis) const { + std::lock_guard lock{mutex}; + // division is not by a perfect 128 to account for some variance in center location + // e.g. my device idled at 131 in X, 120 in Y, and full range of motion was in range + // [20-230] + return (gcadapter->GetPadState()[port].axes.at(axis) - 128.0f) / 95.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}; + } + + bool GetAnalogDirectionStatus(Input::AnalogDirection direction) const override { + const auto [x, y] = GetStatus(); + const float directional_deadzone = 0.4f; + switch (direction) { + case Input::AnalogDirection::RIGHT: + return x > directional_deadzone; + case Input::AnalogDirection::LEFT: + return x < -directional_deadzone; + case Input::AnalogDirection::UP: + return y > directional_deadzone; + case Input::AnalogDirection::DOWN: + return y < -directional_deadzone; + } + return false; + } + +private: + const int port; + const int axis_x; + const int axis_y; + const float deadzone; + mutable std::mutex mutex; + GCAdapter::Adapter* gcadapter; +}; + +/// 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); + + return std::make_unique(port, axis_x, axis_y, deadzone, adapter.get()); +} + +void GCAnalogFactory::BeginConfiguration() { + polling = true; + adapter->BeginConfiguration(); +} + +void GCAnalogFactory::EndConfiguration() { + 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.1) { + 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 = port; + } else if (analog_y_axis == -1 && analog_x_axis != axis && controller_number == 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 0000000000..e96af7d51a --- /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" + +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: + explicit GCButtonFactory(std::shared_ptr adapter_); + + /** + * Creates a button device from a button press + * @param params contains parameters for creating the device: + * - "code": the code of the key to bind with the button + */ + std::unique_ptr Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr adapter; + bool polling = false; +}; + +/// An analog device factory that creates analog devices from GC Adapter +class GCAnalogFactory final : public Input::Factory { +public: + explicit GCAnalogFactory(std::shared_ptr adapter_); + + std::unique_ptr Create(const Common::ParamPackage& params) override; + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr 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 95e351e241..fd0af10193 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -4,8 +4,11 @@ #include #include +#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" @@ -22,8 +25,16 @@ static std::shared_ptr motion_emu; static std::unique_ptr sdl; #endif static std::unique_ptr udp; +static std::shared_ptr gcbuttons; +static std::shared_ptr gcanalog; void Init() { + auto 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", @@ -48,6 +59,11 @@ void Shutdown() { sdl.reset(); #endif udp.reset(); + Input::UnregisterFactory("gcpad"); + Input::UnregisterFactory("gcpad"); + + gcbuttons.reset(); + gcanalog.reset(); } Keyboard* GetKeyboard() { @@ -58,6 +74,14 @@ MotionEmu* GetMotionEmu() { return motion_emu.get(); } +GCButtonFactory* GetGCButtons() { + return gcbuttons.get(); +} + +GCAnalogFactory* GetGCAnalogs() { + return gcanalog.get(); +} + std::string GenerateKeyboardParam(int key_code) { Common::ParamPackage param{ {"engine", "keyboard"}, diff --git a/src/input_common/main.h b/src/input_common/main.h index 77a0ce90b2..0e32856f6d 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -7,6 +7,7 @@ #include #include #include +#include "input_common/gcadapter/gc_poller.h" namespace Common { class ParamPackage; @@ -30,6 +31,10 @@ class MotionEmu; /// Gets the motion emulation factory. MotionEmu* GetMotionEmu(); +GCButtonFactory* GetGCButtons(); + +GCAnalogFactory* GetGCAnalogs(); + /// Generates a serialized param package for creating a keyboard button device std::string GenerateKeyboardParam(int key_code); diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index a05fa64ba3..00433926d3 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -70,6 +70,20 @@ static QString ButtonToText(const Common::ParamPackage& param) { return GetKeyName(param.Get("code", 0)); } + 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)); + } + if (param.Get("engine", "") == "sdl") { if (param.Has("hat")) { const QString hat_str = QString::fromStdString(param.Get("hat", "")); @@ -126,6 +140,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]"); } @@ -332,7 +365,8 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i connect(analog_map_deadzone_and_modifier_slider[analog_id], &QSlider::valueChanged, [=] { const float slider_value = analog_map_deadzone_and_modifier_slider[analog_id]->value(); - if (analogs_param[analog_id].Get("engine", "") == "sdl") { + if (analogs_param[analog_id].Get("engine", "") == "sdl" || + analogs_param[analog_id].Get("engine", "") == "gcpad") { analog_map_deadzone_and_modifier_slider_label[analog_id]->setText( tr("Deadzone: %1%").arg(slider_value)); analogs_param[analog_id].Set("deadzone", slider_value / 100.0f); @@ -352,6 +386,20 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i connect(poll_timer.get(), &QTimer::timeout, [this] { Common::ParamPackage params; + if (InputCommon::GetGCButtons()->IsPolling()) { + params = InputCommon::GetGCButtons()->GetNextInput(); + if (params.Has("engine")) { + SetPollingResult(params, false); + return; + } + } + if (InputCommon::GetGCAnalogs()->IsPolling()) { + params = InputCommon::GetGCAnalogs()->GetNextInput(); + if (params.Has("engine")) { + SetPollingResult(params, false); + return; + } + } for (auto& poller : device_pollers) { params = poller->GetNextInput(); if (params.Has("engine")) { @@ -534,7 +582,7 @@ void ConfigureInputPlayer::UpdateButtonLabels() { analog_map_deadzone_and_modifier_slider_label[analog_id]; if (param.Has("engine")) { - if (param.Get("engine", "") == "sdl") { + if (param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad") { if (!param.Has("deadzone")) { param.Set("deadzone", 0.1f); } @@ -583,6 +631,11 @@ void ConfigureInputPlayer::HandleClick( grabKeyboard(); grabMouse(); + if (type == InputCommon::Polling::DeviceType::Button) { + InputCommon::GetGCButtons()->BeginConfiguration(); + } else { + InputCommon::GetGCAnalogs()->BeginConfiguration(); + } timeout_timer->start(5000); // Cancel after 5 seconds poll_timer->start(200); // Check for new inputs every 200ms } @@ -596,6 +649,9 @@ void ConfigureInputPlayer::SetPollingResult(const Common::ParamPackage& params, poller->Stop(); } + InputCommon::GetGCButtons()->EndConfiguration(); + InputCommon::GetGCAnalogs()->EndConfiguration(); + if (!abort) { (*input_setter)(params); }