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.
This commit is contained in:
2026-06-21 10:31:36 +03:00
parent e6c7aed8eb
commit 0d387a4249
6 changed files with 72 additions and 47 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) 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 17)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
+21 -11
View File
@@ -148,24 +148,34 @@ enum {
}; };
/* ===== Input (DOWN VMSIG_EV_CMD_INPUT, in inln) — NEUTRAL ===== /* ===== Input (DOWN VMSIG_EV_CMD_INPUT, in inln) — NEUTRAL =====
* control describes input abstractly (axis/button/key/scroll), WITHOUT knowing the driver * control describes input abstractly (pointer motion / button / key / scroll), WITHOUT knowing
* (uinput/QMP): the input adapter translates it into its contract. Requires CAP_INPUT. This * the driver: the input adapter translates it into its contract (injection is always uinput).
* is the ONLY public input-encoding contract — an external control encodes vmsig_input into * Requires CAP_INPUT. This is the ONLY public input-encoding contract — an external control
* vmsig_event.inln. */ * 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 { typedef enum {
VMSIG_INPUT_ABS = 0, /* absolute axis: code=axis, value=coordinate */ VMSIG_INPUT_MOVE_ABS = 0, /* absolute pointer: x,y are coordinates (0..VMCTL_ABS_MAX) */
VMSIG_INPUT_REL = 1, /* relative axis: code=axis, value=delta */ 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_BTN = 2, /* button: code=button, value=pressed(1)/released(0) */
VMSIG_INPUT_KEY = 3, /* key: code=evdev code, value=pressed/released */ VMSIG_INPUT_KEY = 3, /* key: code=evdev code, value=pressed/released */
VMSIG_INPUT_SCROLL = 4 /* scroll: code=axis, scroll=magnitude */ VMSIG_INPUT_SCROLL = 4 /* scroll: code=axis, scroll=magnitude */
} vmsig_input_kind; } 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 { typedef struct {
uint32_t kind; /* vmsig_input_kind */ uint16_t kind; /* vmsig_input_kind */
int32_t code; /* axis / button / evdev code (neutral event code) */ uint16_t code; /* button / evdev code / scroll axis (NOT used by MOVE_*) */
int32_t value; /* abs coordinate / rel delta / pressed(1)|released(0) */ int32_t value; /* pressed(1)|released(0) for BTN/KEY (not used by MOVE or SCROLL) */
double scroll; /* scroll magnitude (VMSIG_INPUT_SCROLL only) */ int32_t x; /* MOVE_ABS: abs X (0..VMCTL_ABS_MAX); MOVE_REL: dx */
} vmsig_input; /* fits in vmsig_event.inln[48] */ 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 ===== /* ===== Memory write (DOWN VMSIG_EV_CMD_MEMWRITE) — NEUTRAL, write-signaled =====
* control describes an ATOMIC write into guest memory abstractly: a TARGET address space * control describes an ATOMIC write into guest memory abstractly: a TARGET address space
+4 -6
View File
@@ -2,14 +2,12 @@
#define VMSIG_INPUT_H #define VMSIG_INPUT_H
/* Private config of the input adapter (vmctl). cfg==NULL => stub mode. Armed mode /* 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 * (VMSIG_WITH_VMCTL) opens vmctl_open() and actuates for real. Injection is ALWAYS
* as not to pull vmctl.h into this header (values match VMCTL_DRIVER_*). */ * 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 { typedef struct {
int stub; int stub;
int driver; /* 0=QMP, 1=UINPUT (see VMCTL_DRIVER_*) */ const char* qmp_path; /* for power/lifecycle (vmctl QMP); NOT input injection */
const char* qmp_path;
const char* input_bus;
int ptr_mode;
} vmsig_input_cfg; } vmsig_input_cfg;
/* Input event codes/contract are PUBLIC: vmsig_input / vmsig_input_kind in /* Input event codes/contract are PUBLIC: vmsig_input / vmsig_input_kind in
+29 -21
View File
@@ -22,12 +22,15 @@ typedef struct {
uint32_t corr; uint32_t corr;
uint32_t origin; /* initiator (addressed ACK) */ uint32_t origin; /* initiator (addressed ACK) */
int kind; /* vmsig_input_kind (for cmd==0) */ int kind; /* vmsig_input_kind (for cmd==0) */
int code; /* axis/btn/evdev-code */ int code; /* btn/evdev-code/scroll-axis */
int value; /* abs/rel/down */ 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; double scroll;
int noack; /* CMD_INPUT fire-and-forget: emit no ACT_ACK */
int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */ int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */
} input_req; } 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 /* 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. */ * 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; int stub;
vmsig_emit emit; vmsig_emit emit;
vmsig_worker* worker; vmsig_worker* worker;
int driver; /* 0=QMP, 1=UINPUT (VMCTL_DRIVER_*); carried open->attach */ const char* qmp_path; /* borrowed from cfg (valid through attach); SERVICE power/lifecycle */
const char* qmp_path; /* borrowed from cfg (valid through attach) */
const char* input_bus;
int ptr_mode;
#ifdef VMSIG_WITH_VMCTL #ifdef VMSIG_WITH_VMCTL
vmctl_t* vmctl; vmctl_t* vmctl;
#endif #endif
@@ -52,14 +52,22 @@ static int input_job(void* user, const void* reqp, void* resp) {
memset(rs, 0, sizeof *rs); memset(rs, 0, sizeof *rs);
rs->corr = rq->corr; rs->corr = rq->corr;
rs->origin = rq->origin; rs->origin = rq->origin;
rs->noack = rq->noack;
#ifdef VMSIG_WITH_VMCTL #ifdef VMSIG_WITH_VMCTL
if (a->vmctl) { if (a->vmctl) {
int r = -1; int r = -1;
if (rq->cmd == 0) { 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); vmctl_batch b; vmctl_batch_init(&b);
switch (rq->kind) { switch (rq->kind) {
case VMSIG_INPUT_ABS: vmctl_batch_abs(&b, rq->code, rq->value); break; case VMSIG_INPUT_MOVE_ABS:
case VMSIG_INPUT_REL: vmctl_batch_rel(&b, rq->code, rq->value); break; 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_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_KEY: vmctl_batch_key(&b, rq->code, rq->value); break;
case VMSIG_INPUT_SCROLL: vmctl_batch_scroll(&b, rq->code, rq->scroll); 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; if (!a) return NULL;
a->endpoint = endpoint; a->endpoint = endpoint;
a->stub = c ? c->stub : 1; a->stub = c ? c->stub : 1;
if (c) { /* carry the driver selection to attach (cfg not passed there) */ if (c) a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */
a->driver = c->driver;
a->qmp_path = c->qmp_path;
a->input_bus = c->input_bus;
a->ptr_mode = c->ptr_mode;
}
return a; 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 #ifdef VMSIG_WITH_VMCTL
if (!a->stub) { if (!a->stub) {
/* armed: build vmctl_config from the carried cfg and open the actuator. UINPUT /* armed: open the actuator. Injection is ALWAYS uinput (orphaned host uinput + external
* (host uinput + optional virtio-input-host-pci passthrough via QMP) is the primary * QEMU input-linux). PTR_BOTH gives both pointer forms a device (A=abs tablet, B=rel
* input driver; QMP input-send-event is the fallback. */ * 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; vmctl_config vcfg;
memset(&vcfg, 0, sizeof 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.qmp_path = a->qmp_path;
vcfg.input_bus = a->input_bus; vcfg.input_bus = "";
vcfg.ptr_mode = a->ptr_mode; vcfg.ptr_mode = VMCTL_PTR_BOTH;
vcfg.uinput_id = NULL; /* built-in HID identity defaults */ vcfg.uinput_id = NULL; /* built-in HID identity defaults */
a->vmctl = vmctl_open(&vcfg); a->vmctl = vmctl_open(&vcfg);
if (!a->vmctl) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; } 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); vmsig_worker_ack(a->worker);
input_res rs; int rc; input_res rs; int rc;
while (vmsig_worker_poll(a->worker, &rs, sizeof rs, &rc) == 1) { 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; vmsig_event up;
memset(&up, 0, sizeof up); memset(&up, 0, sizeof up);
up.kind = VMSIG_EV_ACT_ACK; up.source = VMSIG_SRC_INPUT; up.dir = VMSIG_DIR_UP; up.kind = VMSIG_EV_ACT_ACK; up.source = VMSIG_SRC_INPUT; up.dir = VMSIG_DIR_UP;
@@ -202,7 +207,10 @@ static int in_submit(vmsig_adapter* a, const vmsig_event* ev) {
rq.kind = (int)in.kind; rq.kind = (int)in.kind;
rq.code = (int)in.code; rq.code = (int)in.code;
rq.value = (int)in.value; rq.value = (int)in.value;
rq.x = (int)in.x;
rq.y = (int)in.y;
rq.scroll = in.scroll; rq.scroll = in.scroll;
rq.noack = (in.flags & VMSIG_INPUT_F_NOACK) ? 1 : 0;
} else if (ev->kind == VMSIG_EV_CMD_LIFECYCLE) { } else if (ev->kind == VMSIG_EV_CMD_LIFECYCLE) {
rq.cmd = 1; rq.cmd = 1;
rq.life_op = (int)(unsigned char)ev->inln[0]; rq.life_op = (int)(unsigned char)ev->inln[0];
+2 -2
View File
@@ -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.prio = VMSIG_PRIO_HIGH; in.endpoint = 0; in.corr = 0xC0FFEEu;
in.payload.flags = VMSIG_PL_INLINE; in.payload.flags = VMSIG_PL_INLINE;
vmsig_input act; memset(&act, 0, sizeof act); /* neutral public input contract */ 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); 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_inproc_send(d->ctl, &in);
vmsig_event vm; vmsig_event vm;
+9
View File
@@ -85,8 +85,17 @@ static void test_held_query(void) {
vmsig_ctx_free(ctx); 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) { int main(void) {
printf("test_inputobs\n"); printf("test_inputobs\n");
test_contract_size();
test_held_query(); test_held_query();
printf("inputobs tests: %s\n", g_fail ? "FAIL" : "PASS"); printf("inputobs tests: %s\n", g_fail ? "FAIL" : "PASS");
return g_fail ? 1 : 0; return g_fail ? 1 : 0;