From 5dc5335c3b978193484cc53b06d7f911f65410ef Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Fri, 13 Nov 2026 16:18:21 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=E5=AE=BF=E4=B8=BB=E4=BE=A7=E7=82=B9?= =?UTF-8?q?=E5=87=BB=EF=BC=9Amodavd=5Ftouch=5F*=20=E8=B5=B0=E6=A8=A1?= =?UTF-8?q?=E6=8B=9F=E5=99=A8=E6=8E=A7=E5=88=B6=E5=8F=B0=E7=9A=84=E5=A4=9A?= =?UTF-8?q?=E7=82=B9=E8=A7=A6=E6=8E=A7=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 每个句柄一条控制台长连接,第一次点击时连上,断了重连一次;关闭句柄时先抬起还按着的手指。 - 按多点触控协议 B 拼 event send(槽位、跟踪编号、坐标、压力、接触面积),坐标按共享画面的宽高换算到 0–32767,和 event mouse 的换算、默认压力一致。最多 10 根手指;已按下的再按下当移动。 - 接口版本 2,能力位加 MODAVD_CAP_TOUCH;按键、文字仍是 MODAVD_E_UNSUPPORTED。 - 控制台客户端拆成打开、发送、关闭三步,一次性命令照旧。 - 新增 test_input:假控制台核对每条命令、坐标换算、重连、关闭时抬起和认证。 --- CMakeLists.txt | 19 ++- README.md | 10 +- include/modavd_extras.h | 32 +++- src/console.c | 124 +++++++++----- src/core.c | 47 +----- src/input.c | 258 ++++++++++++++++++++++++++++ src/mx_internal.h | 36 ++++ tests/test_core.c | 9 +- tests/test_input.c | 365 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 805 insertions(+), 95 deletions(-) create mode 100644 src/input.c create mode 100644 tests/test_input.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 93ac43a..0278313 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,13 +28,21 @@ if(NOT WIN32) endif() endif() -set(MX_SOURCES src/core.c src/shm.c src/console.c src/log.c) +set(MX_SOURCES src/core.c src/shm.c src/console.c src/input.c src/log.c) + +if(NOT WIN32) + # POSIX: the per-handle input lock. + find_package(Threads REQUIRED) +endif() function(mx_link_system target) if(WIN32) target_link_libraries(${target} PRIVATE ws2_32) - elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_libraries(${target} PRIVATE rt) + else() + target_link_libraries(${target} PRIVATE Threads::Threads) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_libraries(${target} PRIVATE rt) + endif() endif() endfunction() @@ -57,6 +65,11 @@ target_link_libraries(test_core PRIVATE mx_core) mx_link_system(test_core) add_test(NAME test_core COMMAND test_core) +add_executable(test_input tests/test_input.c) +target_link_libraries(test_input PRIVATE mx_core) +mx_link_system(test_input) +add_test(NAME test_input COMMAND test_input) + if(NOT WIN32) add_executable(test_open_posix tests/test_open_posix.c) target_link_libraries(test_open_posix PRIVATE mx_core) diff --git a/README.md b/README.md index cc6ede6..33d10a5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## 这是什么 -一个 C 库:从 [ModAVD](https://github.com/ModAVD/ModAVD) 实例直接取画面,不经 adb。给 MAA(MaaCore)、MaaFramework 这类自动化框架接入用。以后还会加宿主侧的点击和按键。 +一个 C 库:从 [ModAVD](https://github.com/ModAVD/ModAVD) 实例直接取画面、在宿主这边点击,不经 adb。给 MAA(MaaCore)、MaaFramework 这类自动化框架接入用。 - 画面来自模拟器自己的共享内存:`screenrecord webrtc start`(或启动参数 `-share-vid`)之后,模拟器把每一帧写进一块按控制台端口命名的共享内存。 - Windows 上叫 `SHM_videmulator<端口>`,Linux、macOS 上叫 `videmulator<端口>`; @@ -21,12 +21,13 @@ - 拷出一帧,换成调用方要的格式; - 遇到新帧写到一半时重拷; - 告诉调用方现在能不能用。 +- 点击走模拟器控制台的 `event send`,发标准的多点触控事件(槽位、跟踪编号、坐标、压力),到达模拟器自己的虚拟触屏:安卓看到的是屏幕上的手指,和在模拟器窗口里点一样,不是应用注入的事件,客体里也不用装东西。最多 10 根手指。 - 库**不自己退回 adb**:出错时返回错误码,让调用方按自己的规则换别的办法。 - 只依赖系统库:Windows 上是 `KERNEL32`、`WS2_32`,C 运行库静态链接;POSIX 上是 `shm_open`、`mmap` 和套接字。 ## 状态 -- v0.1:只有截图。点击、按键、文字的函数已经声明,现在返回 `MODAVD_E_UNSUPPORTED`。 +- 截图(接口版本 1)和点击(接口版本 2,`MODAVD_CAP_TOUCH`)。按键、文字的函数已经声明,现在返回 `MODAVD_E_UNSUPPORTED`:控制台发的按键会进触屏设备、被丢掉,`event text` 会悄悄丢掉键盘映射里没有的字。 - Windows:截图这套逻辑在 ModAVD 上已经用了一个多月,形式是下面的兼容外壳。 - macOS(Apple Silicon):11-09 有贡献者用官方 Emulator 37.2.13、API 35 ARM64 实测过截图。 - Linux:能编,单元测试在 CI 上跑;还没有在模拟器上实测。 @@ -106,11 +107,12 @@ ctest --test-dir build --output-on-failure ## English -ModAVDExtras is a small C library that reads the screen of a [ModAVD](https://github.com/ModAVD/ModAVD) instance directly, without adb, for automation frameworks such as MaaCore (MAA) and MaaFramework. Host-side touch and key input will follow. +ModAVDExtras is a small C library that reads the screen of a [ModAVD](https://github.com/ModAVD/ModAVD) instance directly, without adb, for automation frameworks such as MaaCore (MAA) and MaaFramework, and touches it from the host. - Frames come from the emulator's own shared memory: after `screenrecord webrtc start` (or `-share-vid`), the emulator writes every frame to a region named after its console port: `SHM_videmulator` on Windows, `videmulator` elsewhere. The layout is a 24-byte header (width, height, fps, frame number, timestamp) followed by BGRA pixels. - The library finds the console port from an adb address, starts frame sharing through the console when it is off, copies a frame in the format the caller asks for (copying again if a new frame arrived meanwhile), and reports whether capture is usable. It never falls back to adb by itself. -- Version 0.1 does screen capture only; the input functions are declared and return `MODAVD_E_UNSUPPORTED`. See [include/modavd_extras.h](include/modavd_extras.h). +- Touch (API version 2, `MODAVD_CAP_TOUCH`) goes through the console's `event send` as standard multi-touch events (slot, tracking id, position, pressure) to the emulator's own virtual touch screen: Android sees a finger on the screen, as with a click on the emulator window, not an injected event, and nothing is installed in the guest. Up to 10 contacts. +- Keys and text are declared and return `MODAVD_E_UNSUPPORTED`: keys sent through the console reach the touch screen device and are dropped, and `event text` silently drops characters missing from the keyboard map. See [include/modavd_extras.h](include/modavd_extras.h). - `external_renderer_ipc.dll` is a Windows-only compatibility shell. It exports the four capture functions that MaaCore looks up in MuMu's DLL, on top of the same code, so MaaCore versions that do not know ModAVD can still capture from it. AUTO-MAS uses it today. It will be retired once MaaCore can connect to ModAVD directly. - Status: Windows has used this capture code for over a month through the shell below; on Apple Silicon macOS a contributor tested capture with the official Emulator 37.2.13 and API 35 ARM64; Linux builds and passes the unit tests in CI but has not been tested with an emulator. From v0.1.2 the [releases](https://github.com/ModAVD/ModAVDExtras/releases) carry `modavd_extras.dll` (Windows x64), `libmodavd_extras.dylib` (macOS arm64) and `libmodavd_extras.so` (Linux x64), plus the Windows-only shell. - Build: `build.ps1` on Windows (Visual Studio 2022 Build Tools, CMake, Ninja); CMake on Linux and macOS. The unit tests need no emulator. diff --git a/include/modavd_extras.h b/include/modavd_extras.h index 735e1ac..c76c11b 100644 --- a/include/modavd_extras.h +++ b/include/modavd_extras.h @@ -1,5 +1,5 @@ /* - * ModAVDExtras: screen capture (and, later, input) for ModAVD instances, + * ModAVDExtras: screen capture and touch input for ModAVD instances, * for automation frameworks that want to skip adb. * * Copyright 2026 qiyinxi @@ -37,8 +37,10 @@ extern "C" { #define MODAVD_API __attribute__((visibility("default"))) #endif -/* Bumped whenever a function or field is added. Nothing is ever removed. */ -#define MODAVD_API_VERSION 1 +/* Bumped whenever a function or field is added, or a declared one starts to work. + * Nothing is ever removed. + * 1: capture. 2: touch (modavd_touch_*). */ +#define MODAVD_API_VERSION 2 typedef struct modavd_instance* modavd_handle; @@ -57,7 +59,7 @@ enum { /* modavd_capabilities() */ enum { MODAVD_CAP_SCREENCAP = 1u << 0, - MODAVD_CAP_TOUCH = 1u << 1, /* not yet */ + MODAVD_CAP_TOUCH = 1u << 1, /* API version 2 */ MODAVD_CAP_KEY = 1u << 2, /* not yet */ MODAVD_CAP_TEXT = 1u << 3, /* not yet */ }; @@ -154,14 +156,30 @@ MODAVD_API int modavd_console_port(modavd_handle h); /* > 0, or MODAVD_E_IN MODAVD_API int modavd_capture(modavd_handle h, int display, uint32_t format, uint32_t flags, void* buf, size_t buf_size, modavd_frame_info* info); -/* ---- input: declared for the next version, MODAVD_E_UNSUPPORTED for now ---- */ +/* ---- touch (API version 2, MODAVD_CAP_TOUCH) ---- */ -/* Coordinates are display pixels; contact is 0-based (at most 10 at once). */ +/* Touches on the emulator's own touch screen, sent through its console: Android + * sees them as a finger on the screen, not as injected events. Needs nothing in + * the guest. The console session opens on the first call and reopens once if it + * broke (the emulator restarted). + * + * Coordinates are display pixels of the shared frame (clamped to it); contact is + * 0-based, at most 10 at once; pressure 0 = the default (what a mouse click on + * the emulator window uses). Only display 0. + * - touch_down on a contact that is already down moves it. + * - touch_move on a contact that is not down: MODAVD_E_INVALID. + * - touch_up on a contact that is not down sends nothing and returns MODAVD_OK. + * MODAVD_E_NOT_READY: no frame was published yet, so the display size is unknown. + * MODAVD_E_NOT_FOUND: the console does not answer. MODAVD_E_IO: auth failed or + * the console refused the command. */ MODAVD_API int modavd_touch_down(modavd_handle h, int display, int contact, int x, int y, int pressure); MODAVD_API int modavd_touch_move(modavd_handle h, int display, int contact, int x, int y, int pressure); MODAVD_API int modavd_touch_up(modavd_handle h, int display, int contact); -/* Lifts every contact this handle pressed. modavd_close() will do the same. */ +/* Lifts every contact this handle pressed. modavd_close() does the same. */ MODAVD_API int modavd_touch_release_all(modavd_handle h); + +/* ---- keys and text: declared, MODAVD_E_UNSUPPORTED for now ---- */ + /* Android key code (KEYCODE_*); down = 1 press, 0 release. */ MODAVD_API int modavd_key(modavd_handle h, int display, int android_keycode, int down); MODAVD_API int modavd_input_text(modavd_handle h, const char* utf8); diff --git a/src/console.c b/src/console.c index a34b643..56e440a 100644 --- a/src/console.c +++ b/src/console.c @@ -1,6 +1,7 @@ /* * A minimal client for the emulator console (telnet-style text on 127.0.0.1). - * Version 0 only uses it to start frame sharing; input will use it too. + * Used to start frame sharing (one-off connections) and to send touch input + * (a session kept open per handle). * * Copyright 2026 qiyinxi * SPDX-License-Identifier: LGPL-3.0-or-later @@ -97,8 +98,9 @@ static int send_line(mx_socket s, const char* line) return send(s, cmd, n, MX_SEND_FLAGS) == n ? 0 : -1; } -int mx_console_command(int port, const char* command, int timeout_ms) +int mx_console_open(int port, int timeout_ms, mx_console* c) { + c->sock = (intptr_t)MX_BAD_SOCKET; char token[256]; read_auth_token(token, sizeof(token)); @@ -108,55 +110,101 @@ int mx_console_command(int port, const char* command, int timeout_ms) return MODAVD_E_IO; } #endif - int rc = MODAVD_E_NOT_FOUND; mx_socket s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (s != MX_BAD_SOCKET) { + if (s == MX_BAD_SOCKET) { #ifdef _WIN32 - DWORD tv = (DWORD)timeout_ms; + WSACleanup(); +#endif + return MODAVD_E_IO; + } +#ifdef _WIN32 + DWORD tv = (DWORD)timeout_ms; #else - struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; + struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; #endif - setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); #if defined(SO_NOSIGPIPE) - int one = 1; - setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); + int one = 1; + setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); #endif - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons((unsigned short)port); - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - char buf[4096]; - if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0 && read_until_ok(s, buf, sizeof(buf)) == 1) { - int authed = 1; - if (token[0]) { - char auth[300]; - snprintf(auth, sizeof(auth), "auth %s", token); - authed = send_line(s, auth) == 0 && read_until_ok(s, buf, sizeof(buf)) == 1; - } - if (!authed) { - mx_log(MODAVD_LOG_WARN, "console %d auth failed", port); - rc = MODAVD_E_IO; - } - else { - int ok = send_line(s, command) == 0 && read_until_ok(s, buf, sizeof(buf)) == 1; - mx_log(MODAVD_LOG_INFO, "console %d %s -> %s", port, command, ok ? "OK" : "fail"); - rc = ok ? MODAVD_OK : MODAVD_E_IO; - } - send_line(s, "quit"); - } - else { + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + char buf[4096]; + int rc = MODAVD_OK; + if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) != 0 || read_until_ok(s, buf, sizeof(buf)) != 1) { #ifdef _WIN32 - int err = WSAGetLastError(); + int err = WSAGetLastError(); #else - int err = 0; + int err = 0; #endif - mx_log(MODAVD_LOG_WARN, "console %d connect failed (%d)", port, err); + mx_log(MODAVD_LOG_WARN, "console %d connect failed (%d)", port, err); + rc = MODAVD_E_NOT_FOUND; + } + else if (token[0]) { + char auth[300]; + snprintf(auth, sizeof(auth), "auth %s", token); + if (send_line(s, auth) != 0 || read_until_ok(s, buf, sizeof(buf)) != 1) { + mx_log(MODAVD_LOG_WARN, "console %d auth failed", port); + rc = MODAVD_E_IO; } + } + if (rc != MODAVD_OK) { mx_close_socket(s); +#ifdef _WIN32 + WSACleanup(); +#endif + return rc; + } + c->sock = (intptr_t)s; + return MODAVD_OK; +} + +int mx_console_send(mx_console* c, const char* command) +{ + if (c->sock == (intptr_t)MX_BAD_SOCKET) { + return MODAVD_E_NOT_FOUND; + } + mx_socket s = (mx_socket)c->sock; + char buf[4096]; + if (send_line(s, command) != 0) { + mx_console_close(c); + return MODAVD_E_NOT_FOUND; } + int r = read_until_ok(s, buf, sizeof(buf)); + if (r < 0) { + /* The emulator went away or stopped answering: the session is unusable. */ + mx_console_close(c); + return MODAVD_E_NOT_FOUND; + } + return r == 1 ? MODAVD_OK : MODAVD_E_IO; +} + +void mx_console_close(mx_console* c) +{ + if (c->sock == (intptr_t)MX_BAD_SOCKET) { + return; + } + mx_socket s = (mx_socket)c->sock; + send_line(s, "quit"); + mx_close_socket(s); + c->sock = (intptr_t)MX_BAD_SOCKET; #ifdef _WIN32 WSACleanup(); #endif - return rc; +} + +int mx_console_command(int port, const char* command, int timeout_ms) +{ + mx_console c; + int rc = mx_console_open(port, timeout_ms, &c); + if (rc != MODAVD_OK) { + return rc; + } + rc = mx_console_send(&c, command); + mx_log(MODAVD_LOG_INFO, "console %d %s -> %s", port, command, rc == MODAVD_OK ? "OK" : "fail"); + mx_console_close(&c); + return rc == MODAVD_OK ? MODAVD_OK : MODAVD_E_IO; } diff --git a/src/core.c b/src/core.c index 84c9d5e..e2039e2 100644 --- a/src/core.c +++ b/src/core.c @@ -133,10 +133,14 @@ int modavd_open(const modavd_open_params* params, modavd_handle* out) } struct modavd_instance* inst = (struct modavd_instance*)calloc(1, sizeof(*inst)); - if (!inst) { + struct mx_input* input = mx_input_create(port); + if (!inst || !input) { + free(inst); + mx_input_destroy(input); mx_shm_close(&shm); return MODAVD_E_IO; } + inst->input = input; inst->console_port = port; inst->simd = mx_cpu_simd(); inst->shm = shm; @@ -152,11 +156,13 @@ void modavd_close(modavd_handle h) if (!h) { return; } + /* Lifts the contacts this handle left down. */ + mx_input_destroy(h->input); mx_shm_close(&h->shm); free(h); } -uint32_t modavd_capabilities(modavd_handle h) { return h ? MODAVD_CAP_SCREENCAP : 0; } +uint32_t modavd_capabilities(modavd_handle h) { return h ? MODAVD_CAP_SCREENCAP | MODAVD_CAP_TOUCH : 0; } int modavd_console_port(modavd_handle h) { return h ? h->console_port : MODAVD_E_INVALID; } @@ -364,40 +370,3 @@ int modavd_capture(modavd_handle h, int display, uint32_t format, uint32_t flags } return rc; } - -/* Input arrives in the next version. */ -int modavd_touch_down(modavd_handle h, int display, int contact, int x, int y, int pressure) -{ - (void)h, (void)display, (void)contact, (void)x, (void)y, (void)pressure; - return MODAVD_E_UNSUPPORTED; -} - -int modavd_touch_move(modavd_handle h, int display, int contact, int x, int y, int pressure) -{ - (void)h, (void)display, (void)contact, (void)x, (void)y, (void)pressure; - return MODAVD_E_UNSUPPORTED; -} - -int modavd_touch_up(modavd_handle h, int display, int contact) -{ - (void)h, (void)display, (void)contact; - return MODAVD_E_UNSUPPORTED; -} - -int modavd_touch_release_all(modavd_handle h) -{ - (void)h; - return MODAVD_E_UNSUPPORTED; -} - -int modavd_key(modavd_handle h, int display, int android_keycode, int down) -{ - (void)h, (void)display, (void)android_keycode, (void)down; - return MODAVD_E_UNSUPPORTED; -} - -int modavd_input_text(modavd_handle h, const char* utf8) -{ - (void)h, (void)utf8; - return MODAVD_E_UNSUPPORTED; -} diff --git a/src/input.c b/src/input.c new file mode 100644 index 0000000..a305e68 --- /dev/null +++ b/src/input.c @@ -0,0 +1,258 @@ +/* + * Touch input through the emulator console: "event send" with raw multi-touch + * events (protocol B: slot, tracking id, position, pressure), which the + * emulator hands to the virtio touch screen of display 0 as they are. They + * reach Android as touches on that screen, like a finger on the window, not + * as events injected by an app. + * + * Copyright 2026 qiyinxi + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +#include +#include +#include + +#include "mx_internal.h" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +typedef SRWLOCK mx_mutex; +#define mx_mutex_init(m) InitializeSRWLock(m) +#define mx_mutex_destroy(m) ((void)(m)) +#define mx_mutex_lock(m) AcquireSRWLockExclusive(m) +#define mx_mutex_unlock(m) ReleaseSRWLockExclusive(m) +#else +#include +typedef pthread_mutex_t mx_mutex; +#define mx_mutex_init(m) pthread_mutex_init((m), NULL) +#define mx_mutex_destroy(m) pthread_mutex_destroy(m) +#define mx_mutex_lock(m) pthread_mutex_lock(m) +#define mx_mutex_unlock(m) pthread_mutex_unlock(m) +#endif + +/* Linux input codes (input-event-codes.h); the console takes numbers as well as names. */ +#define EV_SYN_ 0 +#define EV_ABS_ 3 +#define ABS_MT_SLOT_ 0x2f +#define ABS_MT_TOUCH_MAJOR_ 0x30 +#define ABS_MT_TOUCH_MINOR_ 0x31 +#define ABS_MT_POSITION_X_ 0x35 +#define ABS_MT_POSITION_Y_ 0x36 +#define ABS_MT_TRACKING_ID_ 0x39 +#define ABS_MT_PRESSURE_ 0x3a + +#define MX_INPUT_TIMEOUT_MS 1000 + +struct mx_input { + mx_mutex lock; + int port; + mx_console console; + uint32_t down; /* bit n: contact n is pressed */ +}; + +int mx_touch_axis(int v, int size) +{ + if (size <= 0 || v <= 0) { + return 0; + } + if (v >= size) { + v = size - 1; + } + /* qemu_input_scale_axis(v, 0, size, 0, MX_ABS_MAX), as for "event mouse" */ + return (int)((int64_t)v * MX_ABS_MAX / size); +} + +int mx_touch_command(char* buf, size_t cap, int action, int contact, int ax, int ay, int pressure) +{ + int n = -1; + switch (action) { + case MX_TOUCH_DOWN: + n = snprintf(buf, cap, "event send %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:0", + EV_ABS_, ABS_MT_SLOT_, contact, EV_ABS_, ABS_MT_TRACKING_ID_, contact, EV_ABS_, + ABS_MT_POSITION_X_, ax, EV_ABS_, ABS_MT_POSITION_Y_, ay, EV_ABS_, ABS_MT_PRESSURE_, pressure, + EV_ABS_, ABS_MT_TOUCH_MAJOR_, MX_TOUCH_SIZE, EV_ABS_, ABS_MT_TOUCH_MINOR_, MX_TOUCH_SIZE, EV_SYN_, + 0); + break; + case MX_TOUCH_MOVE: + n = snprintf(buf, cap, "event send %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:%d %d:%d:0", EV_ABS_, ABS_MT_SLOT_, + contact, EV_ABS_, ABS_MT_POSITION_X_, ax, EV_ABS_, ABS_MT_POSITION_Y_, ay, EV_ABS_, + ABS_MT_PRESSURE_, pressure, EV_SYN_, 0); + break; + case MX_TOUCH_UP: + n = snprintf(buf, cap, "event send %d:%d:%d %d:%d:0 %d:%d:-1 %d:%d:0", EV_ABS_, ABS_MT_SLOT_, contact, EV_ABS_, + ABS_MT_PRESSURE_, EV_ABS_, ABS_MT_TRACKING_ID_, EV_SYN_, 0); + break; + default: break; + } + return (n < 0 || (size_t)n >= cap) ? -1 : n; +} + +struct mx_input* mx_input_create(int console_port) +{ + struct mx_input* in = (struct mx_input*)calloc(1, sizeof(*in)); + if (!in) { + return NULL; + } + mx_mutex_init(&in->lock); + in->port = console_port; + in->console.sock = -1; + return in; +} + +/* Sends one command on the session, opening it first and once more if it broke + * (the emulator restarted, or closed an idle session). Caller holds the lock. */ +static int send_locked(struct mx_input* in, const char* command) +{ + for (int attempt = 0; attempt < 2; ++attempt) { + if (in->console.sock == -1) { + int rc = mx_console_open(in->port, MX_INPUT_TIMEOUT_MS, &in->console); + if (rc != MODAVD_OK) { + return rc; + } + } + int rc = mx_console_send(&in->console, command); + if (rc != MODAVD_E_NOT_FOUND) { + if (rc != MODAVD_OK) { + mx_log(MODAVD_LOG_WARN, "console %d: %s -> KO", in->port, command); + } + return rc; + } + } + return MODAVD_E_NOT_FOUND; +} + +/* Lifts every pressed contact. Caller holds the lock. */ +static int release_all_locked(struct mx_input* in) +{ + int result = MODAVD_OK; + for (int c = 0; c < MX_MAX_CONTACTS; ++c) { + if (!(in->down & (1u << c))) { + continue; + } + char cmd[256]; + mx_touch_command(cmd, sizeof(cmd), MX_TOUCH_UP, c, 0, 0, 0); + int rc = send_locked(in, cmd); + if (rc != MODAVD_OK) { + result = rc; + } + in->down &= ~(1u << c); + } + return result; +} + +void mx_input_destroy(struct mx_input* in) +{ + if (!in) { + return; + } + mx_mutex_lock(&in->lock); + if (in->down) { + release_all_locked(in); + } + mx_console_close(&in->console); + mx_mutex_unlock(&in->lock); + mx_mutex_destroy(&in->lock); + free(in); +} + +/* Display size from the shared frame header; 0 when nothing was published. */ +static void display_size(modavd_handle h, int* w, int* hgt) +{ + const volatile mx_video_info* vi = (const volatile mx_video_info*)h->shm.view; + *w = vi ? (int)vi->width : 0; + *hgt = vi ? (int)vi->height : 0; +} + +static int touch(modavd_handle h, int display, int action, int contact, int x, int y, int pressure) +{ + if (!h || !h->input) { + return MODAVD_E_INVALID; + } + if (display != 0) { + return MODAVD_E_UNSUPPORTED; + } + if (contact < 0 || contact >= MX_MAX_CONTACTS) { + return MODAVD_E_INVALID; + } + struct mx_input* in = h->input; + uint32_t bit = 1u << contact; + char cmd[256]; + int rc = MODAVD_OK; + + mx_mutex_lock(&in->lock); + if (action == MX_TOUCH_UP) { + /* Lifting a contact that is not down sends nothing. */ + if (in->down & bit) { + mx_touch_command(cmd, sizeof(cmd), MX_TOUCH_UP, contact, 0, 0, 0); + rc = send_locked(in, cmd); + in->down &= ~bit; + } + mx_mutex_unlock(&in->lock); + return rc; + } + + int w = 0, hgt = 0; + display_size(h, &w, &hgt); + if (w <= 0 || hgt <= 0) { + mx_mutex_unlock(&in->lock); + return MODAVD_E_NOT_READY; + } + if (action == MX_TOUCH_MOVE && !(in->down & bit)) { + mx_mutex_unlock(&in->lock); + return MODAVD_E_INVALID; + } + if (pressure <= 0 || pressure > MX_TOUCH_PRESSURE) { + pressure = MX_TOUCH_PRESSURE; + } + /* Pressing a contact that is already down moves it, as MuMu's library does. */ + int kind = (action == MX_TOUCH_DOWN && !(in->down & bit)) ? MX_TOUCH_DOWN : MX_TOUCH_MOVE; + mx_touch_command(cmd, sizeof(cmd), kind, contact, mx_touch_axis(x, w), mx_touch_axis(y, hgt), pressure); + rc = send_locked(in, cmd); + if (rc == MODAVD_OK && kind == MX_TOUCH_DOWN) { + in->down |= bit; + } + mx_mutex_unlock(&in->lock); + return rc; +} + +int modavd_touch_down(modavd_handle h, int display, int contact, int x, int y, int pressure) +{ + return touch(h, display, MX_TOUCH_DOWN, contact, x, y, pressure); +} + +int modavd_touch_move(modavd_handle h, int display, int contact, int x, int y, int pressure) +{ + return touch(h, display, MX_TOUCH_MOVE, contact, x, y, pressure); +} + +int modavd_touch_up(modavd_handle h, int display, int contact) +{ + return touch(h, display, MX_TOUCH_UP, contact, 0, 0, 0); +} + +int modavd_touch_release_all(modavd_handle h) +{ + if (!h || !h->input) { + return MODAVD_E_INVALID; + } + mx_mutex_lock(&h->input->lock); + int rc = release_all_locked(h->input); + mx_mutex_unlock(&h->input->lock); + return rc; +} + +/* The console's "event send" delivers keys to the touch screen device, which + * drops them, and "event text" silently drops what the keyboard map lacks: + * neither is reliable enough yet. */ +int modavd_key(modavd_handle h, int display, int android_keycode, int down) +{ + (void)h, (void)display, (void)android_keycode, (void)down; + return MODAVD_E_UNSUPPORTED; +} + +int modavd_input_text(modavd_handle h, const char* utf8) +{ + (void)h, (void)utf8; + return MODAVD_E_UNSUPPORTED; +} diff --git a/src/mx_internal.h b/src/mx_internal.h index 60618f2..73be13e 100644 --- a/src/mx_internal.h +++ b/src/mx_internal.h @@ -73,11 +73,47 @@ void mx_shm_close(mx_shm* shm); * MODAVD_E_IO: auth failed, the command answered KO, or a timeout. */ int mx_console_command(int port, const char* command, int timeout_ms); +/* A console session kept open across commands. sock is the platform socket, + * -1 (INVALID_SOCKET) when closed. */ +typedef struct mx_console { + intptr_t sock; +} mx_console; + +/* Connects and authenticates. MODAVD_OK, MODAVD_E_NOT_FOUND (could not + * connect) or MODAVD_E_IO (auth failed). timeout_ms bounds each reply. */ +int mx_console_open(int port, int timeout_ms, mx_console* c); +/* MODAVD_OK, MODAVD_E_IO (the console answered KO) or MODAVD_E_NOT_FOUND + * (the session broke; it is closed and may be opened again). */ +int mx_console_send(mx_console* c, const char* command); +/* Says quit and closes; nothing happens when already closed. */ +void mx_console_close(mx_console* c); + +/* ---- input.c ---- */ +#define MX_MAX_CONTACTS 10 +#define MX_ABS_MAX 32767 /* the virtio touch screen's axis range (INPUT_EVENT_ABS_MAX) */ +#define MX_TOUCH_PRESSURE 0x400 /* what "event mouse" presses with (MTS_PRESSURE_RANGE_MAX) */ +#define MX_TOUCH_SIZE 0x500 /* ABS_MT_TOUCH_MAJOR / MINOR of "event mouse" */ + +enum { MX_TOUCH_DOWN = 0, MX_TOUCH_MOVE = 1, MX_TOUCH_UP = 2 }; + +/* A display pixel coordinate (0 .. size-1, clamped) on the touch screen's + * 0 .. MX_ABS_MAX axis, scaled as the emulator scales "event mouse". */ +int mx_touch_axis(int v, int size); +/* Writes the console command for one touch action on contact (slot) with + * axis coordinates ax, ay. Returns the length, or -1 if it does not fit. */ +int mx_touch_command(char* buf, size_t cap, int action, int contact, int ax, int ay, int pressure); + +struct mx_input; /* lock, console session and pressed contacts of one handle */ +struct mx_input* mx_input_create(int console_port); +/* Lifts the contacts still down, closes the session and frees. */ +void mx_input_destroy(struct mx_input* in); + /* ---- core.c ---- */ struct modavd_instance { int console_port; int simd; /* mx_cpu_simd() at open */ mx_shm shm; + struct mx_input* input; }; /* 1 when this CPU runs the vector row conversion (SSSE3 on x86, NEON on diff --git a/tests/test_core.c b/tests/test_core.c index 4149bc9..05b8ec1 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -157,7 +157,7 @@ static void test_capture_from_fake_frame(void) return; } CHECK_EQ(modavd_console_port(h), port); - CHECK_EQ(modavd_capabilities(h), MODAVD_CAP_SCREENCAP); + CHECK_EQ(modavd_capabilities(h), MODAVD_CAP_SCREENCAP | MODAVD_CAP_TOUCH); check_capture(h, MODAVD_FMT_BGRA, 4, 0, w, hgt); check_capture(h, MODAVD_FMT_BGR, 3, 0, w, hgt); @@ -193,9 +193,10 @@ static void test_capture_from_fake_frame(void) fake_frame_set_size(&f, w, hgt); CHECK_EQ(modavd_capture(h, 0, MODAVD_FMT_BGR, 0, NULL, 0, &info), MODAVD_OK); - /* input is not in this version */ - CHECK_EQ(modavd_touch_down(h, 0, 0, 1, 1, 1), MODAVD_E_UNSUPPORTED); - CHECK_EQ(modavd_touch_release_all(h), MODAVD_E_UNSUPPORTED); + /* touch goes through the console, and nothing answers on this port (test_input covers a fake one) */ + CHECK_EQ(modavd_touch_down(h, 0, 0, 1, 1, 1), MODAVD_E_NOT_FOUND); + CHECK_EQ(modavd_touch_release_all(h), MODAVD_OK); + /* keys and text are not in this version */ CHECK_EQ(modavd_key(h, 0, 4, 1), MODAVD_E_UNSUPPORTED); CHECK_EQ(modavd_input_text(h, "a"), MODAVD_E_UNSUPPORTED); diff --git a/tests/test_input.c b/tests/test_input.c new file mode 100644 index 0000000..2d8764e --- /dev/null +++ b/tests/test_input.c @@ -0,0 +1,365 @@ +/* + * Touch input against a fake emulator console: the commands sent for each + * action, coordinate scaling, reconnecting after the console drops, lifting + * contacts on close, and the auth line. + * + * Copyright 2026 qiyinxi + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +#include +#include +#include + +#include "fake_frame.h" +#include "modavd_extras.h" + +#ifdef _WIN32 +#include +#include +typedef SOCKET sock_t; +typedef int socklen_t; +#define BAD_SOCK INVALID_SOCKET +#define close_sock closesocket +static void sleep_ms(int ms) { Sleep((DWORD)ms); } +#else +#include +#include +#include +#include +#include +#include +#include +typedef int sock_t; +#define BAD_SOCK (-1) +#define close_sock close +static void sleep_ms(int ms) +{ + struct timespec ts = { ms / 1000, (long)(ms % 1000) * 1000000L }; + nanosleep(&ts, NULL); +} +#endif + +static int g_failures; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +#define CHECK_EQ(a, b) \ + do { \ + long long va_ = (long long)(a), vb_ = (long long)(b); \ + if (va_ != vb_) { \ + fprintf(stderr, "%s:%d: CHECK_EQ failed: %s = %lld, %s = %lld\n", __FILE__, __LINE__, #a, va_, #b, vb_); \ + ++g_failures; \ + } \ + } while (0) + +#define CHECK_STR(a, b) \ + do { \ + const char *sa_ = (a), *sb_ = (b); \ + if (strcmp(sa_, sb_) != 0) { \ + fprintf(stderr, "%s:%d: CHECK_STR failed:\n got %s\n want %s\n", __FILE__, __LINE__, sa_, sb_); \ + ++g_failures; \ + } \ + } while (0) + +/* ---- a fake console: replies OK to every line, records what it got ---- */ + +#define MAX_LINES 64 + +static struct { + sock_t listener; + int port; + char lines[MAX_LINES][256]; + volatile int count; + volatile int connections; + volatile int drop_next; /* close without replying to the next command */ + volatile int stop; +} g_con; + +static void record(const char* line) +{ + if (g_con.count < MAX_LINES) { + snprintf(g_con.lines[g_con.count], sizeof(g_con.lines[0]), "%s", line); + g_con.count = g_con.count + 1; + } +} + +static void serve_client(sock_t c) +{ + static const char kBanner[] = "Android Console: fake\r\nOK\r\n"; + send(c, kBanner, (int)strlen(kBanner), 0); + char buf[2048]; + int len = 0; + for (;;) { + int n = (int)recv(c, buf + len, (int)sizeof(buf) - 1 - len, 0); + if (n <= 0) { + return; + } + len += n; + buf[len] = '\0'; + char* eol; + while ((eol = strstr(buf, "\r\n")) != NULL) { + *eol = '\0'; + record(buf); + int quit = strcmp(buf, "quit") == 0; + int drop = !quit && strncmp(buf, "auth ", 5) != 0 && g_con.drop_next; + int rest = len - (int)(eol + 2 - buf); + memmove(buf, eol + 2, (size_t)rest + 1); + len = rest; + if (quit) { + return; + } + if (drop) { + g_con.drop_next = 0; + return; + } + send(c, "OK\r\n", 4, 0); + } + } +} + +#ifdef _WIN32 +static DWORD WINAPI server_main(LPVOID arg) +#else +static void* server_main(void* arg) +#endif +{ + (void)arg; + while (!g_con.stop) { + sock_t c = accept(g_con.listener, NULL, NULL); + if (c == BAD_SOCK || g_con.stop) { + if (c != BAD_SOCK) { + close_sock(c); + } + break; + } + g_con.connections = g_con.connections + 1; + serve_client(c); + close_sock(c); + } + return 0; +} + +static int start_console(void) +{ + g_con.listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (g_con.listener == BAD_SOCK) { + return -1; + } + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + socklen_t alen = sizeof(addr); + if (bind(g_con.listener, (struct sockaddr*)&addr, sizeof(addr)) != 0 || listen(g_con.listener, 4) != 0 || + getsockname(g_con.listener, (struct sockaddr*)&addr, &alen) != 0) { + return -1; + } + g_con.port = ntohs(addr.sin_port); +#ifdef _WIN32 + HANDLE t = CreateThread(NULL, 0, server_main, NULL, 0, NULL); + if (!t) { + return -1; + } + CloseHandle(t); +#else + pthread_t t; + if (pthread_create(&t, NULL, server_main, NULL) != 0) { + return -1; + } + pthread_detach(t); +#endif + return 0; +} + +static void stop_console(void) +{ + g_con.stop = 1; + /* Wake the blocked accept() with a connection of our own. */ + sock_t s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)g_con.port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + connect(s, (struct sockaddr*)&addr, sizeof(addr)); + close_sock(s); + sleep_ms(50); + close_sock(g_con.listener); +} + +/* Waits until the console has recorded n lines (the "quit" of a close is not replied to). */ +static int wait_lines(int n) +{ + for (int i = 0; i < 200 && g_con.count < n; ++i) { + sleep_ms(10); + } + return g_con.count; +} + +/* Points the console auth token at a file holding "secret". */ +static int use_auth_token(void) +{ + static const char kHome[] = "test_input_home"; +#ifdef _WIN32 + _mkdir(kHome); + char full[1024]; + if (!_fullpath(full, kHome, sizeof(full)) || _putenv_s("USERPROFILE", full) != 0) { + return -1; + } +#else + mkdir(kHome, 0700); + char full[1024]; + if (!realpath(kHome, full) || setenv("HOME", full, 1) != 0) { + return -1; + } +#endif + char path[1100]; + snprintf(path, sizeof(path), "%s/.emulator_console_auth_token", full); + FILE* f = fopen(path, "wb"); + if (!f) { + return -1; + } + fputs("secret\n", f); + fclose(f); + return 0; +} + +/* ---- tests ---- */ + +static void test_axis_and_commands(void) +{ + CHECK_EQ(mx_touch_axis(0, 1280), 0); + CHECK_EQ(mx_touch_axis(-5, 1280), 0); + CHECK_EQ(mx_touch_axis(640, 1280), 16383); + CHECK_EQ(mx_touch_axis(1100, 1280), 28159); /* what the emulator sent for "event mouse 1100 ..." */ + CHECK_EQ(mx_touch_axis(1279, 1280), 32741); + CHECK_EQ(mx_touch_axis(5000, 1280), 32741); /* clamped to the last pixel */ + CHECK_EQ(mx_touch_axis(10, 0), 0); + + char buf[256]; + CHECK(mx_touch_command(buf, sizeof(buf), MX_TOUCH_DOWN, 2, 100, 200, 1024) > 0); + CHECK_STR(buf, "event send 3:47:2 3:57:2 3:53:100 3:54:200 3:58:1024 3:48:1280 3:49:1280 0:0:0"); + CHECK(mx_touch_command(buf, sizeof(buf), MX_TOUCH_MOVE, 2, 300, 400, 512) > 0); + CHECK_STR(buf, "event send 3:47:2 3:53:300 3:54:400 3:58:512 0:0:0"); + CHECK(mx_touch_command(buf, sizeof(buf), MX_TOUCH_UP, 2, 0, 0, 0) > 0); + CHECK_STR(buf, "event send 3:47:2 3:58:0 3:57:-1 0:0:0"); + CHECK_EQ(mx_touch_command(buf, 10, MX_TOUCH_DOWN, 0, 1, 1, 1), -1); + CHECK_EQ(mx_touch_command(buf, sizeof(buf), 9, 0, 1, 1, 1), -1); +} + +static void test_touch_session(void) +{ + if (start_console() != 0 || use_auth_token() != 0) { + fprintf(stderr, "fake console setup failed\n"); + ++g_failures; + return; + } + fake_frame f; + if (fake_frame_create(&f, g_con.port, 1280, 720, 1280, 720) != 0) { + fprintf(stderr, "fake frame setup failed\n"); + ++g_failures; + stop_console(); + return; + } + + modavd_open_params p; + memset(&p, 0, sizeof(p)); + p.size = sizeof(p); + p.console_port = g_con.port; + p.flags = MODAVD_OPEN_NO_START; + modavd_handle h = NULL; + CHECK_EQ(modavd_open(&p, &h), MODAVD_OK); + if (!h) { + fake_frame_destroy(&f); + stop_console(); + return; + } + CHECK(modavd_capabilities(h) & MODAVD_CAP_TOUCH); + CHECK_EQ(g_con.connections, 0); /* the session opens on the first touch */ + + /* tap at the centre: auth, down, up on one session */ + CHECK_EQ(modavd_touch_down(h, 0, 0, 640, 360, 0), MODAVD_OK); + CHECK_EQ(modavd_touch_up(h, 0, 0), MODAVD_OK); + CHECK_EQ(g_con.connections, 1); + CHECK_EQ(g_con.count, 3); + CHECK_STR(g_con.lines[0], "auth secret"); + CHECK_STR(g_con.lines[1], "event send 3:47:0 3:57:0 3:53:16383 3:54:16383 3:58:1024 3:48:1280 3:49:1280 0:0:0"); + CHECK_STR(g_con.lines[2], "event send 3:47:0 3:58:0 3:57:-1 0:0:0"); + + /* lifting it again sends nothing; moving a lifted contact is an error */ + CHECK_EQ(modavd_touch_up(h, 0, 0), MODAVD_OK); + CHECK_EQ(modavd_touch_move(h, 0, 0, 1, 1, 0), MODAVD_E_INVALID); + CHECK_EQ(g_con.count, 3); + + /* arguments */ + CHECK_EQ(modavd_touch_down(h, 0, MX_MAX_CONTACTS, 1, 1, 0), MODAVD_E_INVALID); + CHECK_EQ(modavd_touch_down(h, 0, -1, 1, 1, 0), MODAVD_E_INVALID); + CHECK_EQ(modavd_touch_down(h, 1, 0, 1, 1, 0), MODAVD_E_UNSUPPORTED); + CHECK_EQ(modavd_touch_down(NULL, 0, 0, 1, 1, 0), MODAVD_E_INVALID); + CHECK_EQ(modavd_key(h, 0, 4, 1), MODAVD_E_UNSUPPORTED); + CHECK_EQ(modavd_input_text(h, "a"), MODAVD_E_UNSUPPORTED); + CHECK_EQ(g_con.count, 3); + + /* a swipe with its own pressure, clamped to the screen; pressing again moves */ + CHECK_EQ(modavd_touch_down(h, 0, 3, 100, 100, 500), MODAVD_OK); + CHECK_EQ(modavd_touch_move(h, 0, 3, 5000, 719, 0), MODAVD_OK); + CHECK_EQ(modavd_touch_down(h, 0, 3, 640, 360, 0), MODAVD_OK); + CHECK_STR(g_con.lines[3], "event send 3:47:3 3:57:3 3:53:2559 3:54:4550 3:58:500 3:48:1280 3:49:1280 0:0:0"); + CHECK_STR(g_con.lines[4], "event send 3:47:3 3:53:32741 3:54:32721 3:58:1024 0:0:0"); + CHECK_STR(g_con.lines[5], "event send 3:47:3 3:53:16383 3:54:16383 3:58:1024 0:0:0"); + + /* two fingers, then release all lifts both */ + CHECK_EQ(modavd_touch_down(h, 0, 1, 10, 10, 0), MODAVD_OK); + CHECK_EQ(modavd_touch_release_all(h), MODAVD_OK); + CHECK_EQ(g_con.count, 9); + CHECK_STR(g_con.lines[7], "event send 3:47:1 3:58:0 3:57:-1 0:0:0"); + CHECK_STR(g_con.lines[8], "event send 3:47:3 3:58:0 3:57:-1 0:0:0"); + + /* nothing published yet: no size to scale with */ + fake_frame_set_size(&f, 0, 0); + CHECK_EQ(modavd_touch_down(h, 0, 0, 1, 1, 0), MODAVD_E_NOT_READY); + fake_frame_set_size(&f, 1280, 720); + CHECK_EQ(g_con.count, 9); + + /* the console drops the session mid-command: reconnect once and resend */ + g_con.drop_next = 1; + CHECK_EQ(modavd_touch_down(h, 0, 4, 0, 0, 0), MODAVD_OK); + CHECK_EQ(g_con.connections, 2); + CHECK_EQ(g_con.count, 12); + CHECK_STR(g_con.lines[9], g_con.lines[11]); + CHECK_STR(g_con.lines[10], "auth secret"); + + /* closing lifts contact 4, then quits */ + modavd_close(h); + CHECK_EQ(wait_lines(14), 14); + CHECK_STR(g_con.lines[12], "event send 3:47:4 3:58:0 3:57:-1 0:0:0"); + CHECK_STR(g_con.lines[13], "quit"); + + fake_frame_destroy(&f); + stop_console(); +} + +int main(void) +{ +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + test_axis_and_commands(); + test_touch_session(); +#ifdef _WIN32 + WSACleanup(); +#endif + if (g_failures) { + fprintf(stderr, "%d check(s) failed\n", g_failures); + return 1; + } + printf("test_input: all passed\n"); + return 0; +} From da0c80586ab04d1596b2016b08a17a26c7f8d997 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Fri, 13 Nov 2026 16:40:36 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8F=B0=E5=91=BD=E4=BB=A4=E6=8C=AA=E5=88=B0=20consol?= =?UTF-8?q?e=5Fcommand.c=EF=BC=8C=E6=B5=8B=E8=AF=95=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E4=B8=8D=E5=86=8D=E7=94=A8=20snprintf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_open_posix 自己定义 mx_console_command 顶替库里的;点击用到 console.c 的会话函数后,console.c 会被链接进来,两份定义冲突(macOS 链接失败)。一次性命令单独成文件,顶替照旧有效。 - gcc 的 -Wformat-truncation 把 test_input 里按 %s 拷进固定缓冲判为可能截断,改成按长度拷贝。 --- CMakeLists.txt | 2 +- src/console.c | 13 ------------- src/console_command.c | 22 ++++++++++++++++++++++ tests/test_input.c | 7 ++++++- 4 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 src/console_command.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0278313..5f3a7b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ if(NOT WIN32) endif() endif() -set(MX_SOURCES src/core.c src/shm.c src/console.c src/input.c src/log.c) +set(MX_SOURCES src/core.c src/shm.c src/console.c src/console_command.c src/input.c src/log.c) if(NOT WIN32) # POSIX: the per-handle input lock. diff --git a/src/console.c b/src/console.c index 56e440a..2acb65c 100644 --- a/src/console.c +++ b/src/console.c @@ -195,16 +195,3 @@ void mx_console_close(mx_console* c) WSACleanup(); #endif } - -int mx_console_command(int port, const char* command, int timeout_ms) -{ - mx_console c; - int rc = mx_console_open(port, timeout_ms, &c); - if (rc != MODAVD_OK) { - return rc; - } - rc = mx_console_send(&c, command); - mx_log(MODAVD_LOG_INFO, "console %d %s -> %s", port, command, rc == MODAVD_OK ? "OK" : "fail"); - mx_console_close(&c); - return rc == MODAVD_OK ? MODAVD_OK : MODAVD_E_IO; -} diff --git a/src/console_command.c b/src/console_command.c new file mode 100644 index 0000000..86266cc --- /dev/null +++ b/src/console_command.c @@ -0,0 +1,22 @@ +/* + * One console command on its own connection (used to start frame sharing). + * Kept apart from console.c so that a test can replace it while the touch + * session functions still come from the library. + * + * Copyright 2026 qiyinxi + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +#include "mx_internal.h" + +int mx_console_command(int port, const char* command, int timeout_ms) +{ + mx_console c; + int rc = mx_console_open(port, timeout_ms, &c); + if (rc != MODAVD_OK) { + return rc; + } + rc = mx_console_send(&c, command); + mx_log(MODAVD_LOG_INFO, "console %d %s -> %s", port, command, rc == MODAVD_OK ? "OK" : "fail"); + mx_console_close(&c); + return rc == MODAVD_OK ? MODAVD_OK : MODAVD_E_IO; +} diff --git a/tests/test_input.c b/tests/test_input.c index 2d8764e..24cca73 100644 --- a/tests/test_input.c +++ b/tests/test_input.c @@ -84,7 +84,12 @@ static struct { static void record(const char* line) { if (g_con.count < MAX_LINES) { - snprintf(g_con.lines[g_con.count], sizeof(g_con.lines[0]), "%s", line); + size_t n = strlen(line); + if (n >= sizeof(g_con.lines[0])) { + n = sizeof(g_con.lines[0]) - 1; + } + memcpy(g_con.lines[g_con.count], line, n); + g_con.lines[g_con.count][n] = '\0'; g_con.count = g_con.count + 1; } } From 19ec421f5b92bef4994b102c16f5b37f5e79bf1c Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Fri, 13 Nov 2026 16:47:24 +0100 Subject: [PATCH 3/6] =?UTF-8?q?test=5Finput=EF=BC=9Arealpath=20=E7=9A=84?= =?UTF-8?q?=E7=BC=93=E5=86=B2=E5=8C=BA=E6=8C=89=20PATH=5FMAX=20=E7=BB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux 上 PATH_MAX 是 4096,加固过的 glibc 对 1024 字节的缓冲区直接中止进程;macOS 的 PATH_MAX 正好是 1024,所以只在 Linux 上挂。 --- tests/test_input.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_input.c b/tests/test_input.c index 24cca73..0319599 100644 --- a/tests/test_input.c +++ b/tests/test_input.c @@ -23,12 +23,16 @@ typedef int socklen_t; static void sleep_ms(int ms) { Sleep((DWORD)ms); } #else #include +#include #include #include #include #include #include #include +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif typedef int sock_t; #define BAD_SOCK (-1) #define close_sock close @@ -219,12 +223,13 @@ static int use_auth_token(void) } #else mkdir(kHome, 0700); - char full[1024]; + /* realpath() needs PATH_MAX bytes (4096 on Linux); fortified glibc aborts on less. */ + char full[PATH_MAX]; if (!realpath(kHome, full) || setenv("HOME", full, 1) != 0) { return -1; } #endif - char path[1100]; + char path[sizeof(full) + 64]; snprintf(path, sizeof(path), "%s/.emulator_console_auth_token", full); FILE* f = fopen(path, "wb"); if (!f) { From f358928396f2e3481e14da77bf086603c2116f37 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Fri, 13 Nov 2026 18:00:12 +0100 Subject: [PATCH 4/6] =?UTF-8?q?v0.2.0=EF=BC=9A=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E5=92=8C=E5=8F=91=E7=89=88=E8=AF=B4=E6=98=8E=EF=BC=88=E5=93=88?= =?UTF-8?q?=E5=B8=8C=E7=AD=89=20CI=20=E7=BC=96=E5=87=BA=E6=9D=A5=E5=86=8D?= =?UTF-8?q?=E5=A1=AB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 2 +- release/0.2.0.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 release/0.2.0.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f3a7b9..c92f3ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) # macOS: the oldest release the library loads on. Without it the build machine's SDK version is used. set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum macOS version") -project(ModAVDExtras VERSION 0.1.3 LANGUAGES C) +project(ModAVDExtras VERSION 0.2.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/release/0.2.0.md b/release/0.2.0.md new file mode 100644 index 0000000..721646f --- /dev/null +++ b/release/0.2.0.md @@ -0,0 +1,49 @@ +# ModAVDExtras v0.2.0 + +- `modavd_extras.dll`:(CI 编出后填) +- `external_renderer_ipc.dll`:(CI 编出后填) +- `libmodavd_extras.dylib`:(CI 编出后填) +- `libmodavd_extras.so`:(CI 编出后填) + +这一版加上点击:头文件里预留的 `modavd_touch_*` 能用了,接口版本升到 2。截图和 MuMu 外壳的行为不变。 + +## 新增 + +- 点击(`MODAVD_CAP_TOUCH`):`modavd_touch_down`、`modavd_touch_move`、`modavd_touch_up`、`modavd_touch_release_all`(#15)。 + - 走模拟器控制台,按多点触控协议发到模拟器自己的虚拟触屏:安卓看到的是手指在屏幕上,客体里不用装任何东西。 + - 坐标是共享画面的像素,最多 10 根手指同时按下;`pressure` 给 0 就用模拟器窗口鼠标点击时的默认值。只支持显示屏 0。 + - 第一次点击时连上控制台,之后一直用这条连接;模拟器重启、连接断了,下一次点击重连一次。`modavd_close()` 会先抬起这个句柄按下的手指。 + - 还没有画面时(不知道屏幕尺寸)返回 `MODAVD_E_NOT_READY`。 +- `modavd_api_version()` 返回 2,`modavd_capabilities()` 多了 `MODAVD_CAP_TOUCH`。 +- `modavd_version_string()` 返回 `"0.2.0"`。 + +## 说明 + +- 按键和输入文字(`modavd_key`、`modavd_text`)照旧返回 `MODAVD_E_UNSUPPORTED`:控制台发的按键会进触屏设备、被丢掉,输入文字会丢掉键盘映射里没有的字,这两样以后另做。 +- 只认接口版本 1 的程序不受影响:接口版本 1 的函数和行为都没变。 +- `external_renderer_ipc.dll` 的源码没改,但它静态链接了库的代码,所以文件和 v0.1.3 不一样。 + +## 许可证与源码 + +- LGPL-3.0-or-later。正文是 tag 下的 [LICENSE](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/LICENSE)(LGPL-3.0)和 [COPYING](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/COPYING)(GPL-3.0);转发这些库时请一起带上。 +- 源码:。 + +## 下载与核对 + +四个文件都由本仓库的 release workflow 在 GitHub 上编出,和 tag 下的 `release/0.2.0.SHA256SUMS.txt` 核对过才上传。编译环境和 v0.1.3 一样: + +- Windows:`windows-2025`,`cmake -A x64`,`--config Release`。两个 DLL 都静态链接 C 运行库,只依赖 `KERNEL32`、`WS2_32`,编译带 `/Brepro`。 +- macOS:`macos-15`(Apple Silicon),`cmake -DCMAKE_BUILD_TYPE=Release`。 +- Linux:`ubuntu-24.04`(x64),`cmake -DCMAKE_BUILD_TYPE=Release`。 + +请用附件 `SHA256SUMS.txt` 核对:Windows 上 `Get-FileHash .\modavd_extras.dll -Algorithm SHA256`,macOS 上 `shasum -a 256 libmodavd_extras.dylib`,Linux 上 `sha256sum libmodavd_extras.so`。 + +## English + +This release adds touch: the `modavd_touch_*` functions reserved in the header now work, and the API version is 2. Capture and the MuMu shim behave as before. + +- Touch (`MODAVD_CAP_TOUCH`): `modavd_touch_down`, `modavd_touch_move`, `modavd_touch_up`, `modavd_touch_release_all` (#15). They go through the emulator console as multi-touch events on the emulator's own virtual touch screen, so Android sees a finger on the screen and nothing is needed in the guest. Coordinates are pixels of the shared frame; up to 10 contacts at once; `pressure` 0 uses the default of a mouse click on the emulator window; display 0 only. The console session opens on the first touch and is kept; if it broke (the emulator restarted) the next touch reconnects once. `modavd_close()` lifts the contacts the handle pressed. Before the first frame (display size unknown) the calls return `MODAVD_E_NOT_READY`. +- `modavd_api_version()` returns 2, `modavd_capabilities()` adds `MODAVD_CAP_TOUCH`, `modavd_version_string()` returns `"0.2.0"`. +- Keys and text (`modavd_key`, `modavd_text`) still return `MODAVD_E_UNSUPPORTED`. API version 1 functions are unchanged. `external_renderer_ipc.dll` has no source changes, but it links the library code statically, so the file differs from v0.1.3. +- License: LGPL-3.0-or-later; the texts are [LICENSE](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/LICENSE) (LGPL-3.0) and [COPYING](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/COPYING) (GPL-3.0) at the tag. Ship them with the libraries. Source: . +- All four files are built by this repository's release workflow on GitHub, the same way as v0.1.3, and checked against `release/0.2.0.SHA256SUMS.txt` at the tag before upload. Check them with the attached `SHA256SUMS.txt`. From 1594f8afbe8f3920b2192f73297e3e5175331427 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Fri, 13 Nov 2026 18:08:07 +0100 Subject: [PATCH 5/6] =?UTF-8?q?v0.2.0=EF=BC=9A=E5=A1=AB=E4=B8=8A=20CI=20?= =?UTF-8?q?=E7=BC=96=E5=87=BA=E7=9A=84=E5=9B=9B=E4=B8=AA=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E5=93=88=E5=B8=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- release/0.2.0.SHA256SUMS.txt | 4 ++++ release/0.2.0.md | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 release/0.2.0.SHA256SUMS.txt diff --git a/release/0.2.0.SHA256SUMS.txt b/release/0.2.0.SHA256SUMS.txt new file mode 100644 index 0000000..014e6c9 --- /dev/null +++ b/release/0.2.0.SHA256SUMS.txt @@ -0,0 +1,4 @@ +7ce6ea8c0c01c14dcb5ce3cd4271e2fa9a147b3f3c4c57878e8cec8dff52f89f external_renderer_ipc.dll +b6ea821c5710d07411412011ac32e62718b8f9412451017609411ecb958a9c9a libmodavd_extras.dylib +9e870a02f864c9072111b89df77c2747ac5fabd4a4b2a2ee150d093c558cc8d0 libmodavd_extras.so +6fe84a40a5aa8da31d1a24492bf860b6912bf322e29d3bdba73bd80a34fa61da modavd_extras.dll diff --git a/release/0.2.0.md b/release/0.2.0.md index 721646f..af4c433 100644 --- a/release/0.2.0.md +++ b/release/0.2.0.md @@ -1,9 +1,9 @@ # ModAVDExtras v0.2.0 -- `modavd_extras.dll`:(CI 编出后填) -- `external_renderer_ipc.dll`:(CI 编出后填) -- `libmodavd_extras.dylib`:(CI 编出后填) -- `libmodavd_extras.so`:(CI 编出后填) +- `modavd_extras.dll`:`6fe84a40a5aa8da31d1a24492bf860b6912bf322e29d3bdba73bd80a34fa61da` +- `external_renderer_ipc.dll`:`7ce6ea8c0c01c14dcb5ce3cd4271e2fa9a147b3f3c4c57878e8cec8dff52f89f` +- `libmodavd_extras.dylib`:`b6ea821c5710d07411412011ac32e62718b8f9412451017609411ecb958a9c9a` +- `libmodavd_extras.so`:`9e870a02f864c9072111b89df77c2747ac5fabd4a4b2a2ee150d093c558cc8d0` 这一版加上点击:头文件里预留的 `modavd_touch_*` 能用了,接口版本升到 2。截图和 MuMu 外壳的行为不变。 From e2d3860a299b2d297ae4ad780dc8d4ec2b41deb5 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Fri, 13 Nov 2026 19:30:32 +0100 Subject: [PATCH 6/6] =?UTF-8?q?v0.2.0=EF=BC=9A=E5=8F=91=E7=89=88=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E9=87=8C=E7=9A=84=E5=87=BD=E6=95=B0=E5=90=8D=E5=92=8C?= =?UTF-8?q?=20NOT=5FREADY=20=E7=9A=84=E8=8C=83=E5=9B=B4=E6=94=B9=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- release/0.2.0.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/release/0.2.0.md b/release/0.2.0.md index af4c433..1a1ecb0 100644 --- a/release/0.2.0.md +++ b/release/0.2.0.md @@ -13,13 +13,13 @@ - 走模拟器控制台,按多点触控协议发到模拟器自己的虚拟触屏:安卓看到的是手指在屏幕上,客体里不用装任何东西。 - 坐标是共享画面的像素,最多 10 根手指同时按下;`pressure` 给 0 就用模拟器窗口鼠标点击时的默认值。只支持显示屏 0。 - 第一次点击时连上控制台,之后一直用这条连接;模拟器重启、连接断了,下一次点击重连一次。`modavd_close()` 会先抬起这个句柄按下的手指。 - - 还没有画面时(不知道屏幕尺寸)返回 `MODAVD_E_NOT_READY`。 + - 还没有画面时(不知道屏幕尺寸),按下和移动返回 `MODAVD_E_NOT_READY`。 - `modavd_api_version()` 返回 2,`modavd_capabilities()` 多了 `MODAVD_CAP_TOUCH`。 - `modavd_version_string()` 返回 `"0.2.0"`。 ## 说明 -- 按键和输入文字(`modavd_key`、`modavd_text`)照旧返回 `MODAVD_E_UNSUPPORTED`:控制台发的按键会进触屏设备、被丢掉,输入文字会丢掉键盘映射里没有的字,这两样以后另做。 +- 按键和输入文字(`modavd_key`、`modavd_input_text`)照旧返回 `MODAVD_E_UNSUPPORTED`:控制台发的按键会进触屏设备、被丢掉,输入文字会丢掉键盘映射里没有的字,这两样以后另做。 - 只认接口版本 1 的程序不受影响:接口版本 1 的函数和行为都没变。 - `external_renderer_ipc.dll` 的源码没改,但它静态链接了库的代码,所以文件和 v0.1.3 不一样。 @@ -42,8 +42,8 @@ This release adds touch: the `modavd_touch_*` functions reserved in the header now work, and the API version is 2. Capture and the MuMu shim behave as before. -- Touch (`MODAVD_CAP_TOUCH`): `modavd_touch_down`, `modavd_touch_move`, `modavd_touch_up`, `modavd_touch_release_all` (#15). They go through the emulator console as multi-touch events on the emulator's own virtual touch screen, so Android sees a finger on the screen and nothing is needed in the guest. Coordinates are pixels of the shared frame; up to 10 contacts at once; `pressure` 0 uses the default of a mouse click on the emulator window; display 0 only. The console session opens on the first touch and is kept; if it broke (the emulator restarted) the next touch reconnects once. `modavd_close()` lifts the contacts the handle pressed. Before the first frame (display size unknown) the calls return `MODAVD_E_NOT_READY`. +- Touch (`MODAVD_CAP_TOUCH`): `modavd_touch_down`, `modavd_touch_move`, `modavd_touch_up`, `modavd_touch_release_all` (#15). They go through the emulator console as multi-touch events on the emulator's own virtual touch screen, so Android sees a finger on the screen and nothing is needed in the guest. Coordinates are pixels of the shared frame; up to 10 contacts at once; `pressure` 0 uses the default of a mouse click on the emulator window; display 0 only. The console session opens on the first touch and is kept; if it broke (the emulator restarted) the next touch reconnects once. `modavd_close()` lifts the contacts the handle pressed. Before the first frame (display size unknown) down and move return `MODAVD_E_NOT_READY`. - `modavd_api_version()` returns 2, `modavd_capabilities()` adds `MODAVD_CAP_TOUCH`, `modavd_version_string()` returns `"0.2.0"`. -- Keys and text (`modavd_key`, `modavd_text`) still return `MODAVD_E_UNSUPPORTED`. API version 1 functions are unchanged. `external_renderer_ipc.dll` has no source changes, but it links the library code statically, so the file differs from v0.1.3. +- Keys and text (`modavd_key`, `modavd_input_text`) still return `MODAVD_E_UNSUPPORTED`. API version 1 functions are unchanged. `external_renderer_ipc.dll` has no source changes, but it links the library code statically, so the file differs from v0.1.3. - License: LGPL-3.0-or-later; the texts are [LICENSE](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/LICENSE) (LGPL-3.0) and [COPYING](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/COPYING) (GPL-3.0) at the tag. Ship them with the libraries. Source: . - All four files are built by this repository's release workflow on GitHub, the same way as v0.1.3, and checked against `release/0.2.0.SHA256SUMS.txt` at the tag before upload. Check them with the attached `SHA256SUMS.txt`.