diff options
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| m--------- | externals/libusb | 0 | ||||
| -rw-r--r-- | src/input_common/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | src/input_common/gcadapter/gc_adapter.cpp | 379 | ||||
| -rw-r--r-- | src/input_common/gcadapter/gc_adapter.h | 160 | ||||
| -rw-r--r-- | src/input_common/gcadapter/gc_poller.cpp | 272 | ||||
| -rw-r--r-- | src/input_common/gcadapter/gc_poller.h | 67 | ||||
| -rw-r--r-- | src/input_common/main.cpp | 24 | ||||
| -rw-r--r-- | src/input_common/main.h | 5 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_input_player.cpp | 60 | 
11 files changed, 980 insertions, 2 deletions
diff --git a/.gitmodules b/.gitmodules index 6fa823c1c..79028bbb5 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 d0af994da..27383bce8 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 +Subproject 3406d72cda879f8792a88bf5f6bd0b7a65636f7 diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index a9c2392b1..3bd76dd23 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 000000000..b39d2a3fb --- /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 <chrono> +#include <thread> +#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<PadButton, 12> 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<u8, 37>& 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<PadButton, 8> 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<PadButton, 4> 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<u16>(b1_buttons[i]); +            } +        } + +        for (std::size_t j = 0; j < b2_buttons.size(); ++j) { +            if ((b2 & (1U << j)) != 0) { +                pad.button |= static_cast<u16>(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<u16>(button); +        state.buttons.insert_or_assign(button_value, pad.button & button_value); +    } + +    state.axes.insert_or_assign(static_cast<u8>(PadAxes::StickX), pad.stick_x); +    state.axes.insert_or_assign(static_cast<u8>(PadAxes::StickY), pad.stick_y); +    state.axes.insert_or_assign(static_cast<u8>(PadAxes::SubstickX), pad.substick_x); +    state.axes.insert_or_assign(static_cast<u8>(PadAxes::SubstickY), pad.substick_y); +    state.axes.insert_or_assign(static_cast<u8>(PadAxes::TriggerLeft), pad.trigger_left); +    state.axes.insert_or_assign(static_cast<u8>(PadAxes::TriggerRight), pad.trigger_right); +} + +void Adapter::Read() { +    LOG_DEBUG(Input, "GC Adapter Read() thread started"); + +    int payload_size_in, payload_size_copy; +    std::array<u8, 37> adapter_payload; +    std::array<u8, 37> adapter_payload_copy; +    std::array<GCPadStatus, 4> pads; + +    while (adapter_thread_running) { +        libusb_interrupt_transfer(usb_adapter_handle, input_endpoint, adapter_payload.data(), +                                  sizeof(adapter_payload), &payload_size_in, 16); +        payload_size_copy = 0; +        // this mutex might be redundant? +        { +            std::lock_guard<std::mutex> lk(s_mutex); +            std::copy(std::begin(adapter_payload), std::end(adapter_payload), +                      std::begin(adapter_payload_copy)); +            payload_size_copy = payload_size_in; +        } + +        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<std::mutex> lk(initialization_mutex); +            Setup(); +        } +        std::this_thread::sleep_for(std::chrono::milliseconds(500)); +    } +} + +void Adapter::StartScanThread() { +    if (detect_thread_running) { +        return; +    } +    if (!libusb_ctx) { +        return; +    } + +    detect_thread_running = true; +    detect_thread = std::thread([=] { 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<std::mutex> lock(initialization_mutex, std::defer_lock); +    if (!lock.try_lock()) { +        return; +    } +    if (current_status != ADAPTER_DETECTED) { +        return; +    } + +    if (adapter_thread_running) { +        adapter_thread_running = false; +    } +    adapter_input_thread.join(); + +    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<Common::SPSCQueue<GCPadStatus>, 4>& Adapter::GetPadQueue() { +    return pad_queue; +} + +const std::array<Common::SPSCQueue<GCPadStatus>, 4>& Adapter::GetPadQueue() const { +    return pad_queue; +} + +std::array<GCState, 4>& Adapter::GetPadState() { +    return state; +} + +const std::array<GCState, 4>& 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 000000000..0ea6263eb --- /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 <algorithm> +#include <functional> +#include <mutex> +#include <thread> +#include <libusb.h> +#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<PadButton, 12> 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<int, bool> buttons; +    std::unordered_map<int, u16> 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<Common::SPSCQueue<GCPadStatus>, 4>& GetPadQueue(); +    const std::array<Common::SPSCQueue<GCPadStatus>, 4>& GetPadQueue() const; + +    std::array<GCState, 4>& GetPadState(); +    const std::array<GCState, 4>& GetPadState() const; + +private: +    GCPadStatus GetPadStatus(int port, const std::array<u8, 37>& 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<ControllerTypes, 4> adapter_controllers_status{}; + +    std::mutex s_mutex; + +    std::thread adapter_input_thread; +    bool adapter_thread_running; + +    std::mutex initialization_mutex; +    std::thread detect_thread; +    bool detect_thread_running = false; + +    libusb_context* libusb_ctx; + +    u8 input_endpoint = 0; +    u8 output_endpoint = 0; + +    bool configuring = false; + +    std::array<Common::SPSCQueue<GCPadStatus>, 4> pad_queue; +    std::array<GCState, 4> 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 000000000..385ce8430 --- /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 <atomic> +#include <list> +#include <mutex> +#include <utility> +#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<GCAdapter::Adapter> adapter_) +    : adapter(std::move(adapter_)) {} + +GCButton::~GCButton() = default; + +std::unique_ptr<Input::ButtonDevice> 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<u16>(GCAdapter::PadButton::PAD_STICK); + +    // button is not an axis/stick button +    if (button_id != PAD_STICK_ID) { +        auto button = std::make_unique<GCButton>(port, button_id, adapter.get()); +        return std::move(button); +    } + +    // 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<GCAxisButton>(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<int>(port)); +            for (const auto& button : GCAdapter::PadButtonArray) { +                const u16 button_value = static_cast<u16>(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<u8>(pad.axis)); +                params.Set("button", static_cast<u16>(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<float, float> 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<float, float> 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<GCAdapter::Adapter> 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<Input::AnalogDevice> 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<GCAnalog>(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<u8>(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 000000000..e96af7d51 --- /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 <memory> +#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<Input::ButtonDevice> { +public: +    explicit GCButtonFactory(std::shared_ptr<GCAdapter::Adapter> 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<Input::ButtonDevice> 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<GCAdapter::Adapter> adapter; +    bool polling = false; +}; + +/// An analog device factory that creates analog devices from GC Adapter +class GCAnalogFactory final : public Input::Factory<Input::AnalogDevice> { +public: +    explicit GCAnalogFactory(std::shared_ptr<GCAdapter::Adapter> adapter_); + +    std::unique_ptr<Input::AnalogDevice> 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<GCAdapter::Adapter> 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 95e351e24..fd0af1019 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -4,8 +4,11 @@  #include <memory>  #include <thread> +#include <libusb.h>  #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<MotionEmu> motion_emu;  static std::unique_ptr<SDL::State> sdl;  #endif  static std::unique_ptr<CemuhookUDP::State> udp; +static std::shared_ptr<GCButtonFactory> gcbuttons; +static std::shared_ptr<GCAnalogFactory> gcanalog;  void Init() { +    auto gcadapter = std::make_shared<GCAdapter::Adapter>(); +    gcbuttons = std::make_shared<GCButtonFactory>(gcadapter); +    Input::RegisterFactory<Input::ButtonDevice>("gcpad", gcbuttons); +    gcanalog = std::make_shared<GCAnalogFactory>(gcadapter); +    Input::RegisterFactory<Input::AnalogDevice>("gcpad", gcanalog); +      keyboard = std::make_shared<Keyboard>();      Input::RegisterFactory<Input::ButtonDevice>("keyboard", keyboard);      Input::RegisterFactory<Input::AnalogDevice>("analog_from_button", @@ -48,6 +59,11 @@ void Shutdown() {      sdl.reset();  #endif      udp.reset(); +    Input::UnregisterFactory<Input::ButtonDevice>("gcpad"); +    Input::UnregisterFactory<Input::AnalogDevice>("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 77a0ce90b..0e32856f6 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -7,6 +7,7 @@  #include <memory>  #include <string>  #include <vector> +#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 a05fa64ba..00433926d 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);      }  | 
