From a34a62ca4b4cc5aa946b2c6c2a53d18815160abd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH] Add AOA gamepad support Similar to AOA keyboard and mouse, but for gamepads. Can be enabled with --gamepad=aoa. PR #5270 --- app/data/bash-completion/scrcpy | 5 ++ app/data/zsh-completion/_scrcpy | 1 + app/meson.build | 1 + app/scrcpy.1 | 16 ++++-- app/src/cli.c | 44 ++++++++++++++-- app/src/options.c | 1 + app/src/options.h | 6 +++ app/src/scrcpy.c | 29 +++++++++-- app/src/usb/gamepad_aoa.c | 91 +++++++++++++++++++++++++++++++++ app/src/usb/gamepad_aoa.h | 25 +++++++++ 10 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 app/src/usb/gamepad_aoa.c create mode 100644 app/src/usb/gamepad_aoa.h diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index e0928cbd..bcfff85e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -26,6 +26,7 @@ _scrcpy() { -e --select-tcpip -f --fullscreen --force-adb-forward + --gamepad= -h --help -K --keyboard= @@ -127,6 +128,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) return ;; + --gamepad) + COMPREPLY=($(compgen -W 'disabled aoa' -- "$cur")) + return + ;; --orientation|--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0f06ba4b..5cbfd84b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,6 +33,7 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' + '--gamepad=[Set the gamepad input mode]:mode:(disabled aoa)' {-h,--help}'[Print the help]' '-K[Use UHID keyboard (same as --keyboard=uhid)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' diff --git a/app/meson.build b/app/meson.build index a4880420..e3a7501a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -95,6 +95,7 @@ usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', + 'src/usb/gamepad_aoa.c', 'src/usb/keyboard_aoa.c', 'src/usb/mouse_aoa.c', 'src/usb/scrcpy_otg.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9cbb6fcb..2e3522af 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -175,6 +175,16 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. +.TP +.BI "\-\-gamepad " mode +Select how to send gamepad inputs to the device. + +Possible values are "disabled" and "aoa": + + - "disabled" does not send gamepad inputs to the device. + - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. + +Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. .TP .B \-h, \-\-help Print this help. @@ -200,7 +210,7 @@ For "uhid" and "aoa", the keyboard layout must be configured (once and for all) This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). -Also see \fB\-\-mouse\fR. +Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR. .TP .B \-\-kill\-adb\-on\-close @@ -267,7 +277,7 @@ In "uhid" and "aoa" modes, the computer mouse is captured to control the device LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. -Also see \fB\-\-keyboard\fR. +Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR. .TP .BI "\-\-mouse\-bind " xxxx[:xxxx] @@ -369,7 +379,7 @@ If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable ke It may only work over USB. -See \fB\-\-keyboard\fR and \fB\-\-mouse\fR. +See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR. .TP .BI "\-p, \-\-port " port\fR[:\fIport\fR] diff --git a/app/src/cli.c b/app/src/cli.c index e34987f3..96877a51 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -101,6 +101,7 @@ enum { OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, OPT_AUDIO_DUP, + OPT_GAMEPAD, }; struct sc_option { @@ -372,6 +373,17 @@ static const struct sc_option options[] = { .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", }, + { + .longopt_id = OPT_GAMEPAD, + .longopt = "gamepad", + .argdesc = "mode", + .text = "Select how to send gamepad inputs to the device.\n" + "Possible values are \"disabled\" and \"aoa\".\n" + "\"disabled\" does not send gamepad inputs to the device.\n" + "\"aoa\" simulates physical gamepads using the AOAv2 protocol." + "It may only work over USB.\n" + "Also see --keyboard and --mouse.", + }, { .shortopt = 'h', .longopt = "help", @@ -403,7 +415,7 @@ static const struct sc_option options[] = { "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" "This option is only available when a HID keyboard is enabled " "(or a physical keyboard is connected).\n" - "Also see --mouse.", + "Also see --mouse and --gamepad.", }, { .longopt_id = OPT_KILL_ADB_ON_CLOSE, @@ -502,7 +514,7 @@ static const struct sc_option options[] = { "to control the device directly (relative mouse mode).\n" "LAlt, LSuper or RSuper toggle the capture mode, to give " "control of the mouse back to the computer.\n" - "Also see --keyboard.", + "Also see --keyboard and --gamepad.", }, { .longopt_id = OPT_MOUSE_BIND, @@ -637,7 +649,7 @@ static const struct sc_option options[] = { "Keyboard and mouse may be disabled separately using" "--keyboard=disabled and --mouse=disabled.\n" "It may only work over USB.\n" - "See --keyboard and --mouse.", + "See --keyboard, --mouse and --gamepad.", }, { .shortopt = 'p', @@ -2046,6 +2058,27 @@ parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { return false; } +static bool +parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_GAMEPAD_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_GAMEPAD_INPUT_MODE_AOA; + return true; +#else + LOGE("--gamepad=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported gamepad: %s (expected disabled or aoa)", optarg); + return false; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -2612,6 +2645,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_AUDIO_DUP: opts->audio_dup = true; break; + case OPT_GAMEPAD: + if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index b876b660..f8448792 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -23,6 +23,7 @@ const struct scrcpy_options scrcpy_options_default = { .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, + .gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED, .mouse_bindings = { .pri = { .right_click = SC_MOUSE_BINDING_AUTO, diff --git a/app/src/options.h b/app/src/options.h index 6e77c175..a7b96bb6 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -156,6 +156,11 @@ enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AOA, }; +enum sc_gamepad_input_mode { + SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_AOA, +}; + enum sc_mouse_binding { SC_MOUSE_BINDING_AUTO, SC_MOUSE_BINDING_DISABLED, @@ -231,6 +236,7 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; + enum sc_gamepad_input_mode gamepad_input_mode; struct sc_mouse_bindings mouse_bindings; enum sc_camera_facing camera_facing; struct sc_port_range port_range; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 24738876..bd706cc1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -29,6 +29,7 @@ #include "uhid/mouse_uhid.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" +# include "usb/gamepad_aoa.h" # include "usb/keyboard_aoa.h" # include "usb/mouse_aoa.h" # include "usb/usb.h" @@ -79,6 +80,9 @@ struct scrcpy { struct sc_mouse_aoa mouse_aoa; #endif }; +#ifdef HAVE_USB + struct sc_gamepad_aoa gamepad_aoa; +#endif struct sc_timeout timeout; }; @@ -370,6 +374,7 @@ scrcpy(struct scrcpy_options *options) { bool aoa_hid_initialized = false; bool keyboard_aoa_initialized = false; bool mouse_aoa_initialized = false; + bool gamepad_aoa_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -485,9 +490,11 @@ scrcpy(struct scrcpy_options *options) { } } - if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { - LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); - goto end; + if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); + goto end; + } } sdl_configure(options->video_playback, options->disable_screensaver); @@ -587,6 +594,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_controller *controller = NULL; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; + struct sc_gamepad_processor *gp = NULL; if (options->control) { static const struct sc_controller_callbacks controller_cbs = { @@ -606,7 +614,9 @@ scrcpy(struct scrcpy_options *options) { options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool use_mouse_aoa = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - if (use_keyboard_aoa || use_mouse_aoa) { + bool use_gamepad_aoa = + options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; + if (use_keyboard_aoa || use_mouse_aoa || use_gamepad_aoa) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -672,6 +682,12 @@ scrcpy(struct scrcpy_options *options) { } } + if (use_gamepad_aoa) { + sc_gamepad_aoa_init(&s->gamepad_aoa, &s->aoa); + gp = &s->gamepad_aoa.gamepad_processor; + gamepad_aoa_initialized = true; + } + aoa_complete: if (aoa_fail || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); @@ -740,7 +756,7 @@ aoa_complete: .fp = fp, .kp = kp, .mp = mp, - .gp = NULL, + .gp = gp, .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, @@ -878,6 +894,9 @@ end: if (mouse_aoa_initialized) { sc_mouse_aoa_destroy(&s->mouse_aoa); } + if (gamepad_aoa_initialized) { + sc_gamepad_aoa_destroy(&s->gamepad_aoa); + } sc_aoa_stop(&s->aoa); sc_usb_stop(&s->usb); } diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c new file mode 100644 index 00000000..37587532 --- /dev/null +++ b/app/src/usb/gamepad_aoa.c @@ -0,0 +1,91 @@ +#include "gamepad_aoa.h" + +#include "input_events.h" +#include "util/log.h" + +/** Downcast gamepad processor to gamepad_aoa */ +#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) + +static void +sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } + + // exit_on_error: false (a gamepad open failure should not exit scrcpy) + if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { + LOGW("Could not push AOA HID open (gamepad)"); + } + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { + LOGW("Could not push AOA HID close (gamepad)"); + } + } +} + +static void +sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, + event)) { + return; + } + + if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (gamepad axis)"); + } +} + +static void +sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, + event)) { + return; + } + + if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (gamepad button)"); + } +} + +void +sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) { + gamepad->aoa = aoa; + + sc_hid_gamepad_init(&gamepad->hid); + + static const struct sc_gamepad_processor_ops ops = { + .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, + .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, + }; + + gamepad->gamepad_processor.ops = &ops; +} + +void +sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad) { + (void) gamepad; + // Do nothing, gamepad->aoa will automatically unregister all devices +} diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h new file mode 100644 index 00000000..b2dfbe5e --- /dev/null +++ b/app/src/usb/gamepad_aoa.h @@ -0,0 +1,25 @@ +#ifndef SC_GAMEPAD_AOA_H +#define SC_GAMEPAD_AOA_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "hid/hid_gamepad.h" +#include "trait/gamepad_processor.h" + +struct sc_gamepad_aoa { + struct sc_gamepad_processor gamepad_processor; // gamepad processor trait + + struct sc_hid_gamepad hid; + struct sc_aoa *aoa; +}; + +void +sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa); + +void +sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad); + +#endif