mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-25 20:36:36 +03:00
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:
+1
-1
@@ -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)
|
||||
|
||||
+21
-11
@@ -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_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
|
||||
|
||||
@@ -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
|
||||
|
||||
+29
-21
@@ -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;
|
||||
@@ -202,7 +207,10 @@ static int in_submit(vmsig_adapter* a, const vmsig_event* ev) {
|
||||
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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user