From b0b05909c0b1e094c4ffec6cb54f62e9f24bc928 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH] Add UHID gamepad support Similar to UHID keyboard and mouse, but for gamepads. Can be enabled with --gamepad=uhid or -G. It is not enabled by default because not all devices support UHID (there is a permission error on old Android versions). --- app/meson.build | 1 + app/scrcpy.1 | 7 +- app/src/cli.c | 18 ++- app/src/options.h | 1 + app/src/scrcpy.c | 11 +- app/src/uhid/gamepad_uhid.c | 122 ++++++++++++++++++ app/src/uhid/gamepad_uhid.h | 23 ++++ .../genymobile/scrcpy/control/Controller.java | 3 + .../scrcpy/control/UhidManager.java | 18 ++- 9 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 app/src/uhid/gamepad_uhid.c create mode 100644 app/src/uhid/gamepad_uhid.h diff --git a/app/meson.build b/app/meson.build index e3a7501aa0..fc752e860c 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 409d1b1519..64966f7c29 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 keyboard inputs to the device. + - "uhid" simulates a physical HID gamepad using the Linux HID kernel module on the device. - "aoa" simulates a physical HID gamepad 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 ef297884c7..dd0774477d 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 a physical HID gamepad using the Linux " + "UHID kernel module on the device.\n" "\"aoa\" simulates a physical gamepad using the AOAv2 " "protocol. It may only work over USB.\n" "Also see --keyboard and --mouse.", @@ -2077,6 +2083,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; @@ -2659,6 +2670,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; @@ -2797,6 +2811,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AUTO) { + // UHID does not work on all devices (with old Android + // versions), so it cannot be enabled by default opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA : SC_GAMEPAD_INPUT_MODE_DISABLED; } diff --git a/app/src/options.h b/app/src/options.h index 075339ce45..60a22d1567 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -159,6 +159,7 @@ enum sc_mouse_input_mode { enum sc_gamepad_input_mode { SC_GAMEPAD_INPUT_MODE_AUTO, 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 7839848f10..810bf18347 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; }; @@ -749,6 +753,11 @@ scrcpy(struct scrcpy_options *options) { 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 0000000000..3c8d364311 --- /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 0000000000..07d030995b --- /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 1494c10af3..e656cbb6e8 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 @@ private boolean handleEvent() throws IOException { 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 a7d55b7ebb..408dbf5d6f 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 @@ private void registerUhidListener(int id, FileDescriptor fd) { } } + 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 @@ private static byte[] buildUhidInput2Req(byte[] data) { } 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() {