From 0d387a42498bb23d27640b037165343277fed8fa Mon Sep 17 00:00:00 2001 From: Gregory Lirent Date: Sun, 21 Jun 2026 10:31:36 +0300 Subject: [PATCH] input: one-packet pointer moves, uinput-only injection, fire-and-forget - Pointer motion is now a single event carrying both coordinates (MOVE_ABS / MOVE_REL with x,y) rather than one event per axis; the adapter actuates both axes in a single batch. The per-axis ABS/REL kinds are removed. - Input injection is uinput-only: the driver selection and the optional guest-side passthrough drop out of the adapter config (the driver is the driver's concern). A QMP path is still carried for the unchanged service power/lifecycle path. - A per-event fire-and-forget flag lets a control inject input without an actuation acknowledgement, for high-rate streams; without it the addressed ACT_ACK is emitted as before. Service commands always acknowledge. The neutral input payload gains x/y/flags, still within the inline event body. Capability, lease and source gates are unchanged. --- CMakeLists.txt | 2 +- include/vmsig_event.h | 38 +++++++++++++-------- src/adapter/input/include/input.h | 10 +++--- src/adapter/input/input.c | 56 ++++++++++++++++++------------- src/cli.c | 4 +-- src/test/test_inputobs.c | 9 +++++ 6 files changed, 72 insertions(+), 47 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b80f3dc..1247944 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(vmsig C) +project(vmsig VERSION 0.2.0 C) set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/include/vmsig_event.h b/include/vmsig_event.h index 09fa6e4..fe20876 100644 --- a/include/vmsig_event.h +++ b/include/vmsig_event.h @@ -148,24 +148,34 @@ enum { }; /* ===== Input (DOWN VMSIG_EV_CMD_INPUT, in inln) — NEUTRAL ===== - * control describes input abstractly (axis/button/key/scroll), WITHOUT knowing the driver - * (uinput/QMP): the input adapter translates it into its contract. Requires CAP_INPUT. This - * is the ONLY public input-encoding contract — an external control encodes vmsig_input into - * vmsig_event.inln. */ + * control describes input abstractly (pointer motion / button / key / scroll), WITHOUT knowing + * the driver: the input adapter translates it into its contract (injection is always uinput). + * Requires CAP_INPUT. This is the ONLY public input-encoding contract — an external control + * encodes vmsig_input into vmsig_event.inln. + * + * Pointer motion carries BOTH coordinates in ONE event (a pointer position is a single entity, + * not two independent axis updates). btn/key/scroll stay single-valued. */ typedef enum { - VMSIG_INPUT_ABS = 0, /* absolute axis: code=axis, value=coordinate */ - VMSIG_INPUT_REL = 1, /* relative axis: code=axis, value=delta */ - VMSIG_INPUT_BTN = 2, /* button: code=button, value=pressed(1)/released(0) */ - VMSIG_INPUT_KEY = 3, /* key: code=evdev code, value=pressed/released */ - VMSIG_INPUT_SCROLL = 4 /* scroll: code=axis, scroll=magnitude */ + VMSIG_INPUT_MOVE_ABS = 0, /* absolute pointer: x,y are coordinates (0..VMCTL_ABS_MAX) */ + VMSIG_INPUT_MOVE_REL = 1, /* relative pointer: x,y are deltas (dx,dy) */ + VMSIG_INPUT_BTN = 2, /* button: code=button, value=pressed(1)/released(0) */ + VMSIG_INPUT_KEY = 3, /* key: code=evdev code, value=pressed/released */ + VMSIG_INPUT_SCROLL = 4 /* scroll: code=axis, scroll=magnitude */ } vmsig_input_kind; +/* Per-event transfer-context flags (signaling owns the transfer context). */ +#define VMSIG_INPUT_F_NOACK 0x1u /* fire-and-forget: adapter actuates, emits NO ACT_ACK */ + typedef struct { - uint32_t kind; /* vmsig_input_kind */ - int32_t code; /* axis / button / evdev code (neutral event code) */ - int32_t value; /* abs coordinate / rel delta / pressed(1)|released(0) */ - double scroll; /* scroll magnitude (VMSIG_INPUT_SCROLL only) */ -} vmsig_input; /* fits in vmsig_event.inln[48] */ + uint16_t kind; /* vmsig_input_kind */ + uint16_t code; /* button / evdev code / scroll axis (NOT used by MOVE_*) */ + int32_t value; /* pressed(1)|released(0) for BTN/KEY (not used by MOVE or SCROLL) */ + int32_t x; /* MOVE_ABS: abs X (0..VMCTL_ABS_MAX); MOVE_REL: dx */ + int32_t y; /* MOVE_ABS: abs Y; MOVE_REL: dy */ + double scroll; /* SCROLL magnitude only */ + uint32_t flags; /* VMSIG_INPUT_F_* (see above) */ + uint32_t _pad; /* reserved; zero on emit */ +} vmsig_input; /* 32 bytes; fits in vmsig_event.inln[48] */ /* ===== Memory write (DOWN VMSIG_EV_CMD_MEMWRITE) — NEUTRAL, write-signaled ===== * control describes an ATOMIC write into guest memory abstractly: a TARGET address space diff --git a/src/adapter/input/include/input.h b/src/adapter/input/include/input.h index 1850a1e..25b3703 100644 --- a/src/adapter/input/include/input.h +++ b/src/adapter/input/include/input.h @@ -2,14 +2,12 @@ #define VMSIG_INPUT_H /* Private config of the input adapter (vmctl). cfg==NULL => stub mode. Armed mode - * (VMSIG_WITH_VMCTL) opens vmctl_open() and actuates for real. driver is an int so - * as not to pull vmctl.h into this header (values match VMCTL_DRIVER_*). */ + * (VMSIG_WITH_VMCTL) opens vmctl_open() and actuates for real. Injection is ALWAYS + * uinput (orphaned host uinput + external QEMU input-linux). qmp_path is kept for the + * SERVICE path (power/lifecycle via vmctl QMP), not for input injection. */ typedef struct { int stub; - int driver; /* 0=QMP, 1=UINPUT (see VMCTL_DRIVER_*) */ - const char* qmp_path; - const char* input_bus; - int ptr_mode; + const char* qmp_path; /* for power/lifecycle (vmctl QMP); NOT input injection */ } vmsig_input_cfg; /* Input event codes/contract are PUBLIC: vmsig_input / vmsig_input_kind in diff --git a/src/adapter/input/input.c b/src/adapter/input/input.c index 173acae..c267297 100644 --- a/src/adapter/input/input.c +++ b/src/adapter/input/input.c @@ -22,12 +22,15 @@ typedef struct { uint32_t corr; uint32_t origin; /* initiator (addressed ACK) */ int kind; /* vmsig_input_kind (for cmd==0) */ - int code; /* axis/btn/evdev-code */ - int value; /* abs/rel/down */ + int code; /* btn/evdev-code/scroll-axis */ + int value; /* pressed(1)/released(0) for btn/key */ + int x; /* MOVE_ABS: abs X; MOVE_REL: dx */ + int y; /* MOVE_ABS: abs Y; MOVE_REL: dy */ double scroll; + int noack; /* CMD_INPUT fire-and-forget: emit no ACT_ACK */ int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */ } input_req; -typedef struct { int ok; uint32_t corr; uint32_t origin; } input_res; +typedef struct { int ok; uint32_t corr; uint32_t origin; int noack; } input_res; /* signaling does NOT track held state: the record of what is pressed lives in the * ACTUATOR (vmctl); we hand it to control on request (CMD_QUERY_INPUT), release is control's decision. */ @@ -36,10 +39,7 @@ struct vmsig_adapter { int stub; vmsig_emit emit; vmsig_worker* worker; - int driver; /* 0=QMP, 1=UINPUT (VMCTL_DRIVER_*); carried open->attach */ - const char* qmp_path; /* borrowed from cfg (valid through attach) */ - const char* input_bus; - int ptr_mode; + const char* qmp_path; /* borrowed from cfg (valid through attach); SERVICE power/lifecycle */ #ifdef VMSIG_WITH_VMCTL vmctl_t* vmctl; #endif @@ -52,14 +52,22 @@ static int input_job(void* user, const void* reqp, void* resp) { memset(rs, 0, sizeof *rs); rs->corr = rq->corr; rs->origin = rq->origin; + rs->noack = rq->noack; #ifdef VMSIG_WITH_VMCTL if (a->vmctl) { int r = -1; if (rq->cmd == 0) { + /* Pointer motion is ONE packet: both axes in a single batch -> one round-trip. */ vmctl_batch b; vmctl_batch_init(&b); switch (rq->kind) { - case VMSIG_INPUT_ABS: vmctl_batch_abs(&b, rq->code, rq->value); break; - case VMSIG_INPUT_REL: vmctl_batch_rel(&b, rq->code, rq->value); break; + case VMSIG_INPUT_MOVE_ABS: + vmctl_batch_abs(&b, VMCTL_AXIS_X, rq->x); + vmctl_batch_abs(&b, VMCTL_AXIS_Y, rq->y); + break; + case VMSIG_INPUT_MOVE_REL: + vmctl_batch_rel(&b, VMCTL_AXIS_X, rq->x); + vmctl_batch_rel(&b, VMCTL_AXIS_Y, rq->y); + break; case VMSIG_INPUT_BTN: vmctl_batch_btn(&b, rq->code, rq->value); break; case VMSIG_INPUT_KEY: vmctl_batch_key(&b, rq->code, rq->value); break; case VMSIG_INPUT_SCROLL: vmctl_batch_scroll(&b, rq->code, rq->scroll); break; @@ -91,12 +99,7 @@ static vmsig_adapter* in_open(const void* cfg, uint32_t endpoint) { if (!a) return NULL; a->endpoint = endpoint; a->stub = c ? c->stub : 1; - if (c) { /* carry the driver selection to attach (cfg not passed there) */ - a->driver = c->driver; - a->qmp_path = c->qmp_path; - a->input_bus = c->input_bus; - a->ptr_mode = c->ptr_mode; - } + if (c) a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */ return a; } @@ -108,15 +111,16 @@ static int in_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg #ifdef VMSIG_WITH_VMCTL if (!a->stub) { - /* armed: build vmctl_config from the carried cfg and open the actuator. UINPUT - * (host uinput + optional virtio-input-host-pci passthrough via QMP) is the primary - * input driver; QMP input-send-event is the fallback. */ + /* armed: open the actuator. Injection is ALWAYS uinput (orphaned host uinput + external + * QEMU input-linux). PTR_BOTH gives both pointer forms a device (A=abs tablet, B=rel + * mouse) — the contract now promises both MOVE_ABS and MOVE_REL, so neither may be + * disabled. qmp_path serves the SERVICE power/lifecycle path, not input injection. */ vmctl_config vcfg; memset(&vcfg, 0, sizeof vcfg); - vcfg.driver = (a->driver == 1) ? VMCTL_DRIVER_UINPUT : VMCTL_DRIVER_QMP; + vcfg.driver = VMCTL_DRIVER_UINPUT; vcfg.qmp_path = a->qmp_path; - vcfg.input_bus = a->input_bus; - vcfg.ptr_mode = a->ptr_mode; + vcfg.input_bus = ""; + vcfg.ptr_mode = VMCTL_PTR_BOTH; vcfg.uinput_id = NULL; /* built-in HID identity defaults */ a->vmctl = vmctl_open(&vcfg); if (!a->vmctl) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; } @@ -141,6 +145,7 @@ static int in_on_ready(vmsig_adapter* a, uint32_t cookie, uint32_t events) { vmsig_worker_ack(a->worker); input_res rs; int rc; while (vmsig_worker_poll(a->worker, &rs, sizeof rs, &rc) == 1) { + if (rs.noack) continue; /* fire-and-forget CMD_INPUT: actuated, emit no ACT_ACK */ vmsig_event up; memset(&up, 0, sizeof up); up.kind = VMSIG_EV_ACT_ACK; up.source = VMSIG_SRC_INPUT; up.dir = VMSIG_DIR_UP; @@ -199,10 +204,13 @@ static int in_submit(vmsig_adapter* a, const vmsig_event* ev) { * held — that is the vmctl actuator's record (returned via CMD_QUERY_INPUT). */ vmsig_input in; memcpy(&in, ev->inln, sizeof in <= sizeof ev->inln ? sizeof in : sizeof ev->inln); - rq.kind = (int)in.kind; - rq.code = (int)in.code; - rq.value = (int)in.value; + rq.kind = (int)in.kind; + rq.code = (int)in.code; + rq.value = (int)in.value; + rq.x = (int)in.x; + rq.y = (int)in.y; rq.scroll = in.scroll; + rq.noack = (in.flags & VMSIG_INPUT_F_NOACK) ? 1 : 0; } else if (ev->kind == VMSIG_EV_CMD_LIFECYCLE) { rq.cmd = 1; rq.life_op = (int)(unsigned char)ev->inln[0]; diff --git a/src/cli.c b/src/cli.c index b391c6d..a7ebb5e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -106,9 +106,9 @@ static int on_event(void* user, const vmsig_event* ev) { in.prio = VMSIG_PRIO_HIGH; in.endpoint = 0; in.corr = 0xC0FFEEu; in.payload.flags = VMSIG_PL_INLINE; vmsig_input act; memset(&act, 0, sizeof act); /* neutral public input contract */ - act.kind = VMSIG_INPUT_ABS; act.code = 0; act.value = 100; /* demo: abs axis X = 100 */ + act.kind = VMSIG_INPUT_MOVE_ABS; act.x = 100; act.y = 100; /* demo: abs pointer (100,100) */ memcpy(in.inln, &act, sizeof act); - printf(" DOWN CMD_INPUT ABS axis=0 val=100 corr=0x%X\n", (unsigned)in.corr); + printf(" DOWN CMD_INPUT MOVE_ABS x=100 y=100 corr=0x%X\n", (unsigned)in.corr); vmsig_inproc_send(d->ctl, &in); vmsig_event vm; diff --git a/src/test/test_inputobs.c b/src/test/test_inputobs.c index 830e0ec..f419373 100644 --- a/src/test/test_inputobs.c +++ b/src/test/test_inputobs.c @@ -85,8 +85,17 @@ static void test_held_query(void) { vmsig_ctx_free(ctx); } +/* The public input contract must fit in the event's inline zone. */ +static void test_contract_size(void) { + printf("test_contract_size\n"); + CHECK(sizeof(vmsig_input) <= sizeof(((vmsig_event*)0)->inln), + "vmsig_input fits in vmsig_event.inln"); + CHECK(sizeof(vmsig_input) == 32, "vmsig_input is 32 bytes"); +} + int main(void) { printf("test_inputobs\n"); + test_contract_size(); test_held_query(); printf("inputobs tests: %s\n", g_fail ? "FAIL" : "PASS"); return g_fail ? 1 : 0;