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
+4 -6
View File
@@ -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
+32 -24
View File
@@ -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];
+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.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;
+9
View File
@@ -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;