From dd47cefa47f6cbd2e453c14df8e52de592ea6661 Mon Sep 17 00:00:00 2001 From: yangfl Date: Mon, 5 Aug 2024 22:02:12 +0800 Subject: [PATCH 01/67] Fix typos Refs PR #5171 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 4 ++-- app/src/android/keycodes.h | 2 +- app/src/cli.c | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index de2b8ac6..b115e7ff 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -29,7 +29,7 @@ Default is 128K (128000). .BI "\-\-audio\-buffer " ms Configure the audio buffering delay (in milliseconds). -Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). +Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches). Default is 50. @@ -379,7 +379,7 @@ Default is 27183:27199. .TP \fB\-\-pause\-on\-exit\fR[=\fImode\fR] -Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured). +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred). This is useful to prevent the terminal window from automatically closing, so that error messages can be read. diff --git a/app/src/android/keycodes.h b/app/src/android/keycodes.h index 60465a18..03ebb9c8 100644 --- a/app/src/android/keycodes.h +++ b/app/src/android/keycodes.h @@ -633,7 +633,7 @@ enum android_keycode { * Toggles between BS and CS digital satellite services. */ AKEYCODE_TV_SATELLITE_SERVICE = 240, /** Toggle Network key. - * Toggles selecting broacast services. */ + * Toggles selecting broadcast services. */ AKEYCODE_TV_NETWORK = 241, /** Antenna/Cable key. * Toggles broadcast input source between antenna and cable. */ diff --git a/app/src/cli.c b/app/src/cli.c index dd1b6799..a7d85a92 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -156,7 +156,7 @@ static const struct sc_option options[] = { .argdesc = "ms", .text = "Configure the audio buffering delay (in milliseconds).\n" "Lower values decrease the latency, but increase the " - "likelyhood of buffer underrun (causing audio glitches).\n" + "likelihood of buffer underrun (causing audio glitches).\n" "Default is 50.", }, { @@ -654,7 +654,7 @@ static const struct sc_option options[] = { .optional_arg = true, .text = "Configure pause on exit. Possible values are \"true\" (always " "pause on exit), \"false\" (never pause on exit) and " - "\"if-error\" (pause only if an error occured).\n" + "\"if-error\" (pause only if an error occurred).\n" "This is useful to prevent the terminal window from " "automatically closing, so that error messages can be read.\n" "Default is \"false\".\n" @@ -1349,7 +1349,7 @@ print_exit_status(const struct sc_exit_status *status, unsigned cols) { return; } - assert(strlen(text) >= 9); // Contains at least the initial identation + assert(strlen(text) >= 9); // Contains at least the initial indentation // text + 9 to remove the initial indentation printf(" %3d %s\n", status->value, text + 9); @@ -2760,7 +2760,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - // If mouse bindings are not explictly set, configure default bindings + // If mouse bindings are not explicitly set, configure default bindings if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) { assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO); assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO); @@ -3101,7 +3101,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) { if (!strcmp(value, "if-error")) { return SC_PAUSE_ON_EXIT_IF_ERROR; } - // Set to false, inclusing when the value is invalid + // Set to false, including when the value is invalid return SC_PAUSE_ON_EXIT_FALSE; } } From 523f9395326356024f7098107af25c54eab0a6c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 8 Aug 2024 20:29:13 +0200 Subject: [PATCH 02/67] Do not create UHID thread if not used The HandlerThread is used only via the looper queue. --- .../main/java/com/genymobile/scrcpy/control/UhidManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b1e6a9b9..a7d55b7e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -33,13 +33,13 @@ public final class UhidManager { private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; - private final HandlerThread thread = new HandlerThread("UHidManager"); private final MessageQueue queue; public UhidManager(DeviceMessageSender sender) { this.sender = sender; - thread.start(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + HandlerThread thread = new HandlerThread("UHidManager"); + thread.start(); queue = thread.getLooper().getQueue(); } else { queue = null; From 0c95794463ee21317725f6934dc55bd8dc83e0c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Aug 2024 14:27:22 +0200 Subject: [PATCH 03/67] Do not apply all workarounds for ONYX devices Calling fillAppInfo() breaks video mirroring on ONYX devices. Fixes #5182 Refs 2b6089cbfc29c41643e0c0e8049bda3ede777b61 --- .../src/main/java/com/genymobile/scrcpy/Workarounds.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 8fc38555..7de98b72 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -61,7 +61,14 @@ public final class Workarounds { fillConfigurationController(); } - fillAppInfo(); + // On ONYX devices, fillAppInfo() breaks video mirroring: + // + boolean mustFillAppInfo = !Build.BRAND.equalsIgnoreCase("ONYX"); + + if (mustFillAppInfo) { + fillAppInfo(); + } + fillAppContext(); } From 3b241af3f684fed977149e47270dfdb5c2d63d39 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:07:15 +0200 Subject: [PATCH 04/67] Allow to pass an explicit version name on release To build with a specific version name: VERSION=pr1234 ./release.sh If not set, it will use the result of "git describe" (as before). --- release.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.mk b/release.mk index dd544bae..7f082144 100644 --- a/release.mk +++ b/release.mk @@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 -VERSION := $(shell git describe --tags --exclude='*install-release' --always) +VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) DIST := dist WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) From 21b412cd9879b850e44fc0cf52012e8f8078bc60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Sep 2024 14:24:25 +0200 Subject: [PATCH 05/67] Simplify messages reader/writer In Java, control messages were parsed using manual buffering, which was convoluted and error-prone. Instead, read the socket directly through a DataInputStream and a BufferedInputStream. Symmetrically, use a DataOutputStream and a BufferedOutputStream to write messages. --- .../scrcpy/control/ControlChannel.java | 21 +- .../scrcpy/control/ControlMessageReader.java | 231 +++++------------- .../control/ControlProtocolException.java | 9 + .../scrcpy/control/DeviceMessageWriter.java | 38 +-- .../control/ControlMessageReaderTest.java | 220 ++++++++--------- .../control/DeviceMessageWriterTest.java | 27 +- 6 files changed, 210 insertions(+), 336 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java index f24ca117..2f12cdb3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java @@ -3,31 +3,22 @@ package com.genymobile.scrcpy.control; import android.net.LocalSocket; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; public final class ControlChannel { - private final InputStream inputStream; - private final OutputStream outputStream; - private final ControlMessageReader reader = new ControlMessageReader(); - private final DeviceMessageWriter writer = new DeviceMessageWriter(); + private final ControlMessageReader reader; + private final DeviceMessageWriter writer; public ControlChannel(LocalSocket controlSocket) throws IOException { - this.inputStream = controlSocket.getInputStream(); - this.outputStream = controlSocket.getOutputStream(); + reader = new ControlMessageReader(controlSocket.getInputStream()); + writer = new DeviceMessageWriter(controlSocket.getOutputStream()); } public ControlMessage recv() throws IOException { - ControlMessage msg = reader.next(); - while (msg == null) { - reader.readFrom(inputStream); - msg = reader.next(); - } - return msg; + return reader.read(); } public void send(DeviceMessage msg) throws IOException { - writer.writeTo(msg, outputStream); + writer.write(msg); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index f5cfee75..f2e89da2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,259 +1,152 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.util.Binary; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Position; -import java.io.EOFException; +import java.io.BufferedInputStream; +import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class ControlMessageReader { - static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; - static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31; - static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - static final int BACK_OR_SCREEN_ON_LENGTH = 1; - static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - static final int GET_CLIPBOARD_LENGTH = 1; - static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; - static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4; - static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4; - private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; - private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final DataInputStream dis; - public ControlMessageReader() { - // invariant: the buffer is always in "get" mode - buffer.limit(0); + public ControlMessageReader(InputStream rawInputStream) { + dis = new DataInputStream(new BufferedInputStream(rawInputStream)); } - public boolean isFull() { - return buffer.remaining() == rawBuffer.length; - } - - public void readFrom(InputStream input) throws IOException { - if (isFull()) { - throw new IllegalStateException("Buffer full, call next() to consume"); - } - buffer.compact(); - int head = buffer.position(); - int r = input.read(rawBuffer, head, rawBuffer.length - head); - if (r == -1) { - throw new EOFException("Controller socket closed"); - } - buffer.position(head + r); - buffer.flip(); - } - - public ControlMessage next() { - if (!buffer.hasRemaining()) { - return null; - } - int savedPosition = buffer.position(); - - int type = buffer.get(); - ControlMessage msg; + public ControlMessage read() throws IOException { + int type = dis.readUnsignedByte(); switch (type) { case ControlMessage.TYPE_INJECT_KEYCODE: - msg = parseInjectKeycode(); - break; + return parseInjectKeycode(); case ControlMessage.TYPE_INJECT_TEXT: - msg = parseInjectText(); - break; + return parseInjectText(); case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - msg = parseInjectTouchEvent(); - break; + return parseInjectTouchEvent(); case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - msg = parseInjectScrollEvent(); - break; + return parseInjectScrollEvent(); case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - msg = parseBackOrScreenOnEvent(); - break; + return parseBackOrScreenOnEvent(); case ControlMessage.TYPE_GET_CLIPBOARD: - msg = parseGetClipboard(); - break; + return parseGetClipboard(); case ControlMessage.TYPE_SET_CLIPBOARD: - msg = parseSetClipboard(); - break; + return parseSetClipboard(); case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - msg = parseSetScreenPowerMode(); - break; + return parseSetScreenPowerMode(); case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - msg = ControlMessage.createEmpty(type); - break; + return ControlMessage.createEmpty(type); case ControlMessage.TYPE_UHID_CREATE: - msg = parseUhidCreate(); - break; + return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: - msg = parseUhidInput(); - break; + return parseUhidInput(); default: - Ln.w("Unknown event type: " + type); - msg = null; - break; + throw new ControlProtocolException("Unknown event type: " + type); } - - if (msg == null) { - // failure, reset savedPosition - buffer.position(savedPosition); - } - return msg; } - private ControlMessage parseInjectKeycode() { - if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); - int keycode = buffer.getInt(); - int repeat = buffer.getInt(); - int metaState = buffer.getInt(); + private ControlMessage parseInjectKeycode() throws IOException { + int action = dis.readUnsignedByte(); + int keycode = dis.readInt(); + int repeat = dis.readInt(); + int metaState = dis.readInt(); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } - private int parseBufferLength(int sizeBytes) { + private int parseBufferLength(int sizeBytes) throws IOException { assert sizeBytes > 0 && sizeBytes <= 4; - if (buffer.remaining() < sizeBytes) { - return -1; - } int value = 0; for (int i = 0; i < sizeBytes; ++i) { - value = (value << 8) | (buffer.get() & 0xFF); + value = (value << 8) | dis.readUnsignedByte(); } return value; } - private String parseString() { - int len = parseBufferLength(4); - if (len == -1 || buffer.remaining() < len) { - return null; - } - int position = buffer.position(); - // Move the buffer position to consume the text - buffer.position(position + len); - return new String(rawBuffer, position, len, StandardCharsets.UTF_8); + private String parseString() throws IOException { + byte[] data = parseByteArray(4); + return new String(data, StandardCharsets.UTF_8); } - private byte[] parseByteArray(int sizeBytes) { + private byte[] parseByteArray(int sizeBytes) throws IOException { int len = parseBufferLength(sizeBytes); - if (len == -1 || buffer.remaining() < len) { - return null; - } byte[] data = new byte[len]; - buffer.get(data); + dis.readFully(data); return data; } - private ControlMessage parseInjectText() { + private ControlMessage parseInjectText() throws IOException { String text = parseString(); - if (text == null) { - return null; - } return ControlMessage.createInjectText(text); } - private ControlMessage parseInjectTouchEvent() { - if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); - long pointerId = buffer.getLong(); - Position position = readPosition(buffer); - float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); - int actionButton = buffer.getInt(); - int buttons = buffer.getInt(); + private ControlMessage parseInjectTouchEvent() throws IOException { + int action = dis.readUnsignedByte(); + long pointerId = dis.readLong(); + Position position = parsePosition(); + float pressure = Binary.u16FixedPointToFloat(dis.readShort()); + int actionButton = dis.readInt(); + int buttons = dis.readInt(); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); } - private ControlMessage parseInjectScrollEvent() { - if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { - return null; - } - Position position = readPosition(buffer); - float hScroll = Binary.i16FixedPointToFloat(buffer.getShort()); - float vScroll = Binary.i16FixedPointToFloat(buffer.getShort()); - int buttons = buffer.getInt(); + private ControlMessage parseInjectScrollEvent() throws IOException { + Position position = parsePosition(); + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); + int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } - private ControlMessage parseBackOrScreenOnEvent() { - if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); + private ControlMessage parseBackOrScreenOnEvent() throws IOException { + int action = dis.readUnsignedByte(); return ControlMessage.createBackOrScreenOn(action); } - private ControlMessage parseGetClipboard() { - if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { - return null; - } - int copyKey = Binary.toUnsigned(buffer.get()); + private ControlMessage parseGetClipboard() throws IOException { + int copyKey = dis.readUnsignedByte(); return ControlMessage.createGetClipboard(copyKey); } - private ControlMessage parseSetClipboard() { - if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { - return null; - } - long sequence = buffer.getLong(); - boolean paste = buffer.get() != 0; + private ControlMessage parseSetClipboard() throws IOException { + long sequence = dis.readLong(); + boolean paste = dis.readByte() != 0; String text = parseString(); - if (text == null) { - return null; - } return ControlMessage.createSetClipboard(sequence, text, paste); } - private ControlMessage parseSetScreenPowerMode() { - if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { - return null; - } - int mode = buffer.get(); + private ControlMessage parseSetScreenPowerMode() throws IOException { + int mode = dis.readUnsignedByte(); return ControlMessage.createSetScreenPowerMode(mode); } - private ControlMessage parseUhidCreate() { - if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) { - return null; - } - int id = buffer.getShort(); + private ControlMessage parseUhidCreate() throws IOException { + int id = dis.readUnsignedShort(); byte[] data = parseByteArray(2); - if (data == null) { - return null; - } return ControlMessage.createUhidCreate(id, data); } - private ControlMessage parseUhidInput() { - if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) { - return null; - } - int id = buffer.getShort(); + private ControlMessage parseUhidInput() throws IOException { + int id = dis.readUnsignedShort(); byte[] data = parseByteArray(2); - if (data == null) { - return null; - } return ControlMessage.createUhidInput(id, data); } - private static Position readPosition(ByteBuffer buffer) { - int x = buffer.getInt(); - int y = buffer.getInt(); - int screenWidth = Binary.toUnsigned(buffer.getShort()); - int screenHeight = Binary.toUnsigned(buffer.getShort()); + private Position parsePosition() throws IOException { + int x = dis.readInt(); + int y = dis.readInt(); + int screenWidth = dis.readUnsignedShort(); + int screenHeight = dis.readUnsignedShort(); return new Position(x, y, screenWidth, screenHeight); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java new file mode 100644 index 00000000..cabf63ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java @@ -0,0 +1,9 @@ +package com.genymobile.scrcpy.control; + +import java.io.IOException; + +public class ControlProtocolException extends IOException { + public ControlProtocolException(String message) { + super(message); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index 6bf53bed..a18a2e5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -1,11 +1,11 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { @@ -13,35 +13,35 @@ public class DeviceMessageWriter { private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes - private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final DataOutputStream dos; - public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { - buffer.clear(); - buffer.put((byte) msg.getType()); - switch (msg.getType()) { + public DeviceMessageWriter(OutputStream rawOutputStream) { + dos = new DataOutputStream(new BufferedOutputStream(rawOutputStream)); + } + + public void write(DeviceMessage msg) throws IOException { + int type = msg.getType(); + dos.writeByte(type); + switch (type) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putInt(len); - buffer.put(raw, 0, len); - output.write(rawBuffer, 0, buffer.position()); + dos.writeInt(len); + dos.write(raw, 0, len); break; case DeviceMessage.TYPE_ACK_CLIPBOARD: - buffer.putLong(msg.getSequence()); - output.write(rawBuffer, 0, buffer.position()); + dos.writeLong(msg.getSequence()); break; case DeviceMessage.TYPE_UHID_OUTPUT: - buffer.putShort((short) msg.getId()); + dos.writeShort(msg.getId()); byte[] data = msg.getData(); - buffer.putShort((short) data.length); - buffer.put(data); - output.write(rawBuffer, 0, buffer.position()); + dos.writeShort(data.length); + dos.write(data); break; default: - Ln.w("Unknown device message: " + msg.getType()); - break; + throw new ControlProtocolException("Unknown event type: " + type); } + dos.flush(); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 1737730f..ae18154d 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -18,8 +19,6 @@ public class ControlMessageReaderTest { @Test public void testParseKeycodeEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -29,23 +28,21 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseTextEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); @@ -54,17 +51,18 @@ public class ControlMessageReaderTest { dos.write(text); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals("testé", event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseLongTextEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); @@ -74,17 +72,18 @@ public class ControlMessageReaderTest { dos.write(text); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseTouchEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); @@ -100,12 +99,10 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(-42, event.getPointerId()); @@ -116,12 +113,12 @@ public class ControlMessageReaderTest { Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseScrollEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); @@ -132,15 +129,12 @@ public class ControlMessageReaderTest { dos.writeShort(0); // 0.0f encoded as i16 dos.writeShort(0x8000); // -1.0f encoded as i16 dos.writeInt(1); - byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); Assert.assertEquals(260, event.getPosition().getPoint().getX()); Assert.assertEquals(1026, event.getPosition().getPoint().getY()); @@ -149,96 +143,96 @@ public class ControlMessageReaderTest { Assert.assertEquals(0f, event.getHScroll(), 0f); Assert.assertEquals(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseBackOrScreenOnEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); dos.writeByte(KeyEvent.ACTION_UP); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseExpandNotificationPanelEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseExpandSettingsPanelEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseCollapsePanelsEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseGetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); dos.writeByte(ControlMessage.COPY_KEY_COPY); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseSetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); @@ -247,22 +241,22 @@ public class ControlMessageReaderTest { byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeInt(text.length); dos.write(text); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0102030405060708L, event.getSequence()); Assert.assertEquals("testé", event.getText()); Assert.assertTrue(event.getPaste()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseBigSetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); @@ -278,56 +272,54 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); Assert.assertTrue(event.getPaste()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseSetScreenPowerMode() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); dos.writeByte(Device.POWER_MODE_NORMAL); - byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseRotateDevice() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseUhidCreate() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); @@ -335,21 +327,21 @@ public class ControlMessageReaderTest { byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; dos.writeShort(data.length); // size dos.write(data); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); Assert.assertArrayEquals(data, event.getData()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseUhidInput() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_INPUT); @@ -357,37 +349,37 @@ public class ControlMessageReaderTest { byte[] data = {1, 2, 3, 4, 5}; dos.writeShort(data.length); // size dos.write(data); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType()); Assert.assertEquals(42, event.getId()); Assert.assertArrayEquals(data, event.getData()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseOpenHardKeyboardSettings() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testMultiEvents() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -404,27 +396,29 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - event = reader.next(); + event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testPartialEvents() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -438,31 +432,21 @@ public class ControlMessageReaderTest { dos.writeByte(MotionEvent.ACTION_DOWN); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); - ControlMessage event = reader.next(); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - event = reader.next(); - Assert.assertNull(event); // the event is not complete - - bos.reset(); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(5); // repeat - dos.writeInt(KeyEvent.META_CTRL_ON); - packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - - // the event is now complete - event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(5, event.getRepeat()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + try { + event = reader.read(); + Assert.fail("Reader did not reach EOF"); + } catch (EOFException e) { + // expected + } } } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java index ff1a2fbc..4e4717fd 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java @@ -12,8 +12,6 @@ public class DeviceMessageWriterTest { @Test public void testSerializeClipboard() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - String text = "aéûoç"; byte[] data = text.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -21,12 +19,13 @@ public class DeviceMessageWriterTest { dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); dos.writeInt(data.length); dos.write(data); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createClipboard(text); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createClipboard(text); + writer.write(msg); byte[] actual = bos.toByteArray(); @@ -35,18 +34,17 @@ public class DeviceMessageWriterTest { @Test public void testSerializeAckSetClipboard() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); dos.writeLong(0x0102030405060708L); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); + writer.write(msg); byte[] actual = bos.toByteArray(); @@ -55,8 +53,6 @@ public class DeviceMessageWriterTest { @Test public void testSerializeUhidOutput() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT); @@ -64,12 +60,13 @@ public class DeviceMessageWriterTest { byte[] data = {1, 2, 3, 4, 5}; dos.writeShort(data.length); dos.write(data); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); + writer.write(msg); byte[] actual = bos.toByteArray(); From 903a5aaaf599bf7c9cb9bba0fefbc04aa5ec50bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Sep 2024 08:24:52 +0200 Subject: [PATCH 06/67] Replace "could not" by "cannot" where appropriate "Could not" implies that the system tried to disable the option but encountered an issue or failure. "Cannot" indicates a rule or restriction, meaning it's not possible to perform the action at all. --- app/src/cli.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index a7d85a92..4f4aa551 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2816,7 +2816,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { - LOGE("Could not disable both keyboard and mouse in OTG mode."); + LOGE("Cannot disable both keyboard and mouse in OTG mode."); return false; } } @@ -2857,18 +2857,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { - LOGE("Could not specify both --camera-id and --camera-facing"); + LOGE("Cannot specify both --camera-id and --camera-facing"); return false; } if (opts->camera_size) { if (opts->max_size) { - LOGE("Could not specify both --camera-size and -m/--max-size"); + LOGE("Cannot specify both --camera-size and -m/--max-size"); return false; } if (opts->camera_ar) { - LOGE("Could not specify both --camera-size and --camera-ar"); + LOGE("Cannot specify both --camera-size and --camera-ar"); return false; } } @@ -3009,19 +3009,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (!opts->control) { if (opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); + LOGE("Cannot request to turn screen off if control is disabled"); return false; } if (opts->stay_awake) { - LOGE("Could not request to stay awake if control is disabled"); + LOGE("Cannot request to stay awake if control is disabled"); return false; } if (opts->show_touches) { - LOGE("Could not request to show touches if control is disabled"); + LOGE("Cannot request to show touches if control is disabled"); return false; } if (opts->power_off_on_close) { - LOGE("Could not request power off on close if control is disabled"); + LOGE("Cannot request power off on close if control is disabled"); return false; } } @@ -3046,7 +3046,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], // OTG mode is compatible with only very few options. // Only report obvious errors. if (opts->record_filename) { - LOGE("OTG mode: could not record"); + LOGE("OTG mode: cannot record"); return false; } if (opts->turn_screen_off) { From 33a8c39beb970db5efa6449d4e3058134f3be8e5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Sep 2024 11:26:07 +0200 Subject: [PATCH 07/67] Fix local NDEBUG define The struct definition and the implementation did not use the same NDEBUG constant. --- app/src/delay_buffer.c | 2 -- app/src/delay_buffer.h | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index f6141b35..b27d4939 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -8,8 +8,6 @@ #include "util/log.h" -#define SC_BUFFERING_NDEBUG // comment to debug - /** Downcast frame_sink to sc_delay_buffer */ #define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink) diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 53592372..f0fe97d3 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -12,12 +12,14 @@ #include "util/tick.h" #include "util/vecdeque.h" +#define SC_BUFFERING_NDEBUG // comment to debug + // forward declarations typedef struct AVFrame AVFrame; struct sc_delayed_frame { AVFrame *frame; -#ifndef NDEBUG +#ifndef SC_BUFFERING_NDEBUG sc_tick push_date; #endif }; From 63ced79842656d0ea6447a53b267c20e3253b538 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Sep 2024 11:29:00 +0200 Subject: [PATCH 08/67] Reverse NDEBUG conditions By default, these specific debug logs are disabled. Make the ifdef condition less confusing. --- app/src/audio_player.c | 10 +++++----- app/src/clock.c | 4 ++-- app/src/delay_buffer.c | 4 ++-- app/src/delay_buffer.h | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index dac85bf9..274b6948 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -5,7 +5,7 @@ #include "util/log.h" -#define SC_AUDIO_PLAYER_NDEBUG // comment to debug +//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug /** * Real-time audio player with configurable latency @@ -72,7 +72,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { size_t len = len_int; uint32_t count = TO_SAMPLES(len); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif @@ -162,7 +162,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // swr_convert() returns the number of samples which would have been // written if the buffer was big enough. uint32_t samples = MIN(ret, dst_nb_samples); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); #endif @@ -244,7 +244,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (played) { LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 " samples", skip_samples); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG } else { LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", skip_samples); @@ -282,7 +282,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // However, the buffering level must be smoothed sc_average_push(&ap->avg_buffering, can_read); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", can_read, sc_average_get(&ap->avg_buffering)); #endif diff --git a/app/src/clock.c b/app/src/clock.c index 92989bfe..e9c9ac35 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -4,7 +4,7 @@ #include "util/log.h" -#define SC_CLOCK_NDEBUG // comment to debug +//#define SC_CLOCK_DEBUG // uncomment to debug #define SC_CLOCK_RANGE 32 @@ -24,7 +24,7 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { clock->offset = ((clock->range - 1) * clock->offset + offset) / clock->range; -#ifndef SC_CLOCK_NDEBUG +#ifdef SC_CLOCK_DEBUG LOGD("Clock estimation: pts + %" PRItick, clock->offset); #endif } diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index b27d4939..a9dc9a35 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -78,7 +78,7 @@ run_buffering(void *data) { goto stopped; } -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, pts, dframe.push_date, sc_tick_now()); #endif @@ -204,7 +204,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, return false; } -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG dframe.push_date = sc_tick_now(); #endif diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index f0fe97d3..18c1ce94 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -12,14 +12,14 @@ #include "util/tick.h" #include "util/vecdeque.h" -#define SC_BUFFERING_NDEBUG // comment to debug +//#define SC_BUFFERING_DEBUG // uncomment to debug // forward declarations typedef struct AVFrame AVFrame; struct sc_delayed_frame { AVFrame *frame; -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG sc_tick push_date; #endif }; From f089ea67e17784303f46fe0dc1a4abfccbdd9d26 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Sep 2024 15:36:51 +0200 Subject: [PATCH 09/67] Add missing flag initialization The delay buffer `stopped` field was not initialized. Since it practice the unique instance of sc_delay_buffer is initialized in static memory, the flag was initialized to false as a side effect. But with commit fd0f432e877153d83ed435474fb7b04e41de4269, in debug mode only, the delay buffer was broken. --- app/src/delay_buffer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index a9dc9a35..e89a2092 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -132,6 +132,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, sc_clock_init(&db->clock); sc_vecdeque_init(&db->queue); + db->stopped = false; if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { goto error_destroy_wait_cond; From a7cae595780de4bde22447fb22a25953426a09fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Sep 2024 10:53:44 +0200 Subject: [PATCH 10/67] Improve delay buffer startup The delay buffer clock estimates the clock offset between the PTS and the frame decoded date using an "Exponentially Weighted Moving Average" (EWMA). But for the first frames, the clock have less than SC_CLOCK_RANGE points to average. Since the timing for the first frames are typically the worst ones, give more weight to the last point for the estimation. Once SC_CLOCK_RANGE points are available (i.e. when SC_CLOCK_RANGE == clock->range), the new estimation is equivalent to the previous version. --- app/src/clock.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/clock.c b/app/src/clock.c index e9c9ac35..8a77e514 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -21,8 +21,10 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { } sc_tick offset = system - stream; - clock->offset = ((clock->range - 1) * clock->offset + offset) - / clock->range; + unsigned clock_weight = clock->range - 1; + unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1; + clock->offset = (clock->offset * clock_weight + offset * value_weight) + / SC_CLOCK_RANGE; #ifdef SC_CLOCK_DEBUG LOGD("Clock estimation: pts + %" PRItick, clock->offset); From dea1fe3386b8fdb1b8bb1762ce5c9da4c4590419 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 19:48:44 +0200 Subject: [PATCH 11/67] Validate crop and video size A video width or height of 0 triggered an assert. Fail explicitly instead: the server may actually send this size in practice (for example on cropping with small dimensions, even if the requested crop size is not 0). --- app/src/screen.c | 6 ++++++ server/src/main/java/com/genymobile/scrcpy/Options.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 55a06ab3..dc61e835 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -299,6 +299,12 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, struct sc_screen *screen = DOWNCAST(sink); + if (ctx->width <= 0 || ctx->width > 0xFFFF + || ctx->height <= 0 || ctx->height > 0xFFFF) { + LOGE("Invalid video size: %dx%d", ctx->width, ctx->height); + return false; + } + assert(ctx->width > 0 && ctx->width <= 0xFFFF); assert(ctx->height > 0 && ctx->height <= 0xFFFF); // screen->frame_size is never used before the event is pushed, and the diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2f86d8ce..d07828eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -456,8 +456,14 @@ public class Options { } int width = Integer.parseInt(tokens[0]); int height = Integer.parseInt(tokens[1]); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Invalid crop size: " + width + "x" + height); + } int x = Integer.parseInt(tokens[2]); int y = Integer.parseInt(tokens[3]); + if (x < 0 || y < 0) { + throw new IllegalArgumentException("Invalid crop offset: " + x + ":" + y); + } return new Rect(x, y, x + width, y + height); } From bec3321fff4c6dc3b3dbc61fdc6fd98913988a78 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 19:53:05 +0200 Subject: [PATCH 12/67] Validate server arguments Some command line arguments are passed as is to "adb shell". Therefore, they must not contain special shell characters. --- app/src/server.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 41517f18..b67cb8b2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -218,6 +218,21 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { } } +static bool +validate_string(const char *s) { + // The parameters values are passed as command line arguments to adb, so + // they must either be properly escaped, or they must not contain any + // special shell characters. + // Since they are not properly escaped on Windows anyway (see + // sys/win/process.c), just forbid special shell characters. + if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~")) { + LOGE("Invalid server param: [%s]", s); + return false; + } + + return true; +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -260,6 +275,11 @@ execute_server(struct sc_server *server, } \ cmd[count++] = p; \ } while(0) +#define VALIDATE_STRING(s) do { \ + if (!validate_string(s)) { \ + goto end; \ + } \ + } while(0) ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); @@ -311,6 +331,7 @@ execute_server(struct sc_server *server, ADD_PARAM("tunnel_forward=true"); } if (params->crop) { + VALIDATE_STRING(params->crop); ADD_PARAM("crop=%s", params->crop); } if (!params->control) { @@ -321,9 +342,11 @@ execute_server(struct sc_server *server, ADD_PARAM("display_id=%" PRIu32, params->display_id); } if (params->camera_id) { + VALIDATE_STRING(params->camera_id); ADD_PARAM("camera_id=%s", params->camera_id); } if (params->camera_size) { + VALIDATE_STRING(params->camera_size); ADD_PARAM("camera_size=%s", params->camera_size); } if (params->camera_facing != SC_CAMERA_FACING_ANY) { @@ -331,6 +354,7 @@ execute_server(struct sc_server *server, sc_server_get_camera_facing_name(params->camera_facing)); } if (params->camera_ar) { + VALIDATE_STRING(params->camera_ar); ADD_PARAM("camera_ar=%s", params->camera_ar); } if (params->camera_fps) { @@ -346,15 +370,19 @@ execute_server(struct sc_server *server, ADD_PARAM("stay_awake=true"); } if (params->video_codec_options) { + VALIDATE_STRING(params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options); } if (params->audio_codec_options) { + VALIDATE_STRING(params->audio_codec_options); ADD_PARAM("audio_codec_options=%s", params->audio_codec_options); } if (params->video_encoder) { + VALIDATE_STRING(params->video_encoder); ADD_PARAM("video_encoder=%s", params->video_encoder); } if (params->audio_encoder) { + VALIDATE_STRING(params->audio_encoder); ADD_PARAM("audio_encoder=%s", params->audio_encoder); } if (params->power_off_on_close) { From 6451ad271a0a2a869ceb80836b01e3c35d60cb3d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 20:03:17 +0200 Subject: [PATCH 13/67] Ignore minBufferSize on error A negative return value from AudioRecord.getMinBufferSize() represents an error. Only consider positive values (0 would be invalid). Refs #5228 --- .../com/genymobile/scrcpy/audio/AudioDirectCapture.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 361c7bac..8d4a4c2d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -56,8 +56,11 @@ public class AudioDirectCapture implements AudioCapture { builder.setAudioSource(audioSource); builder.setAudioFormat(AudioConfig.createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); + if (minBufferSize > 0) { + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + } + return builder.build(); } From 265a15e0b19f5bfe339ccdae01f30a50457ceecf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 20:03:50 +0200 Subject: [PATCH 14/67] Accept float values for --max-fps Android accepts a float value, there is no reason to limit the option to be an integer. In particular, it allows to capture at a rate lower than 1 fps. For example, to capture 1 frame every 5 seconds: scrcpy --video-source=camera --max-fps=0.2 It was already possible to pass a float manually: scrcpy --video-source=camera \ --video-codec-options=max-fps-to-encoder:float=0.2 But accepting a float directly for --max-fps is more convenient. Refs --- app/src/cli.c | 28 ++++++++++++++++--- app/src/options.h | 2 +- app/src/server.c | 2 +- app/src/server.h | 2 +- app/src/util/str.c | 19 +++++++++++++ app/src/util/str.h | 8 ++++++ .../java/com/genymobile/scrcpy/Options.java | 14 ++++++++-- .../scrcpy/video/SurfaceEncoder.java | 8 +++--- 8 files changed, 69 insertions(+), 14 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4f4aa551..e2ae4804 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1447,6 +1447,26 @@ parse_integers_arg(const char *s, const char sep, size_t max_items, long *out, return count; } +static bool +parse_float_arg(const char *s, float *out, float min, float max, + const char *name) { + float value; + bool ok = sc_str_parse_float(s, &value); + if (!ok) { + LOGE("Could not parse %s: %s", name, s); + return false; + } + + if (value < min || value > max) { + LOGE("Could not parse %s: value (%f) out-of-range (%f; %f)", + name, value, min, max); + return false; + } + + *out = value; + return true; +} + static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -1474,14 +1494,14 @@ parse_max_size(const char *s, uint16_t *max_size) { } static bool -parse_max_fps(const char *s, uint16_t *max_fps) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps"); +parse_max_fps(const char *s, float *max_fps) { + float value; + bool ok = parse_float_arg(s, &value, 0, (float) (1 << 16), "max fps"); if (!ok) { return false; } - *max_fps = (uint16_t) value; + *max_fps = value; return true; } diff --git a/app/src/options.h b/app/src/options.h index 140d12b1..ee0be00a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -240,7 +240,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - uint16_t max_fps; + float max_fps; enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; enum sc_orientation record_orientation; diff --git a/app/src/server.c b/app/src/server.c index b67cb8b2..320062a4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -321,7 +321,7 @@ execute_server(struct sc_server *server, ADD_PARAM("max_size=%" PRIu16, params->max_size); } if (params->max_fps) { - ADD_PARAM("max_fps=%" PRIu16, params->max_fps); + ADD_PARAM("max_fps=%f" , params->max_fps); } if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { ADD_PARAM("lock_video_orientation=%" PRIi8, diff --git a/app/src/server.h b/app/src/server.h index cffa510e..81e8e05b 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,7 +44,7 @@ struct sc_server_params { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - uint16_t max_fps; + float max_fps; int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/app/src/util/str.c b/app/src/util/str.c index 755369d8..7ca880d7 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -147,6 +147,25 @@ sc_str_parse_integer_with_suffix(const char *s, long *out) { return true; } +bool +sc_str_parse_float(const char *s, float *out) { + char *endptr; + if (*s == '\0') { + return false; + } + errno = 0; + float value = strtof(s, &endptr); + if (errno == ERANGE) { + return false; + } + if (*endptr != '\0') { + return false; + } + + *out = value; + return true; +} + bool sc_str_list_contains(const char *list, char sep, const char *s) { char *p; diff --git a/app/src/util/str.h b/app/src/util/str.h index 20da26f0..98cb1d74 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -66,6 +66,14 @@ sc_str_parse_integers(const char *s, const char sep, size_t max_items, bool sc_str_parse_integer_with_suffix(const char *s, long *out); +/** + * `Parse `s` as a float into `out` + * + * Return true if the conversion succeeded, false otherwise. + */ +bool +sc_str_parse_float(const char *s, float *out); + /** * Search `s` in the list separated by `sep` * diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d07828eb..51daeced 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -29,7 +29,7 @@ public class Options { private boolean audioDup; private int videoBitRate = 8000000; private int audioBitRate = 128000; - private int maxFps; + private float maxFps; private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; @@ -113,7 +113,7 @@ public class Options { return audioBitRate; } - public int getMaxFps() { + public float getMaxFps() { return maxFps; } @@ -321,7 +321,7 @@ public class Options { options.audioBitRate = Integer.parseInt(value); break; case "max_fps": - options.maxFps = Integer.parseInt(value); + options.maxFps = parseFloat("max_fps", value); break; case "lock_video_orientation": options.lockVideoOrientation = Integer.parseInt(value); @@ -493,4 +493,12 @@ public class Options { float floatAr = Float.parseFloat(tokens[0]); return CameraAspectRatio.fromFloat(floatAr); } + + private static float parseFloat(String key, String value) { + try { + return Float.parseFloat(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 8fe0b227..a5f2d1e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -39,7 +39,7 @@ public class SurfaceEncoder implements AsyncProcessor { private final String encoderName; private final List codecOptions; private final int videoBitRate; - private final int maxFps; + private final float maxFps; private final boolean downsizeOnError; private boolean firstFrameSent; @@ -48,8 +48,8 @@ public class SurfaceEncoder implements AsyncProcessor { private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, - boolean downsizeOnError) { + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, + String encoderName, boolean downsizeOnError) { this.capture = capture; this.streamer = streamer; this.videoBitRate = videoBitRate; @@ -225,7 +225,7 @@ public class SurfaceEncoder implements AsyncProcessor { } } - private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { + private static MediaFormat createFormat(String videoMimeType, int bitRate, float maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, videoMimeType); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); From 1d713d759850f4b5de8525cfb73c338be8fef94e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 14:32:32 +0200 Subject: [PATCH 15/67] Do not parse --max-fps float in the client Many parsing and formatting C functions like strtof() and asprintf() are locale-dependent. Forcing a C locale just for the conversions in a way that works on all platforms is a mess. In practice, this is not a problem, scrcpy always uses the C locale, because it never calls: setlocale(LC_ALL, ""); But the max-fps option should not depend on the locale configuration anyway. Since the value is parsed by the client in Java anyway, just forward the string value as is. --- app/src/cli.c | 36 +----------------------------------- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/server.c | 3 ++- app/src/server.h | 2 +- app/src/util/str.c | 19 ------------------- app/src/util/str.h | 8 -------- app/tests/test_cli.c | 2 +- 8 files changed, 7 insertions(+), 67 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index e2ae4804..e34987f3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1447,26 +1447,6 @@ parse_integers_arg(const char *s, const char sep, size_t max_items, long *out, return count; } -static bool -parse_float_arg(const char *s, float *out, float min, float max, - const char *name) { - float value; - bool ok = sc_str_parse_float(s, &value); - if (!ok) { - LOGE("Could not parse %s: %s", name, s); - return false; - } - - if (value < min || value > max) { - LOGE("Could not parse %s: value (%f) out-of-range (%f; %f)", - name, value, min, max); - return false; - } - - *out = value; - return true; -} - static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -1493,18 +1473,6 @@ parse_max_size(const char *s, uint16_t *max_size) { return true; } -static bool -parse_max_fps(const char *s, float *max_fps) { - float value; - bool ok = parse_float_arg(s, &value, 0, (float) (1 << 16), "max fps"); - if (!ok) { - return false; - } - - *max_fps = value; - return true; -} - static bool parse_buffering_time(const char *s, sc_tick *tick) { long value; @@ -2252,9 +2220,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "--keyboard=uhid instead."); return false; case OPT_MAX_FPS: - if (!parse_max_fps(optarg, &opts->max_fps)) { - return false; - } + opts->max_fps = optarg; break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { diff --git a/app/src/options.c b/app/src/options.c index 6fca6ad5..b876b660 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -48,7 +48,7 @@ const struct scrcpy_options scrcpy_options_default = { .max_size = 0, .video_bit_rate = 0, .audio_bit_rate = 0, - .max_fps = 0, + .max_fps = NULL, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, diff --git a/app/src/options.h b/app/src/options.h index ee0be00a..6e77c175 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -240,7 +240,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - float max_fps; + const char *max_fps; // float to be parsed by the server enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; enum sc_orientation record_orientation; diff --git a/app/src/server.c b/app/src/server.c index 320062a4..2dc00144 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -321,7 +321,8 @@ execute_server(struct sc_server *server, ADD_PARAM("max_size=%" PRIu16, params->max_size); } if (params->max_fps) { - ADD_PARAM("max_fps=%f" , params->max_fps); + VALIDATE_STRING(params->max_fps); + ADD_PARAM("max_fps=%s", params->max_fps); } if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { ADD_PARAM("lock_video_orientation=%" PRIi8, diff --git a/app/src/server.h b/app/src/server.h index 81e8e05b..d9d42582 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,7 +44,7 @@ struct sc_server_params { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - float max_fps; + const char *max_fps; // float to be parsed by the server int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/app/src/util/str.c b/app/src/util/str.c index 7ca880d7..755369d8 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -147,25 +147,6 @@ sc_str_parse_integer_with_suffix(const char *s, long *out) { return true; } -bool -sc_str_parse_float(const char *s, float *out) { - char *endptr; - if (*s == '\0') { - return false; - } - errno = 0; - float value = strtof(s, &endptr); - if (errno == ERANGE) { - return false; - } - if (*endptr != '\0') { - return false; - } - - *out = value; - return true; -} - bool sc_str_list_contains(const char *list, char sep, const char *s) { char *p; diff --git a/app/src/util/str.h b/app/src/util/str.h index 98cb1d74..20da26f0 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -66,14 +66,6 @@ sc_str_parse_integers(const char *s, const char sep, size_t max_items, bool sc_str_parse_integer_with_suffix(const char *s, long *out); -/** - * `Parse `s` as a float into `out` - * - * Return true if the conversion succeeded, false otherwise. - */ -bool -sc_str_parse_float(const char *s, float *out); - /** * Search `s` in the list separated by `sep` * diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index cef8df3e..14765792 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -78,7 +78,7 @@ static void test_options(void) { assert(opts->video_bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); - assert(opts->max_fps == 30); + assert(!strcmp(opts->max_fps, "30")); assert(opts->max_size == 1024); assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); From 145a9468fdf64196dc42f24b5935ce7d40ea2543 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 14:37:13 +0200 Subject: [PATCH 16/67] Fix ifdef _WIN32 We use _WIN32 across the code base, not __WIN32. --- app/src/compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/compat.h b/app/src/compat.h index fd610c02..1995d384 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -8,7 +8,7 @@ #include #include -#ifndef __WIN32 +#ifndef _WIN32 # define PRIu64_ PRIu64 # define SC_PRIsizet "zu" #else From 8453e3ba7d1b4f8ec55825edef2d4357d35ce8c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 19:40:43 +0200 Subject: [PATCH 17/67] Enable TCP_NODELAY for the control socket It is better to disable Nagle's algorithm to avoid unnecessary latency for control messages. (I'm not sure this has any impact for a local TCP socket though.) --- app/src/server.c | 8 ++++++++ app/src/util/net.c | 17 +++++++++++++++++ app/src/util/net.h | 4 ++++ 3 files changed, 29 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 2dc00144..e94fcce8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -659,6 +659,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { } } + if (control_socket != SC_SOCKET_NONE) { + // Disable Nagle's algorithm for the control socket + // (it only impacts the sending side, so it is useless to set it + // for the other sockets) + bool ok = net_set_tcp_nodelay(control_socket, true); + (void) ok; // error already logged + } + // we don't need the adb tunnel anymore sc_adb_tunnel_close(tunnel, &server->intr, serial, server->device_socket_name); diff --git a/app/src/util/net.c b/app/src/util/net.c index 67317ead..d43d1c7a 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -15,6 +15,7 @@ # include # include # include +# include # include # include # include @@ -273,6 +274,22 @@ net_close(sc_socket socket) { #endif } +bool +net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay) { + sc_raw_socket raw_sock = unwrap(socket); + + int value = tcp_nodelay ? 1 : 0; + int ret = setsockopt(raw_sock, IPPROTO_TCP, TCP_NODELAY, + (const void *) &value, sizeof(value)); + if (ret == -1) { + net_perror("setsockopt(TCP_NODELAY)"); + return false; + } + + assert(ret == 0); + return true; +} + bool net_parse_ipv4(const char *s, uint32_t *ipv4) { struct in_addr addr; diff --git a/app/src/util/net.h b/app/src/util/net.h index 21396882..ea54b793 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -67,6 +67,10 @@ net_interrupt(sc_socket socket); bool net_close(sc_socket socket); +// Disable Nagle's algorithm (if tcp_nodelay is true) +bool +net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay); + /** * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation */ From e03888d5877e04cb1f4575099eda54d9c009ca3a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 21:21:48 +0200 Subject: [PATCH 18/67] Reject arguments containing new line characters Refs bec3321fff4c6dc3b3dbc61fdc6fd98913988a78 --- app/src/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index e94fcce8..90a0ac5d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -225,7 +225,7 @@ validate_string(const char *s) { // special shell characters. // Since they are not properly escaped on Windows anyway (see // sys/win/process.c), just forbid special shell characters. - if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~")) { + if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~\r\n")) { LOGE("Invalid server param: [%s]", s); return false; } From 90ee0062cb84b618882f78165f057f58d97b6939 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 22:00:43 +0200 Subject: [PATCH 19/67] Fix compilation with -Dusb=false UHID does not depend on USB support, so the struct sc_uhid_devices must always be defined. --- app/src/scrcpy.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 43864661..aafe108c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -63,8 +63,8 @@ struct scrcpy { struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; - struct sc_uhid_devices uhid_devices; #endif + struct sc_uhid_devices uhid_devices; union { struct sc_keyboard_sdk keyboard_sdk; struct sc_keyboard_uhid keyboard_uhid; From 4a6b335f7df04599cbea488f40848c96dc2e7541 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Sep 2024 18:35:16 +0200 Subject: [PATCH 20/67] Do not send uninitialized HID event If the function returns false, then there is nothing to send. --- app/src/uhid/keyboard_uhid.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 515a3fd9..4199547a 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -43,7 +43,9 @@ sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); struct sc_hid_event hid_event; - sc_hid_keyboard_event_from_mods(&hid_event, diff); + if (!sc_hid_keyboard_event_from_mods(&hid_event, diff)) { + return; + } LOGV("HID keyboard state synchronized"); From e8f02685e94608ea8d352d4fc33f0ad77f94ab61 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 21/67] Fix deprecated references in scrcpy manpage The options --hid-keyboard and --hid-mouse do not exist anymore. They have been replaced by --keyboard=XXX and --mouse=XXX. --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b115e7ff..9cbb6fcb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -369,7 +369,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\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. +See \fB\-\-keyboard\fR and \fB\-\-mouse\fR. .TP .BI "\-p, \-\-port " port\fR[:\fIport\fR] From ce4e1fc4204d08b57a3fc63a9d39ad5bd9984812 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 22/67] Store events numbers in an enum This avoids to manually set an explicit value for each item. PR #5270 --- app/src/events.h | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index 3cf2b1dd..f1aa22aa 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,10 +1,21 @@ -#define SC_EVENT_NEW_FRAME SDL_USEREVENT -#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1) -#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) -#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) -#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) -#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) -#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) -#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) -#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) -#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9) +#ifndef SC_EVENTS_H +#define SC_EVENTS_H + +#include "common.h" + +#include + +enum { + SC_EVENT_NEW_FRAME = SDL_USEREVENT, + SC_EVENT_DEVICE_DISCONNECTED, + SC_EVENT_SERVER_CONNECTION_FAILED, + SC_EVENT_SERVER_CONNECTED, + SC_EVENT_USB_DEVICE_DISCONNECTED, + SC_EVENT_DEMUXER_ERROR, + SC_EVENT_RECORDER_ERROR, + SC_EVENT_SCREEN_INIT_SIZE, + SC_EVENT_TIME_LIMIT_REACHED, + SC_EVENT_CONTROLLER_ERROR, +}; + +#endif From e9b32d8a52b418eed8fb00b84de89f6c7ac9e5a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 23/67] Extract sc_push_event() Expose a convenience function to push an event without args to the main thread. PR #5270 --- app/meson.build | 1 + app/src/events.c | 19 +++++++++++++++++++ app/src/events.h | 7 +++++++ app/src/scrcpy.c | 34 +++++++++++----------------------- app/src/screen.c | 18 ++++-------------- app/src/usb/scrcpy_otg.c | 7 +------ 6 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 app/src/events.c diff --git a/app/meson.build b/app/meson.build index b0a6aadb..fc6b85e2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -15,6 +15,7 @@ src = [ 'src/demuxer.c', 'src/device_msg.c', 'src/display.c', + 'src/events.c', 'src/icon.c', 'src/file_pusher.c', 'src/fps_counter.c', diff --git a/app/src/events.c b/app/src/events.c new file mode 100644 index 00000000..4e256be2 --- /dev/null +++ b/app/src/events.c @@ -0,0 +1,19 @@ +#include "events.h" + +#include "util/log.h" + +bool +sc_push_event_impl(uint32_t type, const char *name) { + SDL_Event event; + event.type = type; + int ret = SDL_PushEvent(&event); + // ret < 0: error (queue full) + // ret == 0: event was filtered + // ret == 1: success + if (ret != 1) { + LOGE("Could not post %s event: %s", name, SDL_GetError()); + return false; + } + + return true; +} diff --git a/app/src/events.h b/app/src/events.h index f1aa22aa..d803fb68 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -3,6 +3,8 @@ #include "common.h" +#include +#include #include enum { @@ -18,4 +20,9 @@ enum { SC_EVENT_CONTROLLER_ERROR, }; +bool +sc_push_event_impl(uint32_t type, const char *name); + +#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) + #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aafe108c..625a53a9 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -82,22 +82,10 @@ struct scrcpy { struct sc_timeout timeout; }; -static inline void -push_event(uint32_t type, const char *name) { - SDL_Event event; - event.type = type; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGE("Could not post %s event: %s", name, SDL_GetError()); - // What could we do? - } -} -#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) - #ifdef _WIN32 static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { - PUSH_EVENT(SDL_QUIT); + sc_push_event(SDL_QUIT); return TRUE; } return FALSE; @@ -230,7 +218,7 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success, (void) userdata; if (!success) { - PUSH_EVENT(SC_EVENT_RECORDER_ERROR); + sc_push_event(SC_EVENT_RECORDER_ERROR); } } @@ -244,9 +232,9 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, assert(status != SC_DEMUXER_STATUS_DISABLED); if (status == SC_DEMUXER_STATUS_EOS) { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } else { - PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + sc_push_event(SC_EVENT_DEMUXER_ERROR); } } @@ -260,11 +248,11 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, // Contrary to the video demuxer, keep mirroring if only the audio fails // (unless --require-audio is set). if (status == SC_DEMUXER_STATUS_EOS) { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } else if (status == SC_DEMUXER_STATUS_ERROR || (status == SC_DEMUXER_STATUS_DISABLED && options->require_audio)) { - PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + sc_push_event(SC_EVENT_DEMUXER_ERROR); } } @@ -277,9 +265,9 @@ sc_controller_on_ended(struct sc_controller *controller, bool error, (void) userdata; if (error) { - PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + sc_push_event(SC_EVENT_CONTROLLER_ERROR); } else { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } } @@ -288,7 +276,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED); + sc_push_event(SC_EVENT_SERVER_CONNECTION_FAILED); } static void @@ -296,7 +284,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(SC_EVENT_SERVER_CONNECTED); + sc_push_event(SC_EVENT_SERVER_CONNECTED); } static void @@ -314,7 +302,7 @@ sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { (void) timeout; (void) userdata; - PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED); + sc_push_event(SC_EVENT_TIME_LIMIT_REACHED); } // Generate a scrcpy id to differentiate multiple running scrcpy instances diff --git a/app/src/screen.c b/app/src/screen.c index dc61e835..42be554a 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -312,14 +312,9 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, screen->frame_size.width = ctx->width; screen->frame_size.height = ctx->height; - static SDL_Event event = { - .type = SC_EVENT_SCREEN_INIT_SIZE, - }; - // Post the event on the UI thread (the texture must be created from there) - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGW("Could not post init size event: %s", SDL_GetError()); + bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE); + if (!ok) { return false; } @@ -358,14 +353,9 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { // The SC_EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead } else { - static SDL_Event new_frame_event = { - .type = SC_EVENT_NEW_FRAME, - }; - // Post the event on the UI thread - int ret = SDL_PushEvent(&new_frame_event); - if (ret < 0) { - LOGW("Could not post new frame event: %s", SDL_GetError()); + bool ok = sc_push_event(SC_EVENT_NEW_FRAME); + if (!ok) { return false; } } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index c1d38da3..715f690a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -21,12 +21,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { (void) usb; (void) userdata; - SDL_Event event; - event.type = SC_EVENT_USB_DEVICE_DISCONNECTED; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGE("Could not post USB disconnection event: %s", SDL_GetError()); - } + sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED); } static enum scrcpy_exit_code From e9240f6804764b2b4d7163566cd6d5cc0226f865 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 24/67] Expose main thread id This will allow to assert that a function is called from the main thread. PR #5270 --- app/src/main.c | 4 ++++ app/src/util/thread.c | 2 ++ app/src/util/thread.h | 2 ++ 3 files changed, 8 insertions(+) diff --git a/app/src/main.c b/app/src/main.c index 6050de11..8bbd074f 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -16,6 +16,7 @@ #include "usb/scrcpy_otg.h" #include "util/log.h" #include "util/net.h" +#include "util/thread.h" #include "version.h" #ifdef _WIN32 @@ -67,6 +68,9 @@ main_scrcpy(int argc, char *argv[]) { goto end; } + // The current thread is the main thread + SC_MAIN_THREAD_ID = sc_thread_get_id(); + #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 94921fb7..9679dfff 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -6,6 +6,8 @@ #include "log.h" +sc_thread_id SC_MAIN_THREAD_ID; + bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 4183adac..3d544046 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -39,6 +39,8 @@ typedef struct sc_cond { SDL_cond *cond; } sc_cond; +extern sc_thread_id SC_MAIN_THREAD_ID; + bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata); From 8620d06741ceb05ef2ad34e1a811c452216092a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 25/67] Add mechanism to execute code on the main thread This allows to schedule a runnable to be executed on the main thread, until the event loop is explicitly terminated. It is guaranteed that all accepted runnables will be executed (this avoids possible memory leaks if a runnable owns resources). PR #5270 --- app/src/events.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ app/src/events.h | 9 +++++++++ app/src/scrcpy.c | 22 ++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/app/src/events.c b/app/src/events.c index 4e256be2..ce885241 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -1,6 +1,7 @@ #include "events.h" #include "util/log.h" +#include "util/thread.h" bool sc_push_event_impl(uint32_t type, const char *name) { @@ -17,3 +18,49 @@ sc_push_event_impl(uint32_t type, const char *name) { return true; } + +bool +sc_post_to_main_thread(sc_runnable_fn run, void *userdata) { + SDL_Event event = { + .user = { + .type = SC_EVENT_RUN_ON_MAIN_THREAD, + .data1 = run, + .data2 = userdata, + }, + }; + int ret = SDL_PushEvent(&event); + // ret < 0: error (queue full) + // ret == 0: event was filtered + // ret == 1: success + if (ret != 1) { + if (ret == 0) { + // if ret == 0, this is expected on exit, log in debug mode + LOGD("Could not post runnable to main thread (filtered)"); + } else { + assert(ret < 0); + LOGW("Could not post runnable to main thread: %s", SDL_GetError()); + } + return false; + } + + return true; +} + +static int SDLCALL +task_event_filter(void *userdata, SDL_Event *event) { + (void) userdata; + + if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) { + // Reject this event type from now on + return 0; + } + + return 1; +} + +void +sc_reject_new_runnables(void) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + SDL_SetEventFilter(task_event_filter, NULL); +} diff --git a/app/src/events.h b/app/src/events.h index d803fb68..3f15087a 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -9,6 +9,7 @@ enum { SC_EVENT_NEW_FRAME = SDL_USEREVENT, + SC_EVENT_RUN_ON_MAIN_THREAD, SC_EVENT_DEVICE_DISCONNECTED, SC_EVENT_SERVER_CONNECTION_FAILED, SC_EVENT_SERVER_CONNECTED, @@ -25,4 +26,12 @@ sc_push_event_impl(uint32_t type, const char *name); #define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) +typedef void (*sc_runnable_fn)(void *userdata); + +bool +sc_post_to_main_thread(sc_runnable_fn run, void *userdata); + +void +sc_reject_new_runnables(void); + #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 625a53a9..efad5891 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -174,6 +174,12 @@ event_loop(struct scrcpy *s) { case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; + case SC_EVENT_RUN_ON_MAIN_THREAD: { + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); + break; + } default: if (!sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; @@ -184,6 +190,21 @@ event_loop(struct scrcpy *s) { return SCRCPY_EXIT_FAILURE; } +static void +terminate_event_loop(void) { + sc_reject_new_runnables(); + + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) { + // Make sure all posted runnables are run, to avoid memory leaks + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); + } + } +} + // Return true on success, false on error static bool await_for_server(bool *connected) { @@ -819,6 +840,7 @@ scrcpy(struct scrcpy_options *options) { } ret = event_loop(s); + terminate_event_loop(); LOGD("quit..."); if (options->video_playback) { From 72ee195693a030990e9965ac93922c9aae32988d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 26/67] Set clipboard from the main thread The clipboard changes from the device are received from a separate thread, but they must be handled from the main thread. PR #5270 --- app/src/receiver.c | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 3e572067..2911f8b9 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -6,8 +6,10 @@ #include #include "device_msg.h" +#include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/thread.h" bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, @@ -33,20 +35,39 @@ sc_receiver_destroy(struct sc_receiver *receiver) { sc_mutex_destroy(&receiver->mutex); } +static void +task_set_clipboard(void *userdata) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + char *text = userdata; + + char *current = SDL_GetClipboardText(); + bool same = current && !strcmp(current, text); + SDL_free(current); + if (same) { + LOGD("Computer clipboard unchanged"); + } else { + LOGI("Device clipboard copied"); + SDL_SetClipboardText(text); + } + + free(text); +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - char *current = SDL_GetClipboardText(); - bool same = current && !strcmp(current, msg->clipboard.text); - SDL_free(current); - if (same) { - LOGD("Computer clipboard unchanged"); + // Take ownership of the text (do not destroy the msg) + char *text = msg->clipboard.text; + + bool ok = sc_post_to_main_thread(task_set_clipboard, text); + if (!ok) { + LOGW("Could not post clipboard to main thread"); + free(text); return; } - LOGI("Device clipboard copied"); - SDL_SetClipboardText(msg->clipboard.text); break; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: @@ -64,6 +85,7 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); + // No allocation to free in the msg break; case DEVICE_MSG_TYPE_UHID_OUTPUT: if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { @@ -86,6 +108,7 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { // Also check at runtime (do not trust the server) if (!receiver->uhid_devices) { LOGE("Received unexpected HID output message"); + sc_device_msg_destroy(msg); return; } @@ -99,6 +122,8 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } else { LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); } + + sc_device_msg_destroy(msg); break; } } @@ -117,7 +142,7 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { } process_msg(receiver, &msg); - sc_device_msg_destroy(&msg); + // the device msg must be destroyed by process_msg() head += r; assert(head <= len); From cbf5db85c1931d07e9adad32d788fdf84366b74d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 27/67] Process UHID outputs events from the main thread This will guarantee that the callbacks of UHID devices implementations will always be called from the same (main) thread. PR #5270 --- app/src/receiver.c | 57 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 2911f8b9..42682cb4 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -11,6 +11,13 @@ #include "util/str.h" #include "util/thread.h" +struct sc_uhid_output_task_data { + struct sc_uhid_devices *uhid_devices; + uint16_t id; + uint16_t size; + uint8_t *data; +}; + bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, const struct sc_receiver_callbacks *cbs, void *cbs_userdata) { @@ -54,6 +61,25 @@ task_set_clipboard(void *userdata) { free(text); } +static void +task_uhid_output(void *userdata) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + struct sc_uhid_output_task_data *data = userdata; + + struct sc_uhid_receiver *uhid_receiver = + sc_uhid_devices_get_receiver(data->uhid_devices, data->id); + if (uhid_receiver) { + uhid_receiver->ops->process_output(uhid_receiver, data->data, + data->size); + } else { + LOGW("No UHID receiver for id %" PRIu16, data->id); + } + + free(data->data); + free(data); +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { @@ -112,18 +138,29 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { return; } - struct sc_uhid_receiver *uhid_receiver = - sc_uhid_devices_get_receiver(receiver->uhid_devices, - msg->uhid_output.id); - if (uhid_receiver) { - uhid_receiver->ops->process_output(uhid_receiver, - msg->uhid_output.data, - msg->uhid_output.size); - } else { - LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); + struct sc_uhid_output_task_data *data = malloc(sizeof(*data)); + if (!data) { + LOG_OOM(); + return; + } + + // It is guaranteed that these pointers will still be valid when + // the main thread will process them (the main thread will stop + // processing SC_EVENT_RUN_ON_MAIN_THREAD on exit, when everything + // gets deinitialized) + data->uhid_devices = receiver->uhid_devices; + data->id = msg->uhid_output.id; + data->data = msg->uhid_output.data; // take ownership + data->size = msg->uhid_output.size; + + bool ok = sc_post_to_main_thread(task_uhid_output, data); + if (!ok) { + LOGW("Could not post UHID output to main thread"); + free(data->data); + free(data); + return; } - sc_device_msg_destroy(msg); break; } } From a84b0dfd0c3abe7a91e21d8fcd4b3ec33a36964a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 28/67] Remove atomics from keyboard_uhid The UHID output callback is now called from the same (main) thread as the process_key() function. PR #5270 --- app/src/uhid/keyboard_uhid.c | 21 +++++++++------------ app/src/uhid/keyboard_uhid.h | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 4199547a..d63d0ab0 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -31,16 +31,13 @@ static void sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { SDL_Keymod sdl_mod = SDL_GetModState(); uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM); - - uint16_t device_mod = - atomic_load_explicit(&kb->device_mod, memory_order_relaxed); - uint16_t diff = mod ^ device_mod; + uint16_t diff = mod ^ kb->device_mod; if (diff) { // Inherently racy (the HID output reports arrive asynchronously in // response to key presses), but will re-synchronize on next key press // or HID output anyway - atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); + kb->device_mod = mod; struct sc_hid_event hid_event; if (!sc_hid_keyboard_event_from_mods(&hid_event, diff)) { @@ -59,6 +56,8 @@ sc_key_processor_process_key(struct sc_key_processor *kp, uint64_t ack_to_wait) { (void) ack_to_wait; + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so // just ignore key repeat here. @@ -72,11 +71,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // Not all keys are supported, just ignore unsupported keys if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { if (event->scancode == SC_SCANCODE_CAPSLOCK) { - atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS, - memory_order_relaxed); + kb->device_mod ^= SC_MOD_CAPS; } else if (event->scancode == SC_SCANCODE_NUMLOCK) { - atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM, - memory_order_relaxed); + kb->device_mod ^= SC_MOD_NUM; } else { // Synchronize modifiers (only if the scancode itself does not // change the modifiers) @@ -103,7 +100,7 @@ sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { static void sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, const uint8_t *data, size_t len) { - // Called from the thread receiving device messages + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); assert(len); @@ -117,7 +114,7 @@ sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, uint8_t hid_led = data[0]; uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); - atomic_store_explicit(&kb->device_mod, device_mod, memory_order_relaxed); + kb->device_mod = device_mod; } bool @@ -127,7 +124,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, sc_hid_keyboard_init(&kb->hid); kb->controller = controller; - atomic_init(&kb->device_mod, 0); + kb->device_mod = 0; static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h index 5e1be70c..639a3384 100644 --- a/app/src/uhid/keyboard_uhid.h +++ b/app/src/uhid/keyboard_uhid.h @@ -16,7 +16,7 @@ struct sc_keyboard_uhid { struct sc_hid_keyboard hid; struct sc_controller *controller; - atomic_uint_least16_t device_mod; + uint16_t device_mod; }; bool From 49c8ca34fd96548bd40862fc30a69dd54a68d2b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 29/67] Introduce non-droppable control messages Control messages are queued from the main thread and sent to the device from a separate thread. When the queue is full, messages are just dropped. This avoids to accumulate too much delay between the client and the device in case of network issue. However, some messages should not be dropped: for example, dropping a UHID_CREATE message would make all further UHID_INPUT messages invalid. Therefore, mark these messages as non-droppable. A non-droppable event is queued anyway (resizing the queue if necessary, unless the allocation fails). PR #5270 --- app/src/control_msg.c | 7 +++++++ app/src/control_msg.h | 5 +++++ app/src/controller.c | 26 ++++++++++++++++++++------ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 9b0fab67..daa3bde7 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -278,6 +278,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } } +bool +sc_control_msg_is_droppable(const struct sc_control_msg *msg) { + // Cannot drop UHID_CREATE messages, because it would cause all further + // UHID_INPUT messages for this device to be invalid + return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE; +} + void sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 80714096..63670705 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -116,6 +116,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf); void sc_control_msg_log(const struct sc_control_msg *msg); +// Even when the buffer is "full", some messages must absolutely not be dropped +// to avoid inconsistencies. +bool +sc_control_msg_is_droppable(const struct sc_control_msg *msg); + void sc_control_msg_destroy(struct sc_control_msg *msg); diff --git a/app/src/controller.c b/app/src/controller.c index d50e1921..749de0a5 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -4,7 +4,8 @@ #include "util/log.h" -#define SC_CONTROL_MSG_QUEUE_MAX 64 +// Drop droppable events above this limit +#define SC_CONTROL_MSG_QUEUE_LIMIT 60 static void sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error, @@ -22,7 +23,9 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, void *cbs_userdata) { sc_vecdeque_init(&controller->queue); - bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); + // Add 4 to support 4 non-droppable events without re-allocation + bool ok = sc_vecdeque_reserve(&controller->queue, + SC_CONTROL_MSG_QUEUE_LIMIT + 4); if (!ok) { return false; } @@ -93,20 +96,31 @@ sc_controller_push_msg(struct sc_controller *controller, sc_control_msg_log(msg); } + bool pushed = false; + sc_mutex_lock(&controller->mutex); - bool full = sc_vecdeque_is_full(&controller->queue); - if (!full) { + size_t size = sc_vecdeque_size(&controller->queue); + if (size < SC_CONTROL_MSG_QUEUE_LIMIT) { bool was_empty = sc_vecdeque_is_empty(&controller->queue); sc_vecdeque_push_noresize(&controller->queue, *msg); + pushed = true; if (was_empty) { sc_cond_signal(&controller->msg_cond); } + } else if (!sc_control_msg_is_droppable(msg)) { + bool ok = sc_vecdeque_push(&controller->queue, *msg); + if (ok) { + pushed = true; + } else { + // A non-droppable event must be dropped anyway + LOG_OOM(); + } } - // Otherwise (if the queue is full), the msg is discarded + // Otherwise, the msg is discarded sc_mutex_unlock(&controller->mutex); - return !full; + return pushed; } static bool From 08da2e068e7f535464b6e801dad2f74e11370b9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 30/67] Fail on AOA keyboard/mouse initialization error If the AOA keyboard or the AOA mouse fails to be initialized, this is a fatal error. PR #5270 --- app/src/scrcpy.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index efad5891..529a3fc2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -641,12 +641,15 @@ scrcpy(struct scrcpy_options *options) { goto end; } + bool aoa_fail = false; if (use_keyboard_aoa) { if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { keyboard_aoa_initialized = true; kp = &s->keyboard_aoa.key_processor; } else { LOGE("Could not initialize HID keyboard"); + aoa_fail = true; + goto aoa_complete; } } @@ -656,12 +659,13 @@ scrcpy(struct scrcpy_options *options) { mp = &s->mouse_aoa.mouse_processor; } else { LOGE("Could not initialized HID mouse"); + aoa_fail = true; + goto aoa_complete; } } - bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized; - - if (!need_aoa || !sc_aoa_start(&s->aoa)) { +aoa_complete: + if (aoa_fail || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); From 785099b74d4883ee124a89f15d84e84221434032 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 31/67] Remove duplicate definition SC_HID_MAX_SIZE This constant is defined in hid_event.h. PR #5270 --- app/src/usb/aoa_hid.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 33a1f136..4d77ea3d 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -13,8 +13,6 @@ #include "util/tick.h" #include "util/vecdeque.h" -#define SC_HID_MAX_SIZE 8 - struct sc_aoa_event { struct sc_hid_event hid; uint16_t accessory_id; From 1afc8ca36889f668b0fd635d45f63a0113dbb093 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 32/67] Add missing SC_ prefix for HID mouse event size PR #5270 --- app/src/hid/hid_mouse.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 9d814448..1e831fb7 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -2,7 +2,7 @@ // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion -#define HID_MOUSE_EVENT_SIZE 4 +#define SC_HID_MOUSE_EVENT_SIZE 4 /** * Mouse descriptor from the specification: @@ -126,7 +126,7 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = static void sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - hid_event->size = HID_MOUSE_EVENT_SIZE; + hid_event->size = SC_HID_MOUSE_EVENT_SIZE; // Leave hid_event->data uninitialized, it will be fully initialized by // callers } From dad04bf1389ae5e4432b868c562713b127bbc2d3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 33/67] Fix HID mouse header guard PR #5270 --- app/src/hid/hid_mouse.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index e514d7d9..0ebf0ee1 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -1,8 +1,6 @@ #ifndef SC_HID_MOUSE_H #define SC_HID_MOUSE_H -#endif - #include "common.h" #include @@ -24,3 +22,5 @@ sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, void sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, const struct sc_mouse_scroll_event *event); + +#endif From 2dd02ebb80cc8d64f7a3da53ccbb9d78c2506914 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 34/67] Move HID ids to common HID code The HID ids (accessory ids or UHID ids) were defined by the keyboard and mouse implementations. Instead, define them in the common HID part, and make that id part of the sc_hid_event. This prepares the introduction of gamepad support, which will handle several gamepads (and ids) in the common HID gamepad code. PR #5270 --- app/src/hid/hid_event.h | 1 + app/src/hid/hid_keyboard.c | 1 + app/src/hid/hid_keyboard.h | 2 ++ app/src/hid/hid_mouse.c | 1 + app/src/hid/hid_mouse.h | 2 ++ app/src/uhid/keyboard_uhid.c | 8 +++----- app/src/uhid/mouse_uhid.c | 6 ++---- app/src/usb/aoa_hid.c | 16 +++++++--------- app/src/usb/aoa_hid.h | 7 ++----- app/src/usb/keyboard_aoa.c | 13 ++++--------- app/src/usb/mouse_aoa.c | 15 +++++---------- 11 files changed, 30 insertions(+), 42 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index e17f8569..80b65a87 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -8,6 +8,7 @@ #define SC_HID_MAX_SIZE 8 struct sc_hid_event { + uint16_t hid_id; uint8_t data[SC_HID_MAX_SIZE]; uint8_t size; }; diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index f3001df4..f828d014 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -200,6 +200,7 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = static void sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { + hid_event->hid_id = SC_HID_ID_KEYBOARD; hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; uint8_t *data = hid_event->data; diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index ddd2cc91..24d64b15 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -14,6 +14,8 @@ // 0x65 is Application, typically AT-101 Keyboard ends here. #define SC_HID_KEYBOARD_KEYS 0x66 +#define SC_HID_ID_KEYBOARD 1 + extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 1e831fb7..cc1862bc 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -126,6 +126,7 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = static void sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { + hid_event->hid_id = SC_HID_ID_MOUSE; hid_event->size = SC_HID_MOUSE_EVENT_SIZE; // Leave hid_event->data uninitialized, it will be fully initialized by // callers diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 0ebf0ee1..91337de5 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -8,6 +8,8 @@ #include "hid/hid_event.h" #include "input_events.h" +#define SC_HID_ID_MOUSE 2 + extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index d63d0ab0..7d5c6493 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -9,14 +9,12 @@ #define DOWNCAST_RECEIVER(UR) \ container_of(UR, struct sc_keyboard_uhid, uhid_receiver) -#define UHID_KEYBOARD_ID 1 - static void sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, const struct sc_hid_event *event) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_KEYBOARD_ID; + msg.uhid_input.id = event->hid_id; assert(event->size <= SC_HID_MAX_SIZE); memcpy(msg.uhid_input.data, event->data, event->size); @@ -143,13 +141,13 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, .process_output = sc_uhid_receiver_process_output, }; - kb->uhid_receiver.id = UHID_KEYBOARD_ID; + kb->uhid_receiver.id = SC_HID_ID_KEYBOARD; kb->uhid_receiver.ops = &uhid_receiver_ops; sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = UHID_KEYBOARD_ID; + msg.uhid_create.id = SC_HID_ID_KEYBOARD; msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 77446f9e..21dc018a 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -7,14 +7,12 @@ /** Downcast mouse processor to mouse_uhid */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor) -#define UHID_MOUSE_ID 2 - static void sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, const struct sc_hid_event *event, const char *name) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_MOUSE_ID; + msg.uhid_input.id = event->hid_id; assert(event->size <= SC_HID_MAX_SIZE); memcpy(msg.uhid_input.data, event->data, event->size); @@ -77,7 +75,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = UHID_MOUSE_ID; + msg.uhid_create.id = SC_HID_ID_MOUSE; msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 50bc33fe..260fbb75 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,6 +1,7 @@ #include "util/log.h" #include +#include #include #include "aoa_hid.h" @@ -18,14 +19,14 @@ #define SC_AOA_EVENT_QUEUE_MAX 64 static void -sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) { +sc_hid_event_log(const struct sc_hid_event *event) { // HID Event: [00] FF FF FF FF... assert(event->size); char *hex = sc_str_to_hex_string(event->data, event->size); if (!hex) { return; } - LOGV("HID Event: [%d] %s", accessory_id, hex); + LOGV("HID Event: [%" PRIu16 "] %s", event->hid_id, hex); free(hex); } @@ -146,14 +147,13 @@ sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, } static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, - const struct sc_hid_event *event) { +sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) - uint16_t value = accessory_id; + uint16_t value = event->hid_id; uint16_t index = 0; unsigned char *data = (uint8_t *) event->data; // discard const uint16_t length = event->size; @@ -194,11 +194,10 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - uint16_t accessory_id, const struct sc_hid_event *event, uint64_t ack_to_wait) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_event_log(accessory_id, event); + sc_hid_event_log(event); } sc_mutex_lock(&aoa->mutex); @@ -209,7 +208,6 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); aoa_event->hid = *event; - aoa_event->accessory_id = accessory_id; aoa_event->ack_to_wait = ack_to_wait; if (was_empty) { @@ -265,7 +263,7 @@ run_aoa_thread(void *data) { } } - bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid); + bool ok = sc_aoa_send_hid_event(aoa, &event.hid); if (!ok) { LOGW("Could not send HID event to USB device"); } diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 4d77ea3d..87f070ca 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -15,7 +15,6 @@ struct sc_aoa_event { struct sc_hid_event hid; - uint16_t accessory_id; uint64_t ack_to_wait; }; @@ -56,14 +55,12 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - uint16_t accessory_id, const struct sc_hid_event *event, uint64_t ack_to_wait); static inline bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, - const struct sc_hid_event *event) { - return sc_aoa_push_hid_event_with_ack_to_wait(aoa, accessory_id, event, +sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { + return sc_aoa_push_hid_event_with_ack_to_wait(aoa, event, SC_SEQUENCE_INVALID); } diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 736c97b0..f6bd6849 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -8,8 +8,6 @@ /** Downcast key processor to keyboard_aoa */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor) -#define HID_KEYBOARD_ACCESSORY_ID 1 - static bool push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { struct sc_hid_event hid_event; @@ -18,8 +16,7 @@ push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { return true; } - if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { LOGW("Could not request HID event (mod lock state)"); return false; } @@ -58,9 +55,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // synchronization is acknowledged by the server, otherwise it could // paste the old clipboard content. - if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, - HID_KEYBOARD_ACCESSORY_ID, - &hid_event, + if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, &hid_event, ack_to_wait)) { LOGW("Could not request HID event (key)"); } @@ -71,7 +66,7 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, + bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_KEYBOARD, SC_HID_KEYBOARD_REPORT_DESC, SC_HID_KEYBOARD_REPORT_DESC_LEN); if (!ok) { @@ -103,7 +98,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { // Unregister HID keyboard so the soft keyboard shows again on Android - bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); + bool ok = sc_aoa_unregister_hid(kb->aoa, SC_HID_ID_KEYBOARD); if (!ok) { LOGW("Could not unregister HID keyboard"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 93b32328..896578c0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -9,8 +9,6 @@ /** Downcast mouse processor to mouse_aoa */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor) -#define HID_MOUSE_ACCESSORY_ID 2 - static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { @@ -19,8 +17,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, struct sc_hid_event hid_event; sc_hid_mouse_event_from_motion(&hid_event, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { LOGW("Could not request HID event (mouse motion)"); } } @@ -33,8 +30,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, struct sc_hid_event hid_event; sc_hid_mouse_event_from_click(&hid_event, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { LOGW("Could not request HID event (mouse click)"); } } @@ -47,8 +43,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_hid_event hid_event; sc_hid_mouse_event_from_scroll(&hid_event, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { LOGW("Could not request HID event (mouse scroll)"); } } @@ -57,7 +52,7 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, + bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_MOUSE, SC_HID_MOUSE_REPORT_DESC, SC_HID_MOUSE_REPORT_DESC_LEN); if (!ok) { @@ -82,7 +77,7 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); + bool ok = sc_aoa_unregister_hid(mouse->aoa, SC_HID_ID_MOUSE); if (!ok) { LOGW("Could not unregister HID mouse"); } From 9af3bacdd67e2179aea7dc0f56ec487c015181e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 35/67] Refactor AOA handling Extract event processing to a separate function. This will make the code more readable when more event types will be added. PR #5270 --- app/src/usb/aoa_hid.c | 65 ++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 260fbb75..8eee4d3f 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -221,6 +221,41 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, return !full; } +static bool +sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { + uint64_t ack_to_wait = event->ack_to_wait; + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + + // If some events have ack_to_wait set, then sc_aoa must have been + // initialized with a non NULL acksync + assert(aoa->acksync); + + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + // continue to process events + return true; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + return false; + } + } + + bool ok = sc_aoa_send_hid_event(aoa, &event->hid); + if (!ok) { + LOGW("Could not send HID event to USB device"); + } + + // continue to process events + return true; +} + static int run_aoa_thread(void *data) { struct sc_aoa *aoa = data; @@ -238,34 +273,12 @@ run_aoa_thread(void *data) { assert(!sc_vecdeque_is_empty(&aoa->queue)); struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); - uint64_t ack_to_wait = event.ack_to_wait; sc_mutex_unlock(&aoa->mutex); - if (ack_to_wait != SC_SEQUENCE_INVALID) { - LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - - // If some events have ack_to_wait set, then sc_aoa must have been - // initialized with a non NULL acksync - assert(aoa->acksync); - - // Do not block the loop indefinitely if the ack never comes (it - // should never happen) - sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); - enum sc_acksync_wait_result result = - sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); - - if (result == SC_ACKSYNC_WAIT_TIMEOUT) { - LOGW("Ack not received after 500ms, discarding HID event"); - continue; - } else if (result == SC_ACKSYNC_WAIT_INTR) { - // stopped - break; - } - } - - bool ok = sc_aoa_send_hid_event(aoa, &event.hid); - if (!ok) { - LOGW("Could not send HID event to USB device"); + bool cont = sc_aoa_process_event(aoa, &event); + if (!cont) { + // stopped + break; } } return 0; From 3e9c89c535e0304401da877502de6a892d1bfa10 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 36/67] Reorder AOA functions This will allow sc_aoa_setup_hid() to compile even when sc_aoa_unregister_hid() will be made static. PR #5270 --- app/src/usb/aoa_hid.c | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 8eee4d3f..f4ff8b8d 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -126,26 +126,6 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, return true; } -bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size) { - bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); - if (!ok) { - return false; - } - - ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, - report_desc_size); - if (!ok) { - if (!sc_aoa_unregister_hid(aoa, accessory_id)) { - LOGW("Could not unregister HID"); - } - return false; - } - - return true; -} - static bool sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; @@ -192,6 +172,26 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { return true; } +bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size) { + bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); + if (!ok) { + return false; + } + + ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, + report_desc_size); + if (!ok) { + if (!sc_aoa_unregister_hid(aoa, accessory_id)) { + LOGW("Could not unregister HID"); + } + return false; + } + + return true; +} + bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, const struct sc_hid_event *event, From 6e9b0d7d4c4d386c84e8a3a40e442a45d1666e24 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 37/67] Make AOA open and close asynchronous For AOA keyboard and mouse, only input reports were asynchronous. Register/unregister were called from the main thread. This had the benefit to fail immediately if the AOA registration failed, but we want to open/close AOA devices dynamically in order to add gamepad support. PR #5270 --- app/src/usb/aoa_hid.c | 156 +++++++++++++++++++++++++++++-------- app/src/usb/aoa_hid.h | 38 +++++++-- app/src/usb/keyboard_aoa.c | 9 +-- app/src/usb/mouse_aoa.c | 8 +- 4 files changed, 165 insertions(+), 46 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index f4ff8b8d..ff2516e5 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -16,7 +16,8 @@ #define DEFAULT_TIMEOUT 1000 -#define SC_AOA_EVENT_QUEUE_MAX 64 +// Drop droppable events above this limit +#define SC_AOA_EVENT_QUEUE_LIMIT 60 static void sc_hid_event_log(const struct sc_hid_event *event) { @@ -35,7 +36,8 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { sc_vecdeque_init(&aoa->queue); - if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) { + // Add 4 to support 4 non-droppable events without re-allocation + if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_LIMIT + 4)) { return false; } @@ -149,7 +151,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { return true; } -bool +static bool sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_UNREGISTER_HID; @@ -172,7 +174,7 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { return true; } -bool +static bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, const uint8_t *report_desc, uint16_t report_desc_size) { bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); @@ -201,55 +203,145 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, } sc_mutex_lock(&aoa->mutex); - bool full = sc_vecdeque_is_full(&aoa->queue); - if (!full) { + + bool pushed = false; + + size_t size = sc_vecdeque_size(&aoa->queue); + if (size < SC_AOA_EVENT_QUEUE_LIMIT) { bool was_empty = sc_vecdeque_is_empty(&aoa->queue); struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); - aoa_event->hid = *event; - aoa_event->ack_to_wait = ack_to_wait; + aoa_event->type = SC_AOA_EVENT_TYPE_INPUT; + aoa_event->input.hid = *event; + aoa_event->input.ack_to_wait = ack_to_wait; + pushed = true; if (was_empty) { sc_cond_signal(&aoa->event_cond); } } - // Otherwise (if the queue is full), the event is discarded + // Otherwise, the event is discarded sc_mutex_unlock(&aoa->mutex); - return !full; + return pushed; +} + +bool +sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size) { + // TODO log verbose + + sc_mutex_lock(&aoa->mutex); + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + // an OPEN event is non-droppable, so push it to the queue even above the + // SC_AOA_EVENT_QUEUE_LIMIT + struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); + if (!aoa_event) { + LOG_OOM(); + sc_mutex_unlock(&aoa->mutex); + return false; + } + + aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; + aoa_event->open.hid_id = accessory_id; + aoa_event->open.report_desc = report_desc; + aoa_event->open.report_desc_size = report_desc_size; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + + sc_mutex_unlock(&aoa->mutex); + + return true; +} + +bool +sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id) { + // TODO log verbose + + sc_mutex_lock(&aoa->mutex); + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + // a CLOSE event is non-droppable, so push it to the queue even above the + // SC_AOA_EVENT_QUEUE_LIMIT + struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); + if (!aoa_event) { + LOG_OOM(); + sc_mutex_unlock(&aoa->mutex); + return false; + } + + aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE; + aoa_event->close.hid_id = accessory_id; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + + sc_mutex_unlock(&aoa->mutex); + + return true; } static bool sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { - uint64_t ack_to_wait = event->ack_to_wait; - if (ack_to_wait != SC_SEQUENCE_INVALID) { - LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + switch (event->type) { + case SC_AOA_EVENT_TYPE_INPUT: { + uint64_t ack_to_wait = event->input.ack_to_wait; + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - // If some events have ack_to_wait set, then sc_aoa must have been - // initialized with a non NULL acksync - assert(aoa->acksync); + // If some events have ack_to_wait set, then sc_aoa must have + // been initialized with a non NULL acksync + assert(aoa->acksync); - // Do not block the loop indefinitely if the ack never comes (it - // should never happen) - sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); - enum sc_acksync_wait_result result = - sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); - if (result == SC_ACKSYNC_WAIT_TIMEOUT) { - LOGW("Ack not received after 500ms, discarding HID event"); - // continue to process events - return true; - } else if (result == SC_ACKSYNC_WAIT_INTR) { - // stopped - return false; + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + // continue to process events + return true; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + return false; + } + } + + bool ok = sc_aoa_send_hid_event(aoa, &event->input.hid); + if (!ok) { + LOGW("Could not send HID event to USB device: %" PRIu16, + event->input.hid.hid_id); + } + + break; } - } + case SC_AOA_EVENT_TYPE_OPEN: { + bool ok = sc_aoa_setup_hid(aoa, event->open.hid_id, + event->open.report_desc, + event->open.report_desc_size); + if (!ok) { + LOGW("Could not open AOA device: %" PRIu16, event->open.hid_id); + } - bool ok = sc_aoa_send_hid_event(aoa, &event->hid); - if (!ok) { - LOGW("Could not send HID event to USB device"); + break; + } + case SC_AOA_EVENT_TYPE_CLOSE: { + bool ok = sc_aoa_unregister_hid(aoa, event->close.hid_id); + if (!ok) { + LOGW("Could not close AOA device: %" PRIu16, + event->close.hid_id); + } + + break; + } } // continue to process events diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 87f070ca..b2dc04ac 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -13,9 +13,28 @@ #include "util/tick.h" #include "util/vecdeque.h" +enum sc_aoa_event_type { + SC_AOA_EVENT_TYPE_OPEN, + SC_AOA_EVENT_TYPE_INPUT, + SC_AOA_EVENT_TYPE_CLOSE, +}; + struct sc_aoa_event { - struct sc_hid_event hid; - uint64_t ack_to_wait; + enum sc_aoa_event_type type; + union { + struct { + uint16_t hid_id; + const uint8_t *report_desc; // pointer to static memory + uint16_t report_desc_size; + } open; + struct { + uint16_t hid_id; + } close; + struct { + struct sc_hid_event hid; + uint64_t ack_to_wait; + } input; + }; }; struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event); @@ -46,12 +65,21 @@ sc_aoa_stop(struct sc_aoa *aoa); void sc_aoa_join(struct sc_aoa *aoa); +//bool +//sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, +// const uint8_t *report_desc, uint16_t report_desc_size); +// +//bool +//sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); + +// report_desc must be a pointer to static memory, accessed at any time from +// another thread bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size); +sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size); bool -sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); +sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id); bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index f6bd6849..0052c3d8 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -66,11 +66,11 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_KEYBOARD, + bool ok = sc_aoa_push_open(aoa, SC_HID_ID_KEYBOARD, SC_HID_KEYBOARD_REPORT_DESC, SC_HID_KEYBOARD_REPORT_DESC_LEN); if (!ok) { - LOGW("Register HID keyboard failed"); + LOGW("Could not push AOA keyboard open request"); return false; } @@ -97,9 +97,8 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - // Unregister HID keyboard so the soft keyboard shows again on Android - bool ok = sc_aoa_unregister_hid(kb->aoa, SC_HID_ID_KEYBOARD); + bool ok = sc_aoa_push_close(kb->aoa, SC_HID_ID_KEYBOARD); if (!ok) { - LOGW("Could not unregister HID keyboard"); + LOGW("Could not push AOA keyboard close request"); } } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 896578c0..84fd8d64 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -52,11 +52,11 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_MOUSE, + bool ok = sc_aoa_push_open(aoa, SC_HID_ID_MOUSE, SC_HID_MOUSE_REPORT_DESC, SC_HID_MOUSE_REPORT_DESC_LEN); if (!ok) { - LOGW("Register HID mouse failed"); + LOGW("Could not push AOA mouse open request"); return false; } @@ -77,8 +77,8 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_unregister_hid(mouse->aoa, SC_HID_ID_MOUSE); + bool ok = sc_aoa_push_close(mouse->aoa, SC_HID_ID_MOUSE); if (!ok) { - LOGW("Could not unregister HID mouse"); + LOGW("Could not push AOA mouse close request"); } } From f6219d26409a1ba25b4df86aa3468aa18b6a11e5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 38/67] Rename hid_event to hid_input The sc_hid_event structure represents HID input data. Rename it so that we can add other hid event structs without confusion. PR #5270 --- app/src/hid/hid_event.h | 2 +- app/src/hid/hid_keyboard.c | 34 ++++++++++++++++----------------- app/src/hid/hid_keyboard.h | 10 +++++----- app/src/hid/hid_mouse.c | 37 ++++++++++++++++++------------------ app/src/hid/hid_mouse.h | 12 ++++++------ app/src/uhid/keyboard_uhid.c | 22 ++++++++++----------- app/src/uhid/mouse_uhid.c | 29 ++++++++++++++-------------- app/src/usb/aoa_hid.c | 29 ++++++++++++++-------------- app/src/usb/aoa_hid.h | 14 +++++++------- app/src/usb/keyboard_aoa.c | 18 +++++++++--------- app/src/usb/mouse_aoa.c | 24 +++++++++++------------ 11 files changed, 116 insertions(+), 115 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 80b65a87..9f9432e6 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -7,7 +7,7 @@ #define SC_HID_MAX_SIZE 8 -struct sc_hid_event { +struct sc_hid_input { uint16_t hid_id; uint8_t data[SC_HID_MAX_SIZE]; uint8_t size; diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index f828d014..9ab444f6 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -21,7 +21,7 @@ // keyboard support, though OS could support more keys via modifying the report // desc. 6 should be enough for scrcpy. #define SC_HID_KEYBOARD_MAX_KEYS 6 -#define SC_HID_KEYBOARD_EVENT_SIZE \ +#define SC_HID_KEYBOARD_INPUT_SIZE \ (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS) #define SC_HID_RESERVED 0x00 @@ -125,7 +125,7 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = sizeof(SC_HID_KEYBOARD_REPORT_DESC); /** - * A keyboard HID event is 8 bytes long: + * A keyboard HID input report is 8 bytes long: * * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) * - byte 1: reserved (always 0) @@ -199,11 +199,11 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = */ static void -sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - hid_event->hid_id = SC_HID_ID_KEYBOARD; - hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; +sc_hid_keyboard_input_init(struct sc_hid_input *hid_input) { + hid_input->hid_id = SC_HID_ID_KEYBOARD; + hid_input->size = SC_HID_KEYBOARD_INPUT_SIZE; - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE; data[1] = SC_HID_RESERVED; @@ -251,9 +251,9 @@ scancode_is_modifier(enum sc_scancode scancode) { } bool -sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_event *hid_event, - const struct sc_key_event *event) { +sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_input *hid_input, + const struct sc_key_event *event) { enum sc_scancode scancode = event->scancode; assert(scancode >= 0); @@ -265,7 +265,7 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, return false; } - sc_hid_keyboard_event_init(hid_event); + sc_hid_keyboard_input_init(hid_input); uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state); @@ -276,9 +276,9 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, hid->keys[scancode] ? "true" : "false"); } - hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; + hid_input->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; - uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS]; + uint8_t *keys_data = &hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS]; // Re-calculate pressed keys every time int keys_pressed_count = 0; for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { @@ -309,8 +309,8 @@ end: } bool -sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, - uint16_t mods_state) { +sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, + uint16_t mods_state) { bool capslock = mods_state & SC_MOD_CAPS; bool numlock = mods_state & SC_MOD_NUM; if (!capslock && !numlock) { @@ -318,15 +318,15 @@ sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, return false; } - sc_hid_keyboard_event_init(event); + sc_hid_keyboard_input_init(hid_input); unsigned i = 0; if (capslock) { - event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; ++i; } if (numlock) { - event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; ++i; } diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index 24d64b15..01495cc2 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -39,12 +39,12 @@ void sc_hid_keyboard_init(struct sc_hid_keyboard *hid); bool -sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_event *hid_event, - const struct sc_key_event *event); +sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_input *hid_input, + const struct sc_key_event *event); bool -sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, - uint16_t mods_state); +sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, + uint16_t mods_state); #endif diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index cc1862bc..e26c248b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -2,7 +2,7 @@ // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion -#define SC_HID_MOUSE_EVENT_SIZE 4 +#define SC_HID_MOUSE_INPUT_SIZE 4 /** * Mouse descriptor from the specification: @@ -84,7 +84,7 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = sizeof(SC_HID_MOUSE_REPORT_DESC); /** - * A mouse HID event is 4 bytes long: + * A mouse HID input report is 4 bytes long: * * - byte 0: buttons state * - byte 1: relative x motion (signed byte from -127 to 127) @@ -125,11 +125,10 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = */ static void -sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - hid_event->hid_id = SC_HID_ID_MOUSE; - hid_event->size = SC_HID_MOUSE_EVENT_SIZE; - // Leave hid_event->data uninitialized, it will be fully initialized by - // callers +sc_hid_mouse_input_init(struct sc_hid_input *hid_input) { + hid_input->hid_id = SC_HID_ID_MOUSE; + hid_input->size = SC_HID_MOUSE_INPUT_SIZE; + // Leave ->data uninitialized, it will be fully initialized by callers } static uint8_t @@ -154,11 +153,11 @@ sc_hid_buttons_from_buttons_state(uint8_t buttons_state) { } void -sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, - const struct sc_mouse_motion_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, + const struct sc_mouse_motion_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); @@ -166,11 +165,11 @@ sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, } void -sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, - const struct sc_mouse_click_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, + const struct sc_mouse_click_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion @@ -178,11 +177,11 @@ sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, } void -sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, - const struct sc_mouse_scroll_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, + const struct sc_mouse_scroll_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 91337de5..3b647fb3 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -14,15 +14,15 @@ extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; void -sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, - const struct sc_mouse_motion_event *event); +sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, + const struct sc_mouse_motion_event *event); void -sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, - const struct sc_mouse_click_event *event); +sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, + const struct sc_mouse_click_event *event); void -sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, - const struct sc_mouse_scroll_event *event); +sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, + const struct sc_mouse_scroll_event *event); #endif diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 7d5c6493..c91f9539 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -11,14 +11,14 @@ static void sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, - const struct sc_hid_event *event) { + const struct sc_hid_input *hid_input) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = event->hid_id; + msg.uhid_input.id = hid_input->hid_id; - assert(event->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, event->data, event->size); - msg.uhid_input.size = event->size; + 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(kb->controller, &msg)) { LOGE("Could not send UHID_INPUT message (key)"); @@ -37,14 +37,14 @@ sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { // or HID output anyway kb->device_mod = mod; - struct sc_hid_event hid_event; - if (!sc_hid_keyboard_event_from_mods(&hid_event, diff)) { + struct sc_hid_input hid_input; + if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, diff)) { return; } LOGV("HID keyboard state synchronized"); - sc_keyboard_uhid_send_input(kb, &hid_event); + sc_keyboard_uhid_send_input(kb, &hid_input); } } @@ -64,10 +64,10 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_keyboard_uhid *kb = DOWNCAST(kp); - struct sc_hid_event hid_event; + struct sc_hid_input hid_input; // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { if (event->scancode == SC_SCANCODE_CAPSLOCK) { kb->device_mod ^= SC_MOD_CAPS; } else if (event->scancode == SC_SCANCODE_NUMLOCK) { @@ -77,7 +77,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // change the modifiers) sc_keyboard_uhid_synchronize_mod(kb); } - sc_keyboard_uhid_send_input(kb, &hid_event); + sc_keyboard_uhid_send_input(kb, &hid_input); } } diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 21dc018a..e1daa9e5 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -9,14 +9,15 @@ static void sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, - const struct sc_hid_event *event, const char *name) { + 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 = event->hid_id; + msg.uhid_input.id = hid_input->hid_id; - assert(event->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, event->data, event->size); - msg.uhid_input.size = event->size; + 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(mouse->controller, &msg)) { LOGE("Could not send UHID_INPUT message (%s)", name); @@ -28,10 +29,10 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_motion(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_motion(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse motion"); } static void @@ -39,10 +40,10 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_click(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_click(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse click"); } static void @@ -50,10 +51,10 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_scroll(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } bool diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index ff2516e5..f9bcf8e5 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -20,14 +20,14 @@ #define SC_AOA_EVENT_QUEUE_LIMIT 60 static void -sc_hid_event_log(const struct sc_hid_event *event) { - // HID Event: [00] FF FF FF FF... - assert(event->size); - char *hex = sc_str_to_hex_string(event->data, event->size); +sc_hid_input_log(const struct sc_hid_input *hid_input) { + // HID input: [00] FF FF FF FF... + assert(hid_input->size); + char *hex = sc_str_to_hex_string(hid_input->data, hid_input->size); if (!hex) { return; } - LOGV("HID Event: [%" PRIu16 "] %s", event->hid_id, hex); + LOGV("HID input: [%" PRIu16 "] %s", hid_input->hid_id, hex); free(hex); } @@ -129,16 +129,17 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, } static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { +sc_aoa_send_hid_event(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) - uint16_t value = event->hid_id; + uint16_t value = hid_input->hid_id; uint16_t index = 0; - unsigned char *data = (uint8_t *) event->data; // discard const - uint16_t length = event->size; + unsigned char *data = (uint8_t *) hid_input->data; // discard const + uint16_t length = hid_input->size; int result = libusb_control_transfer(aoa->usb->handle, request_type, request, value, index, data, length, DEFAULT_TIMEOUT); @@ -195,11 +196,11 @@ sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, } bool -sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - const struct sc_hid_event *event, - uint64_t ack_to_wait) { +sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input, + uint64_t ack_to_wait) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_event_log(event); + sc_hid_input_log(hid_input); } sc_mutex_lock(&aoa->mutex); @@ -213,7 +214,7 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); aoa_event->type = SC_AOA_EVENT_TYPE_INPUT; - aoa_event->input.hid = *event; + aoa_event->input.hid = *hid_input; aoa_event->input.ack_to_wait = ack_to_wait; pushed = true; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index b2dc04ac..63a9f4de 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -31,7 +31,7 @@ struct sc_aoa_event { uint16_t hid_id; } close; struct { - struct sc_hid_event hid; + struct sc_hid_input hid; uint64_t ack_to_wait; } input; }; @@ -82,14 +82,14 @@ bool sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id); bool -sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - const struct sc_hid_event *event, - uint64_t ack_to_wait); +sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input, + uint64_t ack_to_wait); static inline bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { - return sc_aoa_push_hid_event_with_ack_to_wait(aoa, event, - SC_SEQUENCE_INVALID); +sc_aoa_push_input(struct sc_aoa *aoa, const struct sc_hid_input *hid_input) { + return sc_aoa_push_input_with_ack_to_wait(aoa, hid_input, + SC_SEQUENCE_INVALID); } #endif diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 0052c3d8..33924dbf 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -10,14 +10,14 @@ static bool push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { - struct sc_hid_event hid_event; - if (!sc_hid_keyboard_event_from_mods(&hid_event, mods_state)) { + struct sc_hid_input hid_input; + if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, mods_state)) { // Nothing to do return true; } - if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { - LOGW("Could not request HID event (mod lock state)"); + if (!sc_aoa_push_input(kb->aoa, &hid_input)) { + LOGW("Could not push HID input (mod lock state)"); return false; } @@ -38,10 +38,10 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_keyboard_aoa *kb = DOWNCAST(kp); - struct sc_hid_event hid_event; + struct sc_hid_input hid_input; // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { if (!kb->mod_lock_synchronized) { // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize // keyboard state @@ -55,9 +55,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // synchronization is acknowledged by the server, otherwise it could // paste the old clipboard content. - if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, &hid_event, - ack_to_wait)) { - LOGW("Could not request HID event (key)"); + if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input, + ack_to_wait)) { + LOGW("Could not push HID input (key)"); } } } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 84fd8d64..03d28610 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -14,11 +14,11 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_motion(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_motion(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - LOGW("Could not request HID event (mouse motion)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push HID input (mouse motion)"); } } @@ -27,11 +27,11 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_click(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_click(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - LOGW("Could not request HID event (mouse click)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push HID input (mouse click)"); } } @@ -40,11 +40,11 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_scroll(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - LOGW("Could not request HID event (mouse scroll)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push HID input (mouse scroll)"); } } From 6f0c9eba9bb030cc8e4e6bc4c14ae2c9dd730348 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 39/67] Introduce hid_open and hid_close events This allows to handle HID open/close reports at the same place as HID input reports (in the HID layer). This will be especially useful to manage HID gamepads, to avoid implementing one part in the HID layer and another part in the gamepad processor implementation. PR #5270 --- app/src/hid/hid_event.h | 10 ++++++++++ app/src/hid/hid_keyboard.c | 15 +++++++++++---- app/src/hid/hid_keyboard.h | 9 ++++++--- app/src/hid/hid_mouse.c | 15 +++++++++++---- app/src/hid/hid_mouse.h | 7 +++++-- app/src/uhid/keyboard_uhid.c | 8 ++++++-- app/src/uhid/mouse_uhid.c | 8 ++++++-- app/src/usb/aoa_hid.c | 31 +++++++++++++++---------------- app/src/usb/aoa_hid.h | 11 ++++------- app/src/usb/keyboard_aoa.c | 12 ++++++++---- app/src/usb/mouse_aoa.c | 12 ++++++++---- 11 files changed, 90 insertions(+), 48 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 9f9432e6..9171004e 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -13,4 +13,14 @@ struct sc_hid_input { uint8_t size; }; +struct sc_hid_open { + uint16_t hid_id; + const uint8_t *report_desc; // pointer to static memory + size_t report_desc_size; +}; + +struct sc_hid_close { + uint16_t hid_id; +}; + #endif diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 9ab444f6..64dffe80 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -47,7 +47,7 @@ * * (change vid:pid' to your device's vendor ID and product ID). */ -const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { +static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Keyboard) @@ -121,9 +121,6 @@ const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { 0xC0 }; -const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = - sizeof(SC_HID_KEYBOARD_REPORT_DESC); - /** * A keyboard HID input report is 8 bytes long: * @@ -332,3 +329,13 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, return true; } + +void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { + hid_open->hid_id = SC_HID_ID_KEYBOARD; + hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); +} + +void sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close) { + hid_close->hid_id = SC_HID_ID_KEYBOARD; +} diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index 01495cc2..cde1ac52 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -16,9 +16,6 @@ #define SC_HID_ID_KEYBOARD 1 -extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; -extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; - /** * HID keyboard events are sequence-based, every time keyboard state changes * it sends an array of currently pressed keys, the host is responsible for @@ -38,6 +35,12 @@ struct sc_hid_keyboard { void sc_hid_keyboard_init(struct sc_hid_keyboard *hid); +void +sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open); + +void +sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close); + bool sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, struct sc_hid_input *hid_input, diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index e26c248b..d1aae83a 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -14,7 +14,7 @@ * * §4 Generic Desktop Page (0x01) (p26) */ -const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { +static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Mouse) @@ -80,9 +80,6 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { 0xC0, }; -const size_t SC_HID_MOUSE_REPORT_DESC_LEN = - sizeof(SC_HID_MOUSE_REPORT_DESC); - /** * A mouse HID input report is 4 bytes long: * @@ -190,3 +187,13 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, data[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored } + +void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { + hid_open->hid_id = SC_HID_ID_MOUSE; + hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); +} + +void sc_hid_mouse_generate_close(struct sc_hid_close *hid_close) { + hid_close->hid_id = SC_HID_ID_MOUSE; +} diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 3b647fb3..a9a54718 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -10,8 +10,11 @@ #define SC_HID_ID_MOUSE 2 -extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; -extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; +void +sc_hid_mouse_generate_open(struct sc_hid_open *hid_open); + +void +sc_hid_mouse_generate_close(struct sc_hid_close *hid_close); void sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index c91f9539..e7a0e33a 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -145,11 +145,15 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, kb->uhid_receiver.ops = &uhid_receiver_ops; sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); + struct sc_hid_open hid_open; + sc_hid_keyboard_generate_open(&hid_open); + assert(hid_open.hid_id == SC_HID_ID_KEYBOARD); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_KEYBOARD; - msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; - msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; + 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(controller, &msg)) { LOGE("Could not send UHID_CREATE message (keyboard)"); return false; diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index e1daa9e5..c379f3ad 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -74,11 +74,15 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, mouse->mouse_processor.relative_mode = true; + struct sc_hid_open hid_open; + sc_hid_mouse_generate_open(&hid_open); + assert(hid_open.hid_id == SC_HID_ID_MOUSE); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_MOUSE; - msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; - msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; + 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(controller, &msg)) { LOGE("Could not send UHID_CREATE message (mouse)"); return false; diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index f9bcf8e5..c44c80f6 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -230,8 +230,7 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, } bool -sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size) { +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { // TODO log verbose sc_mutex_lock(&aoa->mutex); @@ -247,9 +246,7 @@ sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, } aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; - aoa_event->open.hid_id = accessory_id; - aoa_event->open.report_desc = report_desc; - aoa_event->open.report_desc_size = report_desc_size; + aoa_event->open.hid = *hid_open; if (was_empty) { sc_cond_signal(&aoa->event_cond); @@ -261,7 +258,7 @@ sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, } bool -sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id) { +sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { // TODO log verbose sc_mutex_lock(&aoa->mutex); @@ -277,7 +274,7 @@ sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id) { } aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE; - aoa_event->close.hid_id = accessory_id; + aoa_event->close.hid = *hid_close; if (was_empty) { sc_cond_signal(&aoa->event_cond); @@ -316,29 +313,31 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { } } - bool ok = sc_aoa_send_hid_event(aoa, &event->input.hid); + struct sc_hid_input *hid_input = &event->input.hid; + bool ok = sc_aoa_send_hid_event(aoa, hid_input); if (!ok) { LOGW("Could not send HID event to USB device: %" PRIu16, - event->input.hid.hid_id); + hid_input->hid_id); } break; } case SC_AOA_EVENT_TYPE_OPEN: { - bool ok = sc_aoa_setup_hid(aoa, event->open.hid_id, - event->open.report_desc, - event->open.report_desc_size); + struct sc_hid_open *hid_open = &event->open.hid; + bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id, + hid_open->report_desc, + hid_open->report_desc_size); if (!ok) { - LOGW("Could not open AOA device: %" PRIu16, event->open.hid_id); + LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); } break; } case SC_AOA_EVENT_TYPE_CLOSE: { - bool ok = sc_aoa_unregister_hid(aoa, event->close.hid_id); + struct sc_hid_close *hid_close = &event->close.hid; + bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id); if (!ok) { - LOGW("Could not close AOA device: %" PRIu16, - event->close.hid_id); + LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id); } break; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 63a9f4de..010b3742 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -23,12 +23,10 @@ struct sc_aoa_event { enum sc_aoa_event_type type; union { struct { - uint16_t hid_id; - const uint8_t *report_desc; // pointer to static memory - uint16_t report_desc_size; + struct sc_hid_open hid; } open; struct { - uint16_t hid_id; + struct sc_hid_close hid; } close; struct { struct sc_hid_input hid; @@ -75,11 +73,10 @@ sc_aoa_join(struct sc_aoa *aoa); // report_desc must be a pointer to static memory, accessed at any time from // another thread bool -sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size); +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open); bool -sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id); +sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close); bool sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 33924dbf..6c4aaed7 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -66,9 +66,10 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_push_open(aoa, SC_HID_ID_KEYBOARD, - SC_HID_KEYBOARD_REPORT_DESC, - SC_HID_KEYBOARD_REPORT_DESC_LEN); + struct sc_hid_open hid_open; + sc_hid_keyboard_generate_open(&hid_open); + + bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { LOGW("Could not push AOA keyboard open request"); return false; @@ -97,7 +98,10 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - bool ok = sc_aoa_push_close(kb->aoa, SC_HID_ID_KEYBOARD); + struct sc_hid_close hid_close; + sc_hid_keyboard_generate_close(&hid_close); + + bool ok = sc_aoa_push_close(kb->aoa, &hid_close); if (!ok) { LOGW("Could not push AOA keyboard close request"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 03d28610..3c4e3693 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -52,9 +52,10 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_push_open(aoa, SC_HID_ID_MOUSE, - SC_HID_MOUSE_REPORT_DESC, - SC_HID_MOUSE_REPORT_DESC_LEN); + struct sc_hid_open hid_open; + sc_hid_mouse_generate_open(&hid_open); + + bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { LOGW("Could not push AOA mouse open request"); return false; @@ -77,7 +78,10 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_push_close(mouse->aoa, SC_HID_ID_MOUSE); + struct sc_hid_close hid_close; + sc_hid_mouse_generate_close(&hid_close); + + bool ok = sc_aoa_push_close(mouse->aoa, &hid_close); if (!ok) { LOGW("Could not push AOA mouse close request"); } From d748ac75e651ff446df74896a39deb6bd427fe67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 40/67] Add AOA open/close verbose logs PR #5270 --- app/src/usb/aoa_hid.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index c44c80f6..ef10e460 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -31,6 +31,25 @@ sc_hid_input_log(const struct sc_hid_input *hid_input) { free(hex); } +static void +sc_hid_open_log(const struct sc_hid_open *hid_open) { + // HID open: [00] FF FF FF FF... + assert(hid_open->report_desc_size); + char *hex = sc_str_to_hex_string(hid_open->report_desc, + hid_open->report_desc_size); + if (!hex) { + return; + } + LOGV("HID open: [%" PRIu16 "] %s", hid_open->hid_id, hex); + free(hex); +} + +static void +sc_hid_close_log(const struct sc_hid_close *hid_close) { + // HID close: [00] + LOGV("HID close: [%" PRIu16 "]", hid_close->hid_id); +} + bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { @@ -231,7 +250,9 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, bool sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { - // TODO log verbose + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_open_log(hid_open); + } sc_mutex_lock(&aoa->mutex); bool was_empty = sc_vecdeque_is_empty(&aoa->queue); @@ -259,7 +280,9 @@ sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { bool sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { - // TODO log verbose + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_close_log(hid_close); + } sc_mutex_lock(&aoa->mutex); bool was_empty = sc_vecdeque_is_empty(&aoa->queue); From 6c707ad8a359a5acf1c59fd065d50b5e1550c781 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 41/67] Make HID logs uniform PR #5270 --- app/src/uhid/keyboard_uhid.c | 2 +- app/src/uhid/mouse_uhid.c | 4 ++-- app/src/usb/keyboard_aoa.c | 8 ++++---- app/src/usb/mouse_aoa.c | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index e7a0e33a..11d41e40 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -21,7 +21,7 @@ sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(kb->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (key)"); + LOGE("Could not push UHID_INPUT message (key)"); } } diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index c379f3ad..9544ab0d 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -20,7 +20,7 @@ sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(mouse->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (%s)", name); + LOGE("Could not push UHID_INPUT message (%s)", name); } } @@ -84,7 +84,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, 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(controller, &msg)) { - LOGE("Could not send UHID_CREATE message (mouse)"); + LOGE("Could not push UHID_CREATE message (mouse)"); return false; } diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 6c4aaed7..738f6875 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -17,7 +17,7 @@ push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { } if (!sc_aoa_push_input(kb->aoa, &hid_input)) { - LOGW("Could not push HID input (mod lock state)"); + LOGW("Could not push AOA HID input (mod lock state)"); return false; } @@ -57,7 +57,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input, ack_to_wait)) { - LOGW("Could not push HID input (key)"); + LOGW("Could not push AOA HID input (key)"); } } } @@ -71,7 +71,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { - LOGW("Could not push AOA keyboard open request"); + LOGW("Could not push AOA HID open (keyboard)"); return false; } @@ -103,6 +103,6 @@ sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { bool ok = sc_aoa_push_close(kb->aoa, &hid_close); if (!ok) { - LOGW("Could not push AOA keyboard close request"); + LOGW("Could not push AOA HID close (keyboard)"); } } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 3c4e3693..b4eb4eb8 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -18,7 +18,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, sc_hid_mouse_generate_input_from_motion(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push HID input (mouse motion)"); + LOGW("Could not push AOA HID input (mouse motion)"); } } @@ -31,7 +31,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, sc_hid_mouse_generate_input_from_click(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push HID input (mouse click)"); + LOGW("Could not push AOA HID input (mouse click)"); } } @@ -44,7 +44,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, sc_hid_mouse_generate_input_from_scroll(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push HID input (mouse scroll)"); + LOGW("Could not push AOA HID input (mouse scroll)"); } } @@ -57,7 +57,7 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { - LOGW("Could not push AOA mouse open request"); + LOGW("Could not push AOA HID open (mouse)"); return false; } @@ -83,6 +83,6 @@ sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { bool ok = sc_aoa_push_close(mouse->aoa, &hid_close); if (!ok) { - LOGW("Could not push AOA mouse close request"); + LOGW("Could not push AOA HID close (mouse)"); } } From 222916eebed01098f3311a1096f4031be2d61197 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 42/67] Unregister all AOA devices automatically on exit Pushing a close event from the keyboard_aoa or mouse_aoa implementation was racy, because the AOA thread might be stopped before these events were processed. Instead, keep the list of open AOA devices to close them automatically from the AOA thread before exiting. PR #5270 --- app/src/usb/aoa_hid.c | 43 ++++++++++++++++++++++++++++++++++---- app/src/usb/keyboard_aoa.c | 9 ++------ app/src/usb/mouse_aoa.c | 9 ++------ 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index ef10e460..59c8304b 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -7,6 +7,7 @@ #include "aoa_hid.h" #include "util/log.h" #include "util/str.h" +#include "util/vector.h" // See . #define ACCESSORY_REGISTER_HID 54 @@ -19,6 +20,8 @@ // Drop droppable events above this limit #define SC_AOA_EVENT_QUEUE_LIMIT 60 +struct sc_vec_hid_ids SC_VECTOR(uint16_t); + static void sc_hid_input_log(const struct sc_hid_input *hid_input) { // HID input: [00] FF FF FF FF... @@ -309,7 +312,8 @@ sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { } static bool -sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { +sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event, + struct sc_vec_hid_ids *vec_open) { switch (event->type) { case SC_AOA_EVENT_TYPE_INPUT: { uint64_t ack_to_wait = event->input.ack_to_wait; @@ -350,7 +354,16 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id, hid_open->report_desc, hid_open->report_desc_size); - if (!ok) { + if (ok) { + // The device is now open, add it to the list of devices to + // close automatically on exit + bool pushed = sc_vector_push(vec_open, hid_open->hid_id); + if (!pushed) { + LOG_OOM(); + // this is not fatal, the HID device will just not be + // explicitly unregistered + } + } else { LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); } @@ -359,7 +372,14 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { case SC_AOA_EVENT_TYPE_CLOSE: { struct sc_hid_close *hid_close = &event->close.hid; bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id); - if (!ok) { + if (ok) { + // The device is not open anymore, remove it from the list of + // devices to close automatically on exit + ssize_t idx = sc_vector_index_of(vec_open, hid_close->hid_id); + if (idx >= 0) { + sc_vector_remove(vec_open, idx); + } + } else { LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id); } @@ -375,6 +395,9 @@ static int run_aoa_thread(void *data) { struct sc_aoa *aoa = data; + // Store the HID ids of opened devices to unregister them all before exiting + struct sc_vec_hid_ids vec_open = SC_VECTOR_INITIALIZER; + for (;;) { sc_mutex_lock(&aoa->mutex); while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) { @@ -390,12 +413,24 @@ run_aoa_thread(void *data) { struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); sc_mutex_unlock(&aoa->mutex); - bool cont = sc_aoa_process_event(aoa, &event); + bool cont = sc_aoa_process_event(aoa, &event, &vec_open); if (!cont) { // stopped break; } } + + // Explicitly unregister all registered HID ids before exiting + for (size_t i = 0; i < vec_open.size; ++i) { + uint16_t hid_id = vec_open.data[i]; + LOGD("Unregistering AOA device %" PRIu16 "...", hid_id); + bool ok = sc_aoa_unregister_hid(aoa, hid_id); + if (!ok) { + LOGW("Could not close AOA device: %" PRIu16, hid_id); + } + } + sc_vector_destroy(&vec_open); + return 0; } diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 738f6875..b7834b0f 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -98,11 +98,6 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - struct sc_hid_close hid_close; - sc_hid_keyboard_generate_close(&hid_close); - - bool ok = sc_aoa_push_close(kb->aoa, &hid_close); - if (!ok) { - LOGW("Could not push AOA HID close (keyboard)"); - } + (void) kb; + // Do nothing, kb->aoa will automatically unregister all devices } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index b4eb4eb8..33b777c4 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -78,11 +78,6 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - struct sc_hid_close hid_close; - sc_hid_mouse_generate_close(&hid_close); - - bool ok = sc_aoa_push_close(mouse->aoa, &hid_close); - if (!ok) { - LOGW("Could not push AOA HID close (mouse)"); - } + (void) mouse; + // Do nothing, mouse->aoa will automatically unregister all devices } From 1f5be743b474164f1efa691ccbc88435058d9ea7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 43/67] Make AOA keyboard/mouse open error fatal Now that the AOA open/close are asynchronous, an open error did not make scrcpy exit anymore. Add a mechanism to exit if the AOA device could not be opened asynchronously. PR #5270 --- app/src/events.h | 1 + app/src/scrcpy.c | 3 +++ app/src/usb/aoa_hid.c | 9 ++++++++- app/src/usb/aoa_hid.h | 4 +++- app/src/usb/keyboard_aoa.c | 2 +- app/src/usb/mouse_aoa.c | 2 +- app/src/usb/scrcpy_otg.c | 3 +++ 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index 3f15087a..59c55de4 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -19,6 +19,7 @@ enum { SC_EVENT_SCREEN_INIT_SIZE, SC_EVENT_TIME_LIMIT_REACHED, SC_EVENT_CONTROLLER_ERROR, + SC_EVENT_AOA_OPEN_ERROR, }; bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 529a3fc2..8e8fe86e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -168,6 +168,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_AOA_OPEN_ERROR: + LOGE("AOA open error"); + return SCRCPY_EXIT_FAILURE; case SC_EVENT_TIME_LIMIT_REACHED: LOGI("Time limit reached"); return SCRCPY_EXIT_SUCCESS; diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 59c8304b..236a78ed 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -5,6 +5,7 @@ #include #include "aoa_hid.h" +#include "events.h" #include "util/log.h" #include "util/str.h" #include "util/vector.h" @@ -252,7 +253,8 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, } bool -sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, + bool exit_on_open_error) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { sc_hid_open_log(hid_open); } @@ -271,6 +273,7 @@ sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; aoa_event->open.hid = *hid_open; + aoa_event->open.exit_on_error = exit_on_open_error; if (was_empty) { sc_cond_signal(&aoa->event_cond); @@ -365,6 +368,10 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event, } } else { LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); + if (event->open.exit_on_error) { + // Notify the error to the main thread, which will exit + sc_push_event(SC_EVENT_AOA_OPEN_ERROR); + } } break; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 010b3742..00961c28 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -24,6 +24,7 @@ struct sc_aoa_event { union { struct { struct sc_hid_open hid; + bool exit_on_error; } open; struct { struct sc_hid_close hid; @@ -73,7 +74,8 @@ sc_aoa_join(struct sc_aoa *aoa); // report_desc must be a pointer to static memory, accessed at any time from // another thread bool -sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open); +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, + bool exit_on_open_error); bool sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close); diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index b7834b0f..8f5cb755 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -69,7 +69,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { struct sc_hid_open hid_open; sc_hid_keyboard_generate_open(&hid_open); - bool ok = sc_aoa_push_open(aoa, &hid_open); + bool ok = sc_aoa_push_open(aoa, &hid_open, true); if (!ok) { LOGW("Could not push AOA HID open (keyboard)"); return false; diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 33b777c4..cb566cc0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -55,7 +55,7 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { struct sc_hid_open hid_open; sc_hid_mouse_generate_open(&hid_open); - bool ok = sc_aoa_push_open(aoa, &hid_open); + bool ok = sc_aoa_push_open(aoa, &hid_open, true); if (!ok) { LOGW("Could not push AOA HID open (mouse)"); return false; diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 715f690a..71d1863f 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -32,6 +32,9 @@ event_loop(struct scrcpy_otg *s) { case SC_EVENT_USB_DEVICE_DISCONNECTED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; + case SC_EVENT_AOA_OPEN_ERROR: + LOGE("AOA open error"); + return SCRCPY_EXIT_FAILURE; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; From de8455400ce7e5ccef1b78e7a30407a90fcc2872 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 44/67] Fix HID comments Fix typo and reference the latest version of "HID Usage Tables" specifications. PR #5270 --- app/src/hid/hid_keyboard.c | 9 ++++++--- app/src/hid/hid_mouse.c | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 64dffe80..961ad790 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -31,13 +31,16 @@ * For HID, only report descriptor is needed. * * The specification is available here: - * + * * * In particular, read: - * - 6.2.2 Report Descriptor + * - §6.2.2 Report Descriptor * - Appendix B.1 Protocol 1 (Keyboard) * - Appendix C: Keyboard Implementation * + * The HID Usage Tables is also useful: + * + * * Normally a basic HID keyboard uses 8 bytes: * Modifier Reserved Key Key Key Key Key Key * @@ -60,7 +63,7 @@ static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { 0x05, 0x07, // Usage Minimum (224) 0x19, 0xE0, - // Usage Maximum (231) + // Usage Maximum (231) 0x29, 0xE7, // Logical Minimum (0) 0x15, 0x00, diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index d1aae83a..7acc413b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -6,13 +6,13 @@ /** * Mouse descriptor from the specification: - * + * * * Appendix E (p71): §E.10 Report Descriptor (Mouse) * * The usage tags (like Wheel) are listed in "HID Usage Tables": - * - * §4 Generic Desktop Page (0x01) (p26) + * + * §4 Generic Desktop Page (0x01) (p32) */ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Page (Generic Desktop) @@ -34,7 +34,7 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Minimum (1) 0x19, 0x01, - // Usage Maximum (5) + // Usage Maximum (5) 0x29, 0x05, // Logical Minimum (0) 0x15, 0x00, @@ -62,9 +62,9 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { 0x09, 0x31, // Usage (Wheel) 0x09, 0x38, - // Local Minimum (-127) + // Logical Minimum (-127) 0x15, 0x81, - // Local Maximum (127) + // Logical Maximum (127) 0x25, 0x7F, // Report Size (8) 0x75, 0x08, From c8479fe8bf71f6f46bde95f2e7034cca97f7ff1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 22:00:24 +0200 Subject: [PATCH 45/67] Discard unknown SDL events Mouse and keyboard events with unknown button/keycode/scancode cannot be handled properly. Discard them without forwarding them to the keyboard or mouse processors. This can happen for example if a more recent version of SDL introduces new enum values. PR #5270 --- app/src/input_manager.c | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index d3c94d03..00f06777 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -400,7 +400,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool paused = im->screen->paused; bool video = im->screen->video; - SDL_Keycode keycode = event->keysym.sym; + SDL_Keycode sdl_keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; bool down = event->type == SDL_KEYDOWN; bool ctrl = event->keysym.mod & KMOD_CTRL; @@ -412,21 +412,21 @@ sc_input_manager_process_key(struct sc_input_manager *im, // The second condition is necessary to ignore the release of the modifier // key (because in this case mod is 0). bool is_shortcut = is_shortcut_mod(im, mod) - || is_shortcut_key(im, keycode); + || is_shortcut_key(im, sdl_keycode); if (down && !repeat) { - if (keycode == im->last_keycode && mod == im->last_mod) { + if (sdl_keycode == im->last_keycode && mod == im->last_mod) { ++im->key_repeat; } else { im->key_repeat = 0; - im->last_keycode = keycode; + im->last_keycode = sdl_keycode; im->last_mod = mod; } } if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - switch (keycode) { + switch (sdl_keycode) { case SDLK_h: if (im->kp && !shift && !repeat && !paused) { action_home(im, action); @@ -585,7 +585,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } uint64_t ack_to_wait = SC_SEQUENCE_INVALID; - bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; + bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat; if (im->clipboard_autosync && is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events @@ -613,10 +613,20 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } + enum sc_keycode keycode = sc_keycode_from_sdl(sdl_keycode); + if (keycode == SC_KEYCODE_UNKNOWN) { + return; + } + + enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode); + if (scancode == SC_SCANCODE_UNKNOWN) { + return; + } + struct sc_key_event evt = { .action = sc_action_from_sdl_keyboard_type(event->type), - .keycode = sc_keycode_from_sdl(event->keysym.sym), - .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .keycode = keycode, + .scancode = scancode, .repeat = event->repeat, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), }; @@ -739,6 +749,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); + if (button == SC_MOUSE_BUTTON_UNKNOWN) { + return; + } + if (!down) { // Mark the button as released im->mouse_buttons_state &= ~button; @@ -827,7 +841,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, struct sc_mouse_click_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), - .button = sc_mouse_button_from_sdl(event->button), + .button = button, .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER : SC_POINTER_ID_MOUSE, .buttons_state = im->mouse_buttons_state, From 4565f36ee6993eff3b0376ad2da74a71060bc5b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 46/67] Handle SDL gamepad events Introduce a gamepad processor trait, similar to the keyboard processor and mouse processor traits. Handle gamepad events received from SDL, convert them to scrcpy-specific gamepad events, and forward them to the gamepad processor. Further commits will provide AOA and UHID implementations of the gamepad processor trait. PR #5270 Co-authored-by: Luiz Henrique Laurini --- app/src/input_events.h | 98 +++++++++++++++++++++++++++++++ app/src/input_manager.c | 97 +++++++++++++++++++++++++++++- app/src/input_manager.h | 3 + app/src/scrcpy.c | 6 ++ app/src/screen.c | 1 + app/src/screen.h | 1 + app/src/trait/gamepad_processor.h | 50 ++++++++++++++++ 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 app/src/trait/gamepad_processor.h diff --git a/app/src/input_events.h b/app/src/input_events.h index bbf4372f..c8966a35 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -323,6 +323,38 @@ enum sc_mouse_button { SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2), }; +// Use the naming from SDL3 for gamepad axis and buttons: +// + +enum sc_gamepad_axis { + SC_GAMEPAD_AXIS_UNKNOWN = -1, + SC_GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX, + SC_GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY, + SC_GAMEPAD_AXIS_RIGHTX = SDL_CONTROLLER_AXIS_RIGHTX, + SC_GAMEPAD_AXIS_RIGHTY = SDL_CONTROLLER_AXIS_RIGHTY, + SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERLEFT, + SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERRIGHT, +}; + +enum sc_gamepad_button { + SC_GAMEPAD_BUTTON_UNKNOWN = -1, + SC_GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A, + SC_GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B, + SC_GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X, + SC_GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y, + SC_GAMEPAD_BUTTON_BACK = SDL_CONTROLLER_BUTTON_BACK, + SC_GAMEPAD_BUTTON_GUIDE = SDL_CONTROLLER_BUTTON_GUIDE, + SC_GAMEPAD_BUTTON_START = SDL_CONTROLLER_BUTTON_START, + SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_CONTROLLER_BUTTON_LEFTSTICK, + SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_CONTROLLER_BUTTON_RIGHTSTICK, + SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SC_GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP, + SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT, +}; + static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod), "SDL_Keymod must be convertible to sc_mod"); @@ -380,6 +412,33 @@ struct sc_touch_event { float pressure; }; +enum sc_gamepad_device_event_type { + SC_GAMEPAD_DEVICE_ADDED, + SC_GAMEPAD_DEVICE_REMOVED, +}; + +// As documented in : +// The ID value starts at 0 and increments from there. The value -1 is an +// invalid ID. +#define SC_GAMEPAD_ID_INVALID UINT32_C(-1) + +struct sc_gamepad_device_event { + enum sc_gamepad_device_event_type type; + uint32_t gamepad_id; +}; + +struct sc_gamepad_button_event { + uint32_t gamepad_id; + enum sc_action action; + enum sc_gamepad_button button; +}; + +struct sc_gamepad_axis_event { + uint32_t gamepad_id; + enum sc_gamepad_axis axis; + int16_t value; +}; + static inline uint16_t sc_mods_state_from_sdl(uint16_t mods_state) { return mods_state; @@ -444,4 +503,43 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { return buttons_state; } +static inline enum sc_gamepad_device_event_type +sc_gamepad_device_event_type_from_sdl_type(uint32_t type) { + assert(type == SDL_CONTROLLERDEVICEADDED + || type == SDL_CONTROLLERDEVICEREMOVED); + if (type == SDL_CONTROLLERDEVICEADDED) { + return SC_GAMEPAD_DEVICE_ADDED; + } + return SC_GAMEPAD_DEVICE_REMOVED; +} + +static inline enum sc_gamepad_axis +sc_gamepad_axis_from_sdl(uint8_t axis) { + if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { + // SC_GAMEPAD_AXIS_* constants are initialized from + // SDL_CONTROLLER_AXIS_* + return axis; + } + return SC_GAMEPAD_AXIS_UNKNOWN; +} + +static inline enum sc_gamepad_button +sc_gamepad_button_from_sdl(uint8_t button) { + if (button <= SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { + // SC_GAMEPAD_BUTTON_* constants are initialized from + // SDL_CONTROLLER_BUTTON_* + return button; + } + return SC_GAMEPAD_BUTTON_UNKNOWN; +} + +static inline enum sc_action +sc_action_from_sdl_controllerbutton_type(uint32_t type) { + assert(type == SDL_CONTROLLERBUTTONDOWN || type == SDL_CONTROLLERBUTTONUP); + if (type == SDL_CONTROLLERBUTTONDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 00f06777..77cb4f1d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -56,16 +56,18 @@ void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { // A key/mouse processor may not be present if there is no controller - assert((!params->kp && !params->mp) || params->controller); + assert((!params->kp && !params->mp && !params->gp) || params->controller); // A processor must have ops initialized assert(!params->kp || params->kp->ops); assert(!params->mp || params->mp->ops); + assert(!params->gp || params->gp->ops); im->controller = params->controller; im->fp = params->fp; im->screen = params->screen; im->kp = params->kp; im->mp = params->mp; + im->gp = params->gp; im->mouse_bindings = params->mouse_bindings; im->legacy_paste = params->legacy_paste; @@ -920,6 +922,78 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, im->mp->ops->process_mouse_scroll(im->mp, &evt); } +static void +sc_input_manager_process_gamepad_device(struct sc_input_manager *im, + const SDL_ControllerDeviceEvent *event) { + SDL_JoystickID id; + if (event->type == SDL_CONTROLLERDEVICEADDED) { + SDL_GameController *gc = SDL_GameControllerOpen(event->which); + if (!gc) { + LOGW("Could not open game controller"); + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); + if (!joystick) { + LOGW("Could not get controller joystick"); + SDL_GameControllerClose(gc); + return; + } + + id = SDL_JoystickInstanceID(joystick); + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + id = event->which; + + SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); + if (gc) { + SDL_GameControllerClose(gc); + } else { + LOGW("Unknown gamepad device removed"); + } + } else { + // Nothing to do + return; + } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + im->gp->ops->process_gamepad_device(im->gp, &evt); +} + +static void +sc_input_manager_process_gamepad_axis(struct sc_input_manager *im, + const SDL_ControllerAxisEvent *event) { + enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); + if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { + return; + } + + struct sc_gamepad_axis_event evt = { + .gamepad_id = event->which, + .axis = axis, + .value = event->value, + }; + im->gp->ops->process_gamepad_axis(im->gp, &evt); +} + +static void +sc_input_manager_process_gamepad_button(struct sc_input_manager *im, + const SDL_ControllerButtonEvent *event) { + enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); + if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { + return; + } + + struct sc_gamepad_button_event evt = { + .gamepad_id = event->which, + .action = sc_action_from_sdl_controllerbutton_type(event->type), + .button = button, + }; + im->gp->ops->process_gamepad_button(im->gp, &evt); +} + static bool is_apk(const char *file) { const char *ext = strrchr(file, '.'); @@ -992,6 +1066,27 @@ sc_input_manager_handle_event(struct sc_input_manager *im, } sc_input_manager_process_touch(im, &event->tfinger); break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + // Handle device added or removed even if paused + if (!im->gp) { + break; + } + sc_input_manager_process_gamepad_device(im, &event->cdevice); + break; + case SDL_CONTROLLERAXISMOTION: + if (!im->gp || paused) { + break; + } + sc_input_manager_process_gamepad_axis(im, &event->caxis); + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (!im->gp || paused) { + break; + } + sc_input_manager_process_gamepad_button(im, &event->cbutton); + break; case SDL_DROPFILE: { if (!control) { break; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 88558549..8efd0153 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -11,6 +11,7 @@ #include "file_pusher.h" #include "fps_counter.h" #include "options.h" +#include "trait/gamepad_processor.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" @@ -21,6 +22,7 @@ struct sc_input_manager { struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; @@ -50,6 +52,7 @@ struct sc_input_manager_params { struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8e8fe86e..24738876 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -485,6 +485,11 @@ scrcpy(struct scrcpy_options *options) { } } + 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); // Await for server without blocking Ctrl+C handling @@ -735,6 +740,7 @@ aoa_complete: .fp = fp, .kp = kp, .mp = mp, + .gp = NULL, .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/screen.c b/app/src/screen.c index 42be554a..cb455cb1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -477,6 +477,7 @@ sc_screen_init(struct sc_screen *screen, .screen = screen, .kp = params->kp, .mp = params->mp, + .gp = params->gp, .mouse_bindings = params->mouse_bindings, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, diff --git a/app/src/screen.h b/app/src/screen.h index 079d4fbb..7e1f7e6e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -78,6 +78,7 @@ struct sc_screen_params { struct sc_file_pusher *fp; struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h new file mode 100644 index 00000000..72479783 --- /dev/null +++ b/app/src/trait/gamepad_processor.h @@ -0,0 +1,50 @@ +#ifndef SC_GAMEPAD_PROCESSOR_H +#define SC_GAMEPAD_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include "input_events.h" + +/** + * Gamepad processor trait. + * + * Component able to handle gamepads devices and inject buttons and axis events. + */ +struct sc_gamepad_processor { + const struct sc_gamepad_processor_ops *ops; +}; + +struct sc_gamepad_processor_ops { + + /** + * Process a gamepad device added or removed + * + * This function is mandatory. + */ + void + (*process_gamepad_device)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); + + /** + * Process a gamepad axis event + * + * This function is mandatory. + */ + void + (*process_gamepad_axis)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event); + + /** + * Process a gamepad button event + * + * This function is mandatory. + */ + void + (*process_gamepad_button)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event); +}; + +#endif From f4d1e49ad91586ea3590eefc992bdfed558f7e06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 47/67] Add util functions to write in little-endian This will be helpful for writing HID values. PR #5270 --- app/src/util/binary.h | 20 ++++++++++++++++++++ app/tests/test_binary.c | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 6dc1b58e..7de9b505 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -13,6 +13,12 @@ sc_write16be(uint8_t *buf, uint16_t value) { buf[1] = value; } +static inline void +sc_write16le(uint8_t *buf, uint16_t value) { + buf[0] = value; + buf[1] = value >> 8; +} + static inline void sc_write32be(uint8_t *buf, uint32_t value) { buf[0] = value >> 24; @@ -21,12 +27,26 @@ sc_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline void +sc_write32le(uint8_t *buf, uint32_t value) { + buf[0] = value; + buf[1] = value >> 8; + buf[2] = value >> 16; + buf[3] = value >> 24; +} + static inline void sc_write64be(uint8_t *buf, uint64_t value) { sc_write32be(buf, value >> 32); sc_write32be(&buf[4], (uint32_t) value); } +static inline void +sc_write64le(uint8_t *buf, uint64_t value) { + sc_write32le(buf, (uint32_t) value); + sc_write32le(&buf[4], value >> 32); +} + static inline uint16_t sc_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c index 82a9c1e0..bce74ce2 100644 --- a/app/tests/test_binary.c +++ b/app/tests/test_binary.c @@ -42,6 +42,44 @@ static void test_write64be(void) { assert(buf[7] == 0xEF); } +static void test_write16le(void) { + uint16_t val = 0xABCD; + uint8_t buf[2]; + + sc_write16le(buf, val); + + assert(buf[0] == 0xCD); + assert(buf[1] == 0xAB); +} + +static void test_write32le(void) { + uint32_t val = 0xABCD1234; + uint8_t buf[4]; + + sc_write32le(buf, val); + + assert(buf[0] == 0x34); + assert(buf[1] == 0x12); + assert(buf[2] == 0xCD); + assert(buf[3] == 0xAB); +} + +static void test_write64le(void) { + uint64_t val = 0xABCD1234567890EF; + uint8_t buf[8]; + + sc_write64le(buf, val); + + assert(buf[0] == 0xEF); + assert(buf[1] == 0x90); + assert(buf[2] == 0x78); + assert(buf[3] == 0x56); + assert(buf[4] == 0x34); + assert(buf[5] == 0x12); + assert(buf[6] == 0xCD); + assert(buf[7] == 0xAB); +} + static void test_read16be(void) { uint8_t buf[2] = {0xAB, 0xCD}; @@ -108,6 +146,10 @@ int main(int argc, char *argv[]) { test_read32be(); test_read64be(); + test_write16le(); + test_write32le(); + test_write64le(); + test_float_to_u16fp(); test_float_to_i16fp(); return 0; From a59c6df4b7659adabd5cd98a95f1dbf330a50c14 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 48/67] Implement HID gamepad Implement the HID protocol for gamepads, that will be used in further commits by the AOA and UHID gamepad processor implementations. PR #5270 --- app/meson.build | 1 + app/src/hid/hid_event.h | 2 +- app/src/hid/hid_gamepad.c | 451 ++++++++++++++++++++++++++++++++++++++ app/src/hid/hid_gamepad.h | 53 +++++ 4 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 app/src/hid/hid_gamepad.c create mode 100644 app/src/hid/hid_gamepad.h diff --git a/app/meson.build b/app/meson.build index fc6b85e2..a4880420 100644 --- a/app/meson.build +++ b/app/meson.build @@ -32,6 +32,7 @@ src = [ 'src/screen.c', 'src/server.c', 'src/version.c', + 'src/hid/hid_gamepad.c', 'src/hid/hid_keyboard.c', 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 9171004e..d6818e30 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -5,7 +5,7 @@ #include -#define SC_HID_MAX_SIZE 8 +#define SC_HID_MAX_SIZE 15 struct sc_hid_input { uint16_t hid_id; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c new file mode 100644 index 00000000..cd009d15 --- /dev/null +++ b/app/src/hid/hid_gamepad.c @@ -0,0 +1,451 @@ +#include "hid_gamepad.h" + +#include +#include + +#include "util/binary.h" +#include "util/log.h" + +// 2x2 bytes for left stick (X, Y) +// 2x2 bytes for right stick (Z, Rz) +// 2x2 bytes for L2/R2 triggers +// 2 bytes for buttons + padding, +// 1 byte for hat switch (dpad) + padding +#define SC_HID_GAMEPAD_EVENT_SIZE 15 + +// The ->buttons field stores the state for all buttons, but only some of them +// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are +// stored locally in the MSB of this field, but not transmitted as is: they are +// transformed to generate another specific byte. +#define SC_HID_BUTTONS_MASK 0xFFFF + +// outside SC_HID_BUTTONS_MASK +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_UP UINT32_C(0x10000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN UINT32_C(0x20000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT UINT32_C(0x40000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000) + +/** + * Gamepad descriptor manually crafted to transmit the input reports. + * + * The HID specification is available here: + * + * + * The HID Usage Tables is also useful: + * + */ +static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Gamepad) + 0x09, 0x05, + + // Collection (Application) + 0xA1, 0x01, + + // Collection (Physical) + 0xA1, 0x00, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (X) Left stick x + 0x09, 0x30, + // Usage (Y) Left stick y + 0x09, 0x31, + // Usage (Z) Right stick x + 0x09, 0x32, + // Usage (Rz) Right stick y + 0x09, 0x35, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (65535) + // Cannot use 26 FF FF because 0xFFFF is interpreted as signed 16-bit + 0x27, 0xFF, 0xFF, 0x00, 0x00, // little-endian + // Report Size (16) + 0x75, 0x10, + // Report Count (4) + 0x95, 0x04, + // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz) + 0x81, 0x02, + + // Usage Page (Simulation Controls) + 0x05, 0x02, + // Usage (Brake) + 0x09, 0xC5, + // Usage (Accelerator) + 0x09, 0xC4, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (32767) + 0x26, 0xFF, 0x7F, + // Report Size (16) + 0x75, 0x10, + // Report Count (2) + 0x95, 0x02, + // Input (Data, Variable, Absolute): 2 bytes (L2, R2) + 0x81, 0x02, + + // Usage Page (Buttons) + 0x05, 0x09, + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (16) + 0x29, 0x10, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Count (16) + 0x95, 0x10, + // Report Size (1) + 0x75, 0x01, + // Input (Data, Variable, Absolute): 16 buttons bits + 0x81, 0x02, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Hat switch) + 0x09, 0x39, + // Logical Minimum (1) + 0x15, 0x01, + // Logical Maximum (8) + 0x25, 0x08, + // Report Size (4) + 0x75, 0x04, + // Report Count (1) + 0x95, 0x01, + // Input (Data, Variable, Null State): 4-bit value + 0x81, 0x42, + + // End Collection + 0xC0, + + // End Collection + 0xC0, +}; + +/** + * A gamepad HID input report is 15 bytes long: + * - bytes 0-3: left stick state + * - bytes 4-7: right stick state + * - bytes 8-11: L2/R2 triggers state + * - bytes 12-13: buttons state + * - bytes 14: hat switch position (dpad) + * + * +---------------+ + * byte 0: |. . . . . . . .| + * | | left stick x (0-65535, little-endian) + * byte 1: |. . . . . . . .| + * +---------------+ + * byte 2: |. . . . . . . .| + * | | left stick y (0-65535, little-endian) + * byte 3: |. . . . . . . .| + * +---------------+ + * byte 4: |. . . . . . . .| + * | | right stick x (0-65535, little-endian) + * byte 5: |. . . . . . . .| + * +---------------+ + * byte 6: |. . . . . . . .| + * | | right stick y (0-65535, little-endian) + * byte 7: |. . . . . . . .| + * +---------------+ + * byte 8: |. . . . . . . .| + * | | L2 trigger (0-32767, little-endian) + * byte 9: |0 . . . . . . .| + * +---------------+ + * byte 10: |. . . . . . . .| + * | | R2 trigger (0-32767, little-endian) + * byte 11: |0 . . . . . . .| + * +---------------+ + * + * ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER + * | ,------------- SC_GAMEPAD_BUTTON_LEFT_SHOULDER + * | | + * | | ,--------- SC_GAMEPAD_BUTTON_NORTH + * | | | ,------- SC_GAMEPAD_BUTTON_WEST + * | | | | + * | | | | ,--- SC_GAMEPAD_BUTTON_EAST + * | | | | | ,- SC_GAMEPAD_BUTTON_SOUTH + * v v v v v v + * +---------------+ + * byte 12: |. . 0 . . 0 . .| + * | | Buttons (16-bit little-endian) + * byte 13: |0 . . . . . 0 0| + * +---------------+ + * ^ ^ ^ ^ ^ + * | | | | | + * | | | | | + * | | | | `----- SC_GAMEPAD_BUTTON_BACK + * | | | `------- SC_GAMEPAD_BUTTON_START + * | | `--------- SC_GAMEPAD_BUTTON_GUIDE + * | `----------- SC_GAMEPAD_BUTTON_LEFT_STICK + * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK + * + * +---------------+ + * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8) + * +---------------+ + * 9 possible positions and their values: + * 8 1 2 + * 7 0 3 + * 6 5 4 + * (8 is top-left, 1 is top, 2 is top-right, etc.) + */ + +static void +sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + slot->gamepad_id = gamepad_id; + slot->buttons = 0; + slot->axis_left_x = 0; + slot->axis_left_y = 0; + slot->axis_right_x = 0; + slot->axis_right_y = 0; + slot->axis_left_trigger = 0; + slot->axis_right_trigger = 0; +} + +static ssize_t +sc_hid_gamepad_slot_find(struct sc_hid_gamepad *hid, uint32_t gamepad_id) { + for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { + if (gamepad_id == hid->slots[i].gamepad_id) { + // found + return i; + } + } + + return -1; +} + +void +sc_hid_gamepad_init(struct sc_hid_gamepad *hid) { + for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { + hid->slots[i].gamepad_id = SC_GAMEPAD_ID_INVALID; + } +} + +static inline uint16_t +sc_hid_gamepad_slot_get_id(size_t slot_idx) { + assert(slot_idx < SC_MAX_GAMEPADS); + return SC_HID_ID_GAMEPAD_FIRST + slot_idx; +} + +bool +sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, + struct sc_hid_open *hid_open, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID); + if (slot_idx == -1) { + LOGW("No gamepad slot available for new gamepad %" PRIu32, gamepad_id); + return false; + } + + sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + hid_open->hid_id = hid_id; + hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); + + return true; +} + +bool +sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, + struct sc_hid_close *hid_close, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Unknown gamepad removed %" PRIu32, gamepad_id); + return false; + } + + hid->slots[slot_idx].gamepad_id = SC_GAMEPAD_ID_INVALID; + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + hid_close->hid_id = hid_id; + + return true; +} + +static uint8_t +sc_hid_gamepad_get_dpad_value(uint32_t buttons) { + // Value depending on direction: + // 8 1 2 + // 7 0 3 + // 6 5 4 + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_UP) { + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 8; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 2; + } + return 1; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN) { + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 6; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 4; + } + return 5; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 7; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 3; + } + + return 0; +} + +static void +sc_hid_gamepad_event_from_slot(uint16_t hid_id, + const struct sc_hid_gamepad_slot *slot, + struct sc_hid_input *hid_input) { + hid_input->hid_id = hid_id; + hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE; + + uint8_t *data = hid_input->data; + // Values must be written in little-endian + sc_write16le(data, slot->axis_left_x); + sc_write16le(data + 2, slot->axis_left_y); + sc_write16le(data + 4, slot->axis_right_x); + sc_write16le(data + 6, slot->axis_right_y); + sc_write16le(data + 8, slot->axis_left_trigger); + sc_write16le(data + 10, slot->axis_right_trigger); + sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK); + data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons); +} + +static uint32_t +sc_hid_gamepad_get_button_id(enum sc_gamepad_button button) { + switch (button) { + case SC_GAMEPAD_BUTTON_SOUTH: + return 0x0001; + case SC_GAMEPAD_BUTTON_EAST: + return 0x0002; + case SC_GAMEPAD_BUTTON_WEST: + return 0x0008; + case SC_GAMEPAD_BUTTON_NORTH: + return 0x0010; + case SC_GAMEPAD_BUTTON_BACK: + return 0x0400; + case SC_GAMEPAD_BUTTON_GUIDE: + return 0x1000; + case SC_GAMEPAD_BUTTON_START: + return 0x0800; + case SC_GAMEPAD_BUTTON_LEFT_STICK: + return 0x2000; + case SC_GAMEPAD_BUTTON_RIGHT_STICK: + return 0x4000; + case SC_GAMEPAD_BUTTON_LEFT_SHOULDER: + return 0x0040; + case SC_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return 0x0080; + case SC_GAMEPAD_BUTTON_DPAD_UP: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_UP; + case SC_GAMEPAD_BUTTON_DPAD_DOWN: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN; + case SC_GAMEPAD_BUTTON_DPAD_LEFT: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT; + case SC_GAMEPAD_BUTTON_DPAD_RIGHT: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT; + default: + // unknown button, ignore + return 0; + } +} + +bool +sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_button_event *event) { + if ((event->button < 0) || (event->button > 15)) { + return false; + } + + uint32_t gamepad_id = event->gamepad_id; + + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Axis event for unknown gamepad %" PRIu32, gamepad_id); + return false; + } + + assert(slot_idx < SC_MAX_GAMEPADS); + + struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; + + uint32_t button = sc_hid_gamepad_get_button_id(event->button); + if (!button) { + // unknown button, ignore + return false; + } + + if (event->action == SC_ACTION_DOWN) { + slot->buttons |= button; + } else { + assert(event->action == SC_ACTION_UP); + slot->buttons &= ~button; + } + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); + + return true; +} + +bool +sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_axis_event *event) { + uint32_t gamepad_id = event->gamepad_id; + + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Button event for unknown gamepad %" PRIu32, gamepad_id); + return false; + } + + assert(slot_idx < SC_MAX_GAMEPADS); + + struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; + +// [-32768 to 32767] -> [0 to 65535] +#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) + switch (event->axis) { + case SC_GAMEPAD_AXIS_LEFTX: + slot->axis_left_x = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_LEFTY: + slot->axis_left_y = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_RIGHTX: + slot->axis_right_x = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_RIGHTY: + slot->axis_right_y = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_LEFT_TRIGGER: + // Trigger is always positive between 0 and 32767 + slot->axis_left_trigger = MAX(0, event->value); + break; + case SC_GAMEPAD_AXIS_RIGHT_TRIGGER: + // Trigger is always positive between 0 and 32767 + slot->axis_right_trigger = MAX(0, event->value); + break; + default: + return false; + } + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); + + return true; +} diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h new file mode 100644 index 00000000..b532a703 --- /dev/null +++ b/app/src/hid/hid_gamepad.h @@ -0,0 +1,53 @@ +#ifndef SC_HID_GAMEPAD_H +#define SC_HID_GAMEPAD_H + +#include "common.h" + +#include + +#include "hid/hid_event.h" +#include "input_events.h" + +#define SC_MAX_GAMEPADS 8 +#define SC_HID_ID_GAMEPAD_FIRST 3 +#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1) + +struct sc_hid_gamepad_slot { + uint32_t gamepad_id; + uint32_t buttons; + uint16_t axis_left_x; + uint16_t axis_left_y; + uint16_t axis_right_x; + uint16_t axis_right_y; + uint16_t axis_left_trigger; + uint16_t axis_right_trigger; +}; + +struct sc_hid_gamepad { + struct sc_hid_gamepad_slot slots[SC_MAX_GAMEPADS]; +}; + +void +sc_hid_gamepad_init(struct sc_hid_gamepad *hid); + +bool +sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, + struct sc_hid_open *hid_open, + uint32_t gamepad_id); + +bool +sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, + struct sc_hid_close *hid_close, + uint32_t gamepad_id); + +bool +sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_button_event *event); + +bool +sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_axis_event *event); + +#endif From a34a62ca4b4cc5aa946b2c6c2a53d18815160abd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 49/67] 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 From 3e68244dd3f410da5ff5cce3e6bb1c40743a7c51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 50/67] Add connected gamepads on start Trigger SDL_CONTROLLERDEVICEADDED for all gamepads already connected when scrcpy starts. We want to handle both the gamepads initially connected and the gamepads connected while scrcpy is running. This is not racy, because this event may not be trigged automatically until SDL events are "pumped" (SDL_PumpEvents/SDL_WaitEvent). PR #5270 --- app/src/scrcpy.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index bd706cc1..fbd00db7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -342,6 +342,21 @@ scrcpy_generate_scid(void) { return sc_rand_u32(&rand) & 0x7FFFFFFF; } +static void +init_sdl_gamepads(void) { + // Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already + // connected + int num_joysticks = SDL_NumJoysticks(); + for (int i = 0; i < num_joysticks; ++i) { + if (SDL_IsGameController(i)) { + SDL_Event event; + event.cdevice.type = SDL_CONTROLLERDEVICEADDED; + event.cdevice.which = i; + SDL_PushEvent(&event); + } + } +} + enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; @@ -868,6 +883,11 @@ aoa_complete: timeout_started = true; } + if (options->control + && options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + init_sdl_gamepads(); + } + ret = event_loop(s); terminate_event_loop(); LOGD("quit..."); From 5fe884276b099f6313db886312fe691cc0bf69a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 51/67] Add gamepad support in OTG mode Implement gamepad support for OTG. PR #5270 --- app/src/cli.c | 12 ++++- app/src/usb/scrcpy_otg.c | 22 +++++++++ app/src/usb/screen_otg.c | 100 +++++++++++++++++++++++++++++++++++++++ app/src/usb/screen_otg.h | 3 ++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 96877a51..6fb8cdcb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2838,9 +2838,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode; + if (gmode != SC_GAMEPAD_INPUT_MODE_AOA + && gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --gamepad only supports aoa or disabled."); + return false; + } + if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED - && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { - LOGE("Cannot disable both keyboard and mouse in OTG mode."); + && mmode == SC_MOUSE_INPUT_MODE_DISABLED + && gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) { + LOGE("Cannot not disable all inputs in OTG mode."); return false; } } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 71d1863f..47afd9d0 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -12,6 +12,7 @@ struct scrcpy_otg { struct sc_aoa aoa; struct sc_keyboard_aoa keyboard; struct sc_mouse_aoa mouse; + struct sc_gamepad_aoa gamepad; struct sc_screen_otg screen_otg; }; @@ -63,6 +64,13 @@ scrcpy_otg(struct scrcpy_options *options) { return SCRCPY_EXIT_FAILURE; } + if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL controller: %s", SDL_GetError()); + // Not fatal, keyboard/mouse should still work + } + } + atexit(SDL_Quit); if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { @@ -73,6 +81,7 @@ scrcpy_otg(struct scrcpy_options *options) { struct sc_keyboard_aoa *keyboard = NULL; struct sc_mouse_aoa *mouse = NULL; + struct sc_gamepad_aoa *gamepad = NULL; bool usb_device_initialized = false; bool usb_connected = false; bool aoa_started = false; @@ -119,11 +128,15 @@ scrcpy_otg(struct scrcpy_options *options) { || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED); assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED); + assert(options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA + || options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_DISABLED); bool enable_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool enable_mouse = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; + bool enable_gamepad = + options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; if (enable_keyboard) { ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); @@ -141,6 +154,11 @@ scrcpy_otg(struct scrcpy_options *options) { mouse = &s->mouse; } + if (enable_gamepad) { + sc_gamepad_aoa_init(&s->gamepad, &s->aoa); + gamepad = &s->gamepad; + } + ok = sc_aoa_start(&s->aoa); if (!ok) { goto end; @@ -155,6 +173,7 @@ scrcpy_otg(struct scrcpy_options *options) { struct sc_screen_otg_params params = { .keyboard = keyboard, .mouse = mouse, + .gamepad = gamepad, .window_title = window_title, .always_on_top = options->always_on_top, .window_x = options->window_x, @@ -188,6 +207,9 @@ end: if (keyboard) { sc_keyboard_aoa_destroy(&s->keyboard); } + if (gamepad) { + sc_gamepad_aoa_destroy(&s->gamepad); + } if (aoa_initialized) { sc_aoa_join(&s->aoa); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 5c4f97f0..b13f8d04 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -59,6 +59,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, const struct sc_screen_otg_params *params) { screen->keyboard = params->keyboard; screen->mouse = params->mouse; + screen->gamepad = params->gamepad; screen->mouse_capture_key_pressed = 0; @@ -214,6 +215,87 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, mp->ops->process_mouse_scroll(mp, &evt); } +static void +sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, + const SDL_ControllerDeviceEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + SDL_JoystickID id; + if (event->type == SDL_CONTROLLERDEVICEADDED) { + SDL_GameController *gc = SDL_GameControllerOpen(event->which); + if (!gc) { + LOGW("Could not open game controller"); + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); + if (!joystick) { + LOGW("Could not get controller joystick"); + SDL_GameControllerClose(gc); + return; + } + + id = SDL_JoystickInstanceID(joystick); + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + id = event->which; + + SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); + if (gc) { + SDL_GameControllerClose(gc); + } else { + LOGW("Unknown gamepad device removed"); + } + } else { + // Nothing to do + return; + } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + gp->ops->process_gamepad_device(gp, &evt); +} + +static void +sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen, + const SDL_ControllerAxisEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); + if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { + return; + } + + struct sc_gamepad_axis_event evt = { + .gamepad_id = event->which, + .axis = axis, + .value = event->value, + }; + gp->ops->process_gamepad_axis(gp, &evt); +} + +static void +sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, + const SDL_ControllerButtonEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); + if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { + return; + } + + struct sc_gamepad_button_event evt = { + .gamepad_id = event->which, + .action = sc_action_from_sdl_controllerbutton_type(event->type), + .button = button, + }; + gp->ops->process_gamepad_button(gp, &evt); +} + void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { switch (event->type) { @@ -293,5 +375,23 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + // Handle device added or removed even if paused + if (screen->gamepad) { + sc_screen_otg_process_gamepad_device(screen, &event->cdevice); + } + break; + case SDL_CONTROLLERAXISMOTION: + if (screen->gamepad) { + sc_screen_otg_process_gamepad_axis(screen, &event->caxis); + } + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (screen->gamepad) { + sc_screen_otg_process_gamepad_button(screen, &event->cbutton); + } + break; } } diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index c4e03b87..2ea76eda 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -8,10 +8,12 @@ #include "keyboard_aoa.h" #include "mouse_aoa.h" +#include "gamepad_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; struct sc_mouse_aoa *mouse; + struct sc_gamepad_aoa *gamepad; SDL_Window *window; SDL_Renderer *renderer; @@ -24,6 +26,7 @@ struct sc_screen_otg { struct sc_screen_otg_params { struct sc_keyboard_aoa *keyboard; struct sc_mouse_aoa *mouse; + struct sc_gamepad_aoa *gamepad; const char *window_title; bool always_on_top; From 64a25f6e9dfc12d25fa54126afbbef34b27f56f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 52/67] Add UHID_DESTROY control message This message will be sent on gamepad disconnection. Contrary to keyboard and mouse devices, which are registered once and unregistered when scrcpy exists, each physical gamepad is mapped with its own HID id, and they can be plugged and unplugged dynamically. PR #5270 --- app/src/control_msg.c | 13 ++++++++++-- app/src/control_msg.h | 4 ++++ app/tests/test_control_msg_serialize.c | 20 +++++++++++++++++++ .../scrcpy/control/ControlMessage.java | 10 +++++++++- .../scrcpy/control/ControlMessageReader.java | 7 +++++++ .../control/ControlMessageReaderTest.java | 18 +++++++++++++++++ 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index daa3bde7..b9d50222 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -155,6 +155,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { sc_write16be(&buf[3], msg->uhid_input.size); memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); return 5 + msg->uhid_input.size; + case SC_CONTROL_MSG_TYPE_UHID_DESTROY: + sc_write16be(&buf[1], msg->uhid_destroy.id); + return 3; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -269,6 +272,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } break; } + case SC_CONTROL_MSG_TYPE_UHID_DESTROY: + LOG_CMSG("UHID destroy [%" PRIu16 "]", msg->uhid_destroy.id); + break; case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: LOG_CMSG("open hard keyboard settings"); break; @@ -281,8 +287,11 @@ sc_control_msg_log(const struct sc_control_msg *msg) { bool sc_control_msg_is_droppable(const struct sc_control_msg *msg) { // Cannot drop UHID_CREATE messages, because it would cause all further - // UHID_INPUT messages for this device to be invalid - return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE; + // UHID_INPUT messages for this device to be invalid. + // Cannot drop UHID_DESTROY messages either, because a further UHID_CREATE + // with the same id may fail. + return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE + && msg->type != SC_CONTROL_MSG_TYPE_UHID_DESTROY; } void diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 63670705..b48d91af 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -39,6 +39,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, + SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; @@ -105,6 +106,9 @@ struct sc_control_msg { uint16_t size; uint8_t data[SC_HID_MAX_SIZE]; } uhid_input; + struct { + uint16_t id; + } uhid_destroy; }; }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 7a978f2b..f88048d8 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -370,6 +370,25 @@ static void test_serialize_uhid_input(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_uhid_destroy(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_DESTROY, + .uhid_destroy = { + .id = 42, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 3); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_DESTROY, + 0, 42, // id + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_open_hard_keyboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, @@ -405,6 +424,7 @@ int main(int argc, char *argv[]) { test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); + test_serialize_uhid_destroy(); test_serialize_open_hard_keyboard(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index c414f2a5..ef71353a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -21,7 +21,8 @@ public final class ControlMessage { public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; - public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; + public static final int TYPE_UHID_DESTROY = 14; + public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final long SEQUENCE_INVALID = 0; @@ -146,6 +147,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createUhidDestroy(int id) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_DESTROY; + msg.id = id; + return msg; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index f2e89da2..ef7877f1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -51,6 +51,8 @@ public class ControlMessageReader { return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: return parseUhidInput(); + case ControlMessage.TYPE_UHID_DESTROY: + return parseUhidDestroy(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -142,6 +144,11 @@ public class ControlMessageReader { return ControlMessage.createUhidInput(id, data); } + private ControlMessage parseUhidDestroy() throws IOException { + int id = dis.readUnsignedShort(); + return ControlMessage.createUhidDestroy(id); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index ae18154d..0fd5f0ac 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -362,6 +362,24 @@ public class ControlMessageReaderTest { Assert.assertEquals(-1, bis.read()); // EOS } + @Test + public void testParseUhidDestroy() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_DESTROY); + dos.writeShort(42); // id + byte[] packet = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); + Assert.assertEquals(ControlMessage.TYPE_UHID_DESTROY, event.getType()); + Assert.assertEquals(42, event.getId()); + + Assert.assertEquals(-1, bis.read()); // EOS + } + @Test public void testParseOpenHardKeyboardSettings() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); From f9d1a333a0807bc354a7ba65a9fd2a9dbd12aa4b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 53/67] 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). PR #5270 --- app/data/bash-completion/scrcpy | 3 +- app/data/zsh-completion/_scrcpy | 3 +- app/meson.build | 1 + app/scrcpy.1 | 7 +- app/src/cli.c | 16 ++- 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 ++- 11 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 app/src/uhid/gamepad_uhid.c create mode 100644 app/src/uhid/gamepad_uhid.h 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() { From c4febd55ebb75e9033fe7af072cf4af54182ce45 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Sep 2024 23:06:16 +0200 Subject: [PATCH 54/67] Make -K -M and -G use AOA in OTG mode For convenience, short options were added to select UHID input modes: - -K for --keyboard=uhid - -M for --mouse=uhid - -G for --gamepad=uhid In OTG mode, UHID is not available, so the short options should select AOA instead. PR #5270 --- app/data/zsh-completion/_scrcpy | 6 +++--- app/scrcpy.1 | 8 ++++---- app/src/cli.c | 24 ++++++++++++++++++------ app/src/options.h | 3 +++ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index b5ceda3a..fa0fa84f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,10 +33,10 @@ 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]' - '-G[Use UHID gamepad (same as --gamepad=uhid)]' + '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' - '-K[Use UHID keyboard (same as --keyboard=uhid)]' + '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -46,7 +46,7 @@ arguments=( '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID mouse (same as --mouse=uhid)]' + '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e4295feb..a256c40e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -176,8 +176,8 @@ Start in fullscreen. Do not attempt to use "adb reverse" to connect to the device. .TP -.B \-K -Same as \fB\-\-gamepad=uhid\fR. +.B \-G +Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-gamepad " mode @@ -196,7 +196,7 @@ Print this help. .TP .B \-K -Same as \fB\-\-keyboard=uhid\fR. +Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-keyboard " mode @@ -261,7 +261,7 @@ Default is 0 (unlimited). .TP .B \-M -Same as \fB\-\-mouse=uhid\fR. +Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-max\-fps " value diff --git a/app/src/cli.c b/app/src/cli.c index 7fe0bc70..3c1f9a1b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -375,7 +375,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'G', - .text = "Same as --gamepad=uhid.", + .text = "Same as --gamepad=uhid, or --gamepad=aoa if --otg is set.", }, { .longopt_id = OPT_GAMEPAD, @@ -397,7 +397,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'K', - .text = "Same as --keyboard=uhid.", + .text = "Same as --keyboard=uhid, or --keyboard=aoa if --otg is set.", }, { .longopt_id = OPT_KEYBOARD, @@ -493,7 +493,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'M', - .text = "Same as --mouse=uhid.", + .text = "Same as --mouse=uhid, or --mouse=aoa if --otg is set.", }, { .longopt_id = OPT_MAX_FPS, @@ -2252,7 +2252,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], args->help = true; break; case 'K': - opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID; + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA; break; case OPT_KEYBOARD: if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { @@ -2272,7 +2272,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case 'M': - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID_OR_AOA; break; case OPT_MOUSE: if (!parse_mouse(optarg, &opts->mouse_input_mode)) { @@ -2657,7 +2657,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->audio_dup = true; break; case 'G': - opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID; + opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA; break; case OPT_GAMEPAD: if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { @@ -2781,7 +2781,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA : SC_KEYBOARD_INPUT_MODE_SDK; + } else if (opts->keyboard_input_mode + == SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA) { + opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA + : SC_KEYBOARD_INPUT_MODE_UHID; } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { if (otg) { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; @@ -2791,11 +2796,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } else { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } + } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) { + opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA + : SC_MOUSE_INPUT_MODE_UHID; } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK && !opts->video_playback) { LOGE("SDK mouse mode requires video playback. Try --mouse=uhid."); return false; } + if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) { + opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA + : SC_GAMEPAD_INPUT_MODE_UHID; + } } // If mouse bindings are not explicitly set, configure default bindings diff --git a/app/src/options.h b/app/src/options.h index e2652773..5f6726e0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -142,6 +142,7 @@ enum sc_lock_video_orientation { enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, + SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_SDK, SC_KEYBOARD_INPUT_MODE_UHID, @@ -150,6 +151,7 @@ enum sc_keyboard_input_mode { enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AUTO, + SC_MOUSE_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_MOUSE_INPUT_MODE_DISABLED, SC_MOUSE_INPUT_MODE_SDK, SC_MOUSE_INPUT_MODE_UHID, @@ -158,6 +160,7 @@ enum sc_mouse_input_mode { enum sc_gamepad_input_mode { SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_GAMEPAD_INPUT_MODE_UHID, SC_GAMEPAD_INPUT_MODE_AOA, }; From 68e27c7357fbf0c3a3a5d75fb40611b809c13282 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Sep 2024 20:01:28 +0200 Subject: [PATCH 55/67] Reorder function parameters for consistency Make the local function write_string() accept the output buffer as a first parameter, like the other similar functions. PR #5270 --- app/src/control_msg.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index b9d50222..bef7ab05 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -85,7 +85,7 @@ write_position(uint8_t *buf, const struct sc_position *position) { // write length (4 bytes) + string (non null-terminated) static size_t -write_string(const char *utf8, size_t max_len, uint8_t *buf) { +write_string(uint8_t *buf, const char *utf8, size_t max_len) { size_t len = sc_str_utf8_truncation_index(utf8, max_len); sc_write32be(buf, len); memcpy(&buf[4], utf8, len); @@ -103,9 +103,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = - write_string(msg->inject_text.text, - SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); + size_t len = write_string(&buf[1], msg->inject_text.text, + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); return 1 + len; } case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: @@ -137,9 +136,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: sc_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; - size_t len = write_string(msg->set_clipboard.text, - SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[10]); + size_t len = write_string(&buf[10], msg->set_clipboard.text, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); return 10 + len; case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; From 7f250dd66923018e145f90789d53c4c7a1e30962 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Sep 2024 19:57:14 +0200 Subject: [PATCH 56/67] Mention physical gamepad names for UHID devices Initialize UHID devices with a custom name: - "scrcpy: $GAMEPAD_NAME" for gamepads - "scrcpy" for keyboard and mouse (or if no gamepad name is available) The name may appear in Android apps. PR #5270 --- app/src/control_msg.c | 52 +++++++++++++++---- app/src/control_msg.h | 1 + app/src/hid/hid_event.h | 1 + app/src/hid/hid_gamepad.c | 6 +++ app/src/hid/hid_keyboard.c | 1 + app/src/hid/hid_mouse.c | 1 + app/src/uhid/gamepad_uhid.c | 1 + app/src/uhid/keyboard_uhid.c | 1 + app/src/uhid/mouse_uhid.c | 1 + app/tests/test_control_msg_serialize.c | 7 ++- .../scrcpy/control/ControlMessage.java | 3 +- .../scrcpy/control/ControlMessageReader.java | 12 +++-- .../genymobile/scrcpy/control/Controller.java | 2 +- .../scrcpy/control/UhidManager.java | 17 ++++-- .../control/ControlMessageReaderTest.java | 5 +- 15 files changed, 88 insertions(+), 23 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index bef7ab05..d599b62d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -83,15 +83,34 @@ write_position(uint8_t *buf, const struct sc_position *position) { sc_write16be(&buf[10], position->screen_size.height); } -// write length (4 bytes) + string (non null-terminated) +// Write truncated string, and return the size +static size_t +write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) { + if (!utf8) { + return 0; + } + size_t len = sc_str_utf8_truncation_index(utf8, max_len); + memcpy(payload, utf8, len); + return len; +} + +// Write length (4 bytes) + string (non null-terminated) static size_t write_string(uint8_t *buf, const char *utf8, size_t max_len) { - size_t len = sc_str_utf8_truncation_index(utf8, max_len); + size_t len = write_string_payload(buf + 4, utf8, max_len); sc_write32be(buf, len); - memcpy(&buf[4], utf8, len); return 4 + len; } +// Write length (1 byte) + string (non null-terminated) +static size_t +write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) { + assert(max_len <= 0xFF); + size_t len = write_string_payload(buf + 1, utf8, max_len); + buf[0] = len; + return 1 + len; +} + size_t sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { buf[0] = msg->type; @@ -144,10 +163,18 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); - sc_write16be(&buf[3], msg->uhid_create.report_desc_size); - memcpy(&buf[5], msg->uhid_create.report_desc, - msg->uhid_create.report_desc_size); - return 5 + msg->uhid_create.report_desc_size; + + size_t index = 3; + index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); + + sc_write16be(&buf[index], msg->uhid_create.report_desc_size); + index += 2; + + memcpy(&buf[index], msg->uhid_create.report_desc, + msg->uhid_create.report_desc_size); + index += msg->uhid_create.report_desc_size; + + return index; case SC_CONTROL_MSG_TYPE_UHID_INPUT: sc_write16be(&buf[1], msg->uhid_input.id); sc_write16be(&buf[3], msg->uhid_input.size); @@ -253,10 +280,15 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; - case SC_CONTROL_MSG_TYPE_UHID_CREATE: - LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16, - msg->uhid_create.id, msg->uhid_create.report_desc_size); + case SC_CONTROL_MSG_TYPE_UHID_CREATE: { + // Quote only if name is not null + const char *name = msg->uhid_create.name; + const char *quote = name ? "\"" : ""; + LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s " + "report_desc_size=%" PRIu16, msg->uhid_create.id, + quote, name, quote, msg->uhid_create.report_desc_size); break; + } case SC_CONTROL_MSG_TYPE_UHID_INPUT: { char *hex = sc_str_to_hex_string(msg->uhid_input.data, msg->uhid_input.size); diff --git a/app/src/control_msg.h b/app/src/control_msg.h index b48d91af..1ae8cae4 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -98,6 +98,7 @@ struct sc_control_msg { } set_screen_power_mode; struct { uint16_t id; + const char *name; // pointer to static data uint16_t report_desc_size; const uint8_t *report_desc; // pointer to static data } uhid_create; diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index d6818e30..37c3611b 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -15,6 +15,7 @@ struct sc_hid_input { struct sc_hid_open { uint16_t hid_id; + const char *name; // pointer to static memory const uint8_t *report_desc; // pointer to static memory size_t report_desc_size; }; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index cd009d15..e2bf0616 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -243,8 +243,14 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); + SDL_GameController* game_controller = + SDL_GameControllerFromInstanceID(gamepad_id); + assert(game_controller); + const char *name = SDL_GameControllerName(game_controller); + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); hid_open->hid_id = hid_id; + hid_open->name = name; hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 961ad790..2109224a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -335,6 +335,7 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_KEYBOARD; + hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); } diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 7acc413b..ac215165 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -190,6 +190,7 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_MOUSE; + hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); } diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 3c8d3643..62b0f653 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -30,6 +30,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.name = hid_open->name; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 11d41e40..9fdf4def 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -152,6 +152,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_KEYBOARD; + msg.uhid_create.name = hid_open.name; 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(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 9544ab0d..1dc02777 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -81,6 +81,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_MOUSE; + msg.uhid_create.name = hid_open.name; 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(controller, &msg)) { diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index f88048d8..72ec61ee 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -329,6 +329,7 @@ static void test_serialize_uhid_create(void) { .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, .uhid_create = { .id = 42, + .name = "ABC", .report_desc_size = sizeof(report_desc), .report_desc = report_desc, }, @@ -336,12 +337,14 @@ static void test_serialize_uhid_create(void) { uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 20); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_UHID_CREATE, 0, 42, // id - 0, 11, // size + 3, // name size + 65, 66, 67, // "ABC" + 0, 11, // report desc size 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index ef71353a..d1406ed0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -131,10 +131,11 @@ public final class ControlMessage { return msg; } - public static ControlMessage createUhidCreate(int id, byte[] reportDesc) { + public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_UHID_CREATE; msg.id = id; + msg.text = name; msg.data = reportDesc; return msg; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index ef7877f1..45116935 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -75,11 +75,16 @@ public class ControlMessageReader { return value; } - private String parseString() throws IOException { - byte[] data = parseByteArray(4); + private String parseString(int sizeBytes) throws IOException { + assert sizeBytes > 0 && sizeBytes <= 4; + byte[] data = parseByteArray(sizeBytes); return new String(data, StandardCharsets.UTF_8); } + private String parseString() throws IOException { + return parseString(4); + } + private byte[] parseByteArray(int sizeBytes) throws IOException { int len = parseBufferLength(sizeBytes); byte[] data = new byte[len]; @@ -134,8 +139,9 @@ public class ControlMessageReader { private ControlMessage parseUhidCreate() throws IOException { int id = dis.readUnsignedShort(); + String name = parseString(1); byte[] data = parseByteArray(2); - return ControlMessage.createUhidCreate(id, data); + return ControlMessage.createUhidCreate(id, name, data); } private ControlMessage parseUhidInput() throws IOException { 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 e656cbb6..38251655 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -210,7 +210,7 @@ public class Controller implements AsyncProcessor { device.rotateDevice(); break; case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); 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 408dbf5d..d8cfd81f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.StringUtils; import android.os.Build; import android.os.HandlerThread; @@ -46,7 +47,7 @@ public final class UhidManager { } } - public void open(int id, byte[] reportDesc) throws IOException { + public void open(int id, String name, byte[] reportDesc) throws IOException { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { @@ -56,7 +57,7 @@ public final class UhidManager { close(old); } - byte[] req = buildUhidCreate2Req(reportDesc); + byte[] req = buildUhidCreate2Req(name, reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); @@ -146,7 +147,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -171,8 +172,14 @@ public final class UhidManager { byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); - buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII)); - buf.put(empty, 0, 256 - "scrcpy".length()); + + String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name; + byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); + assert len <= 127; + buf.put(utf8Name, 0, len); + buf.put(empty, 0, 256 - len); + buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(0); // vendor id diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 0fd5f0ac..f29be2f4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -324,8 +324,10 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); dos.writeShort(42); // id + dos.writeByte(3); // name size + dos.write("ABC".getBytes(StandardCharsets.US_ASCII)); byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; - dos.writeShort(data.length); // size + dos.writeShort(data.length); // report desc size dos.write(data); byte[] packet = bos.toByteArray(); @@ -335,6 +337,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); + Assert.assertEquals("ABC", event.getText()); Assert.assertArrayEquals(data, event.getData()); Assert.assertEquals(-1, bis.read()); // EOS From bf2b679e705ed0864e765b1de5f238799a09ece7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 57/67] Simplify UHID outputs routing There was a registration mechanism to listen to HID outputs with a specific HID id. However, the UHID gamepad processor handles several ids, so it cannot work. We could complexify the registration mechanism, but instead, directly dispatch to the expected processor based on the UHID id. Concretely, instead of passing a sc_uhid_devices instance to construct a sc_keyboard_uhid, so that it can register itself, construct the sc_uhid_devices with all the UHID instances (currently only sc_keyboard_uhid) so that it can dispatch HID outputs directly. PR #5270 --- app/src/receiver.c | 10 ++-------- app/src/scrcpy.c | 15 ++++++++++----- app/src/uhid/keyboard_uhid.c | 23 ++++++----------------- app/src/uhid/keyboard_uhid.h | 9 +++++---- app/src/uhid/uhid_output.c | 30 ++++++++++++++++-------------- app/src/uhid/uhid_output.h | 30 ++++++------------------------ 6 files changed, 45 insertions(+), 72 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 42682cb4..15cd05df 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -67,14 +67,8 @@ task_uhid_output(void *userdata) { struct sc_uhid_output_task_data *data = userdata; - struct sc_uhid_receiver *uhid_receiver = - sc_uhid_devices_get_receiver(data->uhid_devices, data->id); - if (uhid_receiver) { - uhid_receiver->ops->process_output(uhid_receiver, data->data, - data->size); - } else { - LOGW("No UHID receiver for id %" PRIu16, data->id); - } + sc_uhid_devices_process_hid_output(data->uhid_devices, data->id, data->data, + data->size); free(data->data); free(data); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 71e64344..687a9f34 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -402,7 +402,6 @@ scrcpy(struct scrcpy_options *options) { bool timeout_started = false; struct sc_acksync *acksync = NULL; - struct sc_uhid_devices *uhid_devices = NULL; uint32_t scid = scrcpy_generate_scid(); @@ -725,6 +724,8 @@ aoa_complete: assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif + struct sc_keyboard_uhid *uhid_keyboard = NULL; + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, options->key_inject_mode, @@ -732,14 +733,12 @@ aoa_complete: kp = &s->keyboard_sdk.key_processor; } else if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_UHID) { - sc_uhid_devices_init(&s->uhid_devices); - bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller, - &s->uhid_devices); + bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller); if (!ok) { goto end; } - uhid_devices = &s->uhid_devices; kp = &s->keyboard_uhid.key_processor; + uhid_keyboard = &s->keyboard_uhid; } if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { @@ -759,6 +758,12 @@ aoa_complete: gp = &s->gamepad_uhid.gamepad_processor; } + struct sc_uhid_devices *uhid_devices = NULL; + if (uhid_keyboard) { + sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard); + uhid_devices = &s->uhid_devices; + } + sc_controller_configure(&s->controller, acksync, uhid_devices); if (!sc_controller_start(&s->controller)) { diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 9fdf4def..496da23d 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -95,21 +95,19 @@ sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { return mod; } -static void -sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, - const uint8_t *data, size_t len) { +void +sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, + const uint8_t *data, size_t size) { assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - assert(len); + assert(size); // Also check at runtime (do not trust the server) - if (!len) { + if (!size) { LOGE("Unexpected empty HID output message"); return; } - struct sc_keyboard_uhid *kb = DOWNCAST_RECEIVER(receiver); - uint8_t hid_led = data[0]; uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); kb->device_mod = device_mod; @@ -117,8 +115,7 @@ sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller, - struct sc_uhid_devices *uhid_devices) { + struct sc_controller *controller) { sc_hid_keyboard_init(&kb->hid); kb->controller = controller; @@ -137,14 +134,6 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, kb->key_processor.hid = true; kb->key_processor.ops = &ops; - static const struct sc_uhid_receiver_ops uhid_receiver_ops = { - .process_output = sc_uhid_receiver_process_output, - }; - - kb->uhid_receiver.id = SC_HID_ID_KEYBOARD; - kb->uhid_receiver.ops = &uhid_receiver_ops; - sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); - struct sc_hid_open hid_open; sc_hid_keyboard_generate_open(&hid_open); assert(hid_open.hid_id == SC_HID_ID_KEYBOARD); diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h index 639a3384..1628a678 100644 --- a/app/src/uhid/keyboard_uhid.h +++ b/app/src/uhid/keyboard_uhid.h @@ -7,12 +7,10 @@ #include "controller.h" #include "hid/hid_keyboard.h" -#include "uhid/uhid_output.h" #include "trait/key_processor.h" struct sc_keyboard_uhid { struct sc_key_processor key_processor; // key processor trait - struct sc_uhid_receiver uhid_receiver; struct sc_hid_keyboard hid; struct sc_controller *controller; @@ -21,7 +19,10 @@ struct sc_keyboard_uhid { bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller, - struct sc_uhid_devices *uhid_devices); + struct sc_controller *controller); + +void +sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, + const uint8_t *data, size_t size); #endif diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index 3b095faf..05e691da 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,25 +1,27 @@ #include "uhid_output.h" #include +#include + +#include "uhid/keyboard_uhid.h" +#include "util/log.h" void -sc_uhid_devices_init(struct sc_uhid_devices *devices) { - devices->count = 0; +sc_uhid_devices_init(struct sc_uhid_devices *devices, + struct sc_keyboard_uhid *keyboard) { + devices->keyboard = keyboard; } void -sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, - struct sc_uhid_receiver *receiver) { - assert(devices->count < SC_UHID_MAX_RECEIVERS); - devices->receivers[devices->count++] = receiver; -} - -struct sc_uhid_receiver * -sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id) { - for (size_t i = 0; i < devices->count; ++i) { - if (devices->receivers[i]->id == id) { - return devices->receivers[i]; +sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, + const uint8_t *data, size_t size) { + if (id == SC_HID_ID_KEYBOARD) { + if (devices->keyboard) { + sc_keyboard_uhid_process_hid_output(devices->keyboard, data, size); + } else { + LOGW("Unexpected keyboard HID output without UHID keyboard"); } + } else { + LOGW("HID output ignored for id %" PRIu16, id); } - return NULL; } diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index e13eed87..cd6a800f 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -9,37 +9,19 @@ /** * The communication with UHID devices is bidirectional. * - * This component manages the registration of receivers to handle UHID output - * messages (sent from the device to the computer). + * This component dispatches HID outputs to the expected processor. */ -struct sc_uhid_receiver { - uint16_t id; - - const struct sc_uhid_receiver_ops *ops; -}; - -struct sc_uhid_receiver_ops { - void - (*process_output)(struct sc_uhid_receiver *receiver, - const uint8_t *data, size_t len); -}; - -#define SC_UHID_MAX_RECEIVERS 1 - struct sc_uhid_devices { - struct sc_uhid_receiver *receivers[SC_UHID_MAX_RECEIVERS]; - unsigned count; + struct sc_keyboard_uhid *keyboard; }; void -sc_uhid_devices_init(struct sc_uhid_devices *devices); +sc_uhid_devices_init(struct sc_uhid_devices *devices, + struct sc_keyboard_uhid *keyboard); void -sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, - struct sc_uhid_receiver *receiver); - -struct sc_uhid_receiver * -sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id); +sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, + const uint8_t *data, size_t size); #endif From 9f3d51106d026e9f234477d7600fcb6162edf4bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 21:58:11 +0200 Subject: [PATCH 58/67] Remove fragile assert() The sc_uhid_devices instance is initialized only when there is a UHID keyboard. The device message receiver assumed that it could not receive HID output reports without a sc_uhid_devices instance (i.e. without a UHID keyboard), but in practice, a UHID driver implementation on the device may decide to send UHID output reports for mouse or for gamepads (and we must just ignore them). So remove the assert(). PR #5270 --- app/src/receiver.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 15cd05df..b89b0c6e 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -121,11 +121,6 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } } - // This is a programming error to receive this message if there is - // no uhid_devices instance - assert(receiver->uhid_devices); - - // Also check at runtime (do not trust the server) if (!receiver->uhid_devices) { LOGE("Received unexpected HID output message"); sc_device_msg_destroy(msg); From 91d40c75483592eae78c5702c4e7278370cd06c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Sep 2024 18:24:29 +0200 Subject: [PATCH 59/67] Fix link in OTG documentation PR #5270 --- doc/otg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/otg.md b/doc/otg.md index 5f42ac9c..93002f14 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -6,7 +6,7 @@ was a [physical keyboard] and/or a [physical mouse] connected to the Android device (see [keyboard](keyboard.md) and [mouse](mouse.md)). [physical keyboard]: keyboard.md#physical-keyboard-simulation -[physical mouse]: physical-keyboard-simulation +[physical mouse]: mouse.md#physical-mouse-simulation A special mode (OTG) allows to control the device using AOA [keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at From 0ba430a462f778d1dc9de1a82b527f1570cc8cf3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Sep 2024 18:25:50 +0200 Subject: [PATCH 60/67] Add gamepad user documentation Mainly copied and adapted from HID keyboard and mouse documentation. PR #5270 --- README.md | 9 +++++++++ doc/gamepad.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ doc/otg.md | 27 ++++++++++++++++--------- 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 doc/gamepad.md diff --git a/README.md b/README.md index 67fdf364..0d44228e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Its features include: - [camera mirroring](doc/camera.md) (Android 12+) - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID) + - [gamepad](doc/gamepad.md) support - [OTG mode](doc/otg.md) - and more… @@ -111,6 +112,13 @@ Here are just some common examples. scrcpy --otg ``` + - Control the device using gamepad controllers plugged into the computer: + + ```bash + scrcpy --gamepad=uhid + scrcpy -G # short version + ``` + ## User documentation The application provides a lot of features and configuration options. They are @@ -122,6 +130,7 @@ documented in the following pages: - [Control](doc/control.md) - [Keyboard](doc/keyboard.md) - [Mouse](doc/mouse.md) + - [Gamepad](doc/gamepad.md) - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) diff --git a/doc/gamepad.md b/doc/gamepad.md new file mode 100644 index 00000000..f78fb828 --- /dev/null +++ b/doc/gamepad.md @@ -0,0 +1,53 @@ +# Gamepad + +Several gamepad input modes are available: + + - `--gamepad=disabled` (default) + - `--gamepad=uhid` (or `-G`): simulates physical HID gamepads using the UHID + kernel module on the device + - `--gamepad=aoa`: simulates physical HID gamepads using the AOAv2 protocol + + +## Physical gamepad simulation + +Two modes allow to simulate physical HID gamepads on the device, one for each +physical gamepad plugged into the computer. + + +### UHID + +This mode simulates physical HID gamepads using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID gamepads, use: + +```bash +scrcpy --gamepad=uhid +scrcpy -G # short version +``` + + +### AOA + +This mode simulates physical HID gamepads using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA gamepads, use: + +```bash +scrcpy --gamepad=aoa +``` + +Contrary to the other mode, it works at the USB level directly (so it only works +over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/otg.md b/doc/otg.md index 93002f14..7d31c0a7 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -9,13 +9,15 @@ device (see [keyboard](keyboard.md) and [mouse](mouse.md)). [physical mouse]: mouse.md#physical-mouse-simulation A special mode (OTG) allows to control the device using AOA -[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at -all (so USB debugging is not necessary). In this mode, video and audio are -disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set. +[keyboard](keyboard.md#aoa), [mouse](mouse.md#aoa) and +[gamepad](gamepad.md#aoa), without using _adb_ at all (so USB debugging is not +necessary). In this mode, video and audio are disabled, and `--keyboard=aoa` and +`--mouse=aoa` are implicitly set. However, gamepads are disabled by default, so +`--gamepad=aoa` (or `-G` in OTG mode) must be explicitly set. -Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse -simulation, as if the computer keyboard and mouse were plugged directly to the -device via an OTG cable. +Therefore, it is possible to run _scrcpy_ with only physical keyboard, mouse and +gamepad simulation, as if the computer keyboard, mouse and gamepads were plugged +directly to the device via an OTG cable. To enable OTG mode: @@ -32,6 +34,13 @@ scrcpy --otg --keyboard=disabled scrcpy --otg --mouse=disabled ``` +and to enable gamepads: + +```bash +scrcpy --otg --gamepad=aoa +scrcpy --otg -G # short version +``` + It only works if the device is connected over USB. ## OTG issues on Windows @@ -50,9 +59,9 @@ is enabled, then OTG mode is not necessary. Instead, disable video and audio, and select UHID (or AOA): ```bash -scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid -scrcpy --no-video --no-audio -KM # short version -scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa +scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid --gamepad=uhid +scrcpy --no-video --no-audio -KMG # short version +scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa --gamepad=aoa ``` One benefit of UHID is that it also works wirelessly. From f01a622eadebb66e2fe0da22e24e71679e20bcb5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 10 Sep 2024 09:16:05 +0200 Subject: [PATCH 61/67] Enable joystick events in background Capture the gamepads even when the window is not focused. Note: In theory, with this flag set, we could capture gamepad events even without a window (--no-window). In practice, scrcpy still requires a window, because --no-window implies --no-control, and the input manager is owned by the sc_screen instance, which does not exist if there is no window. Supporting this use case would require a lot of refactors. Refs PR #5270 Suggested-by: Luiz Henrique Laurini --- app/src/scrcpy.c | 4 ++++ app/src/usb/scrcpy_otg.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 687a9f34..854657fb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -136,6 +136,10 @@ sdl_set_hints(const char *render_driver) { if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } + + if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { + LOGW("Could not allow joystick background events"); + } } static void diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 47afd9d0..9595face 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -58,6 +58,10 @@ scrcpy_otg(struct scrcpy_options *options) { LOGW("Could not enable linear filtering"); } + if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { + LOGW("Could not allow joystick background events"); + } + // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); From befc0fac5b0f3c819ba798fc979f1a946a21976c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:32:10 +0200 Subject: [PATCH 62/67] Mention UHID permission errors UHID may not work on old Android versions due to permission errors. Mention it in UHID mouse and gamepad documentation (it was already mentioned for UHID keyboard). Refs #4473 comment PR #5270 --- doc/gamepad.md | 2 ++ doc/mouse.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/gamepad.md b/doc/gamepad.md index f78fb828..607bb935 100644 --- a/doc/gamepad.md +++ b/doc/gamepad.md @@ -28,6 +28,8 @@ scrcpy --gamepad=uhid scrcpy -G # short version ``` +Note: UHID may not work on old Android versions due to permission errors. + ### AOA diff --git a/doc/mouse.md b/doc/mouse.md index ec4aea63..ae7c6834 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -53,6 +53,8 @@ scrcpy --mouse=uhid scrcpy -M # short version ``` +Note: UHID may not work on old Android versions due to permission errors. + ### AOA From 4cc4abdcc8b3f0d76a362f450db4cefe96edca5d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:35:22 +0200 Subject: [PATCH 63/67] Mention issue with AOA and multiple gamepads Android does not support multiple HID gamepads properly over AOA. PR #5270 --- doc/gamepad.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/gamepad.md b/doc/gamepad.md index 607bb935..d3d27b51 100644 --- a/doc/gamepad.md +++ b/doc/gamepad.md @@ -50,6 +50,9 @@ It does not use the scrcpy server, and does not require `adb` (USB debugging). Therefore, it is possible to control the device (but not mirror) even with USB debugging disabled (see [OTG](otg.md)). +Note: For some reason, in this mode, Android detects multiple physical gamepads +as a single misbehaving one. Use UHID if you need multiple gamepads. + Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring (it is not possible to open a USB device if it is already open by another process like the _adb daemon_). From 337901368e7bd143c68052d4a92e809fecdfe5a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 14:40:52 +0200 Subject: [PATCH 64/67] Upgrade SDL (2.30.7) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 0a42bc1f..c8b62746 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.30.5 +VERSION=2.30.7 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf +SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5 cd "$SOURCES_DIR" From 6d23a389cab2d8678627d6743223506095729ff2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:54:37 +0200 Subject: [PATCH 65/67] Upgrade FFmpeg (7.0.2) for Windows --- app/deps/ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index ef92d4a5..89431542 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=7.0.1 +VERSION=7.0.2 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff +SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 cd "$SOURCES_DIR" From 292adf294dd654e79b42215fb6bc2c6d2d44165b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:59:27 +0200 Subject: [PATCH 66/67] Bump version to 2.7 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 9e0d90c2..6454c88e 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.6.1" + VALUE "ProductVersion", "2.7" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b532006a..f76d5ecf 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.6.1', + version: '2.7', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index decacd3f..655298a9 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20601 - versionName "2.6.1" + versionCode 20700 + versionName "2.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5ee7af30..ab6c821d 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.6.1 +SCRCPY_VERSION_NAME=2.7 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 665ccb32f5306ebd866dc0d99f4d08ed2aeb91c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 21:15:26 +0200 Subject: [PATCH 67/67] Update links to 2.7 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0d44228e..44f3d740 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.6.1) +# scrcpy (v2.7) scrcpy diff --git a/doc/build.md b/doc/build.md index 15e0ffff..63bd7ca7 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.6.1`][direct-scrcpy-server] - SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b` + - [`scrcpy-server-v2.7`][direct-scrcpy-server] + SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 65ec2b45..36e59178 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit) - SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c` - - [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit) - SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04` + - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) + SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5` + - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) + SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win64-v2.6.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win32-v2.6.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2aad8cdc..3cf3490c 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 -PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 +PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server