diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index bcfff85e..db825ecc 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 + -G --gamepad= -h --help -K @@ -129,7 +130,7 @@ _scrcpy() { return ;; --gamepad) - COMPREPLY=($(compgen -W 'disabled aoa' -- "$cur")) + COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) return ;; --orientation|--display-orientation) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 5cbfd84b..b5ceda3a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,7 +33,8 @@ 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)' + '-G[Use UHID gamepad (same as --gamepad=uhid)]' + '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid 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 e3a7501a..fc752e86 100644 --- a/app/meson.build +++ b/app/meson.build @@ -37,6 +37,7 @@ src = [ 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', + 'src/uhid/gamepad_uhid.c', 'src/uhid/keyboard_uhid.c', 'src/uhid/mouse_uhid.c', 'src/uhid/uhid_output.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2e3522af..e4295feb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -175,13 +175,18 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. +.TP +.B \-K +Same as \fB\-\-gamepad=uhid\fR. + .TP .BI "\-\-gamepad " mode Select how to send gamepad inputs to the device. -Possible values are "disabled" and "aoa": +Possible values are "disabled", "uhid" and "aoa": - "disabled" does not send gamepad inputs to the device. + - "uhid" simulates physical HID gamepads using the Linux HID kernel module on 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. diff --git a/app/src/cli.c b/app/src/cli.c index 6fb8cdcb..7fe0bc70 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -373,13 +373,19 @@ static const struct sc_option options[] = { .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", }, + { + .shortopt = 'G', + .text = "Same as --gamepad=uhid.", + }, { .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" + "Possible values are \"disabled\", \"uhid\" and \"aoa\".\n" "\"disabled\" does not send gamepad inputs to the device.\n" + "\"uhid\" simulates physical HID gamepads using the Linux UHID " + "kernel module on the device.\n" "\"aoa\" simulates physical gamepads using the AOAv2 protocol." "It may only work over USB.\n" "Also see --keyboard and --mouse.", @@ -2065,6 +2071,11 @@ parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) { return true; } + if (!strcmp(optarg, "uhid")) { + *mode = SC_GAMEPAD_INPUT_MODE_UHID; + return true; + } + if (!strcmp(optarg, "aoa")) { #ifdef HAVE_USB *mode = SC_GAMEPAD_INPUT_MODE_AOA; @@ -2645,6 +2656,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_AUDIO_DUP: opts->audio_dup = true; break; + case 'G': + opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID; + break; case OPT_GAMEPAD: if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { return false; diff --git a/app/src/options.h b/app/src/options.h index a7b96bb6..e2652773 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -158,6 +158,7 @@ enum sc_mouse_input_mode { enum sc_gamepad_input_mode { SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_UHID, SC_GAMEPAD_INPUT_MODE_AOA, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index fbd00db7..71e64344 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -25,6 +25,7 @@ #include "recorder.h" #include "screen.h" #include "server.h" +#include "uhid/gamepad_uhid.h" #include "uhid/keyboard_uhid.h" #include "uhid/mouse_uhid.h" #ifdef HAVE_USB @@ -80,9 +81,12 @@ struct scrcpy { struct sc_mouse_aoa mouse_aoa; #endif }; + union { + struct sc_gamepad_uhid gamepad_uhid; #ifdef HAVE_USB - struct sc_gamepad_aoa gamepad_aoa; + struct sc_gamepad_aoa gamepad_aoa; #endif + }; struct sc_timeout timeout; }; @@ -750,6 +754,11 @@ aoa_complete: mp = &s->mouse_uhid.mouse_processor; } + if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) { + sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller); + gp = &s->gamepad_uhid.gamepad_processor; + } + sc_controller_configure(&s->controller, acksync, uhid_devices); if (!sc_controller_start(&s->controller)) { diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c new file mode 100644 index 00000000..3c8d3643 --- /dev/null +++ b/app/src/uhid/gamepad_uhid.c @@ -0,0 +1,122 @@ +#include "gamepad_uhid.h" + +#include "hid/hid_gamepad.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast gamepad processor to sc_gamepad_uhid */ +#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) + +static void +sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_input *hid_input, + const char *name) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = hid_input->hid_id; + + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_INPUT message (%s)", name); + } +} + +static void +sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_open *hid_open) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.report_desc = hid_open->report_desc; + msg.uhid_create.report_desc_size = hid_open->report_desc_size; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_CREATE message (gamepad)"); + } +} + +static void +sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_close *hid_close) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY; + msg.uhid_create.id = hid_close->hid_id; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_DESTROY message (gamepad)"); + } +} + +static void +sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_uhid *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; + } + + sc_gamepad_uhid_send_open(gamepad, &hid_open); + } 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; + } + + sc_gamepad_uhid_send_close(gamepad, &hid_close); + } +} + +static void +sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, + event)) { + return; + } + + sc_gamepad_uhid_send_input(gamepad, &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_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, + event)) { + return; + } + + sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button"); + +} + +void +sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, + struct sc_controller *controller) { + sc_hid_gamepad_init(&gamepad->hid); + + gamepad->controller = controller; + + 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; +} diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h new file mode 100644 index 00000000..07d03099 --- /dev/null +++ b/app/src/uhid/gamepad_uhid.h @@ -0,0 +1,23 @@ +#ifndef SC_GAMEPAD_UHID_H +#define SC_GAMEPAD_UHID_H + +#include "common.h" + +#include + +#include "controller.h" +#include "hid/hid_gamepad.h" +#include "trait/gamepad_processor.h" + +struct sc_gamepad_uhid { + struct sc_gamepad_processor gamepad_processor; // gamepad processor trait + + struct sc_hid_gamepad hid; + struct sc_controller *controller; +}; + +void +sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse, + struct sc_controller *controller); + +#endif diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 1494c10a..e656cbb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -215,6 +215,9 @@ public class Controller implements AsyncProcessor { case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); break; + case ControlMessage.TYPE_UHID_DESTROY: + getUhidManager().close(msg.getId()); + break; case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index a7d55b7e..408dbf5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -95,6 +95,12 @@ public final class UhidManager { } } + private void unregisterUhidListener(FileDescriptor fd) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue.removeOnFileDescriptorEventListener(fd); + } + } + private static byte[] extractHidOutputData(ByteBuffer buffer) { /* * #define UHID_DATA_MAX 4096 @@ -199,9 +205,15 @@ public final class UhidManager { } public void close(int id) { - FileDescriptor fd = fds.get(id); - assert fd != null; - close(fd); + // Linux: Documentation/hid/uhid.rst + // If you close() the fd, the device is automatically unregistered and destroyed internally. + FileDescriptor fd = fds.remove(id); + if (fd != null) { + unregisterUhidListener(fd); + close(fd); + } else { + Ln.w("Closing unknown UHID device: " + id); + } } public void closeAll() {