3 Commits

Author SHA1 Message Date
lirent 0f452fe37c feat(input): drop absolute-pointer (ABS) support
ABS was glued onto device A alongside the keyboard and never worked right; it is
not needed in practice. Remove it entirely: device A is now keyboard-only, and
device B is the relative mouse (motion + buttons incl. middle + wheel). Drops the
ptr_mode model (one layout remains), VMCTL_EV_ABS/PTR_*, and the absolute axes.

The public input-kind enum keeps its numeric values (MOVE_REL=1, BTN=2, KEY=3,
SCROLL=4) so the wire stays compatible -- only MOVE_ABS (0) is removed and its
slot reserved; an unknown/0 kind is a no-op.

Bump 0.3.11.
2026-06-24 17:14:15 +03:00
lirent 85041c12ab fix(input): resolve the real evdev node for input-linux, not the sysfs name
UI_GET_SYSNAME returns the input-class directory name (inputNNN), not a usable
device node -- /dev/input/inputNNN does not exist, so QEMU's input-linux failed
with "Could not open". Resolve the actual evdev node (/dev/input/eventN) as the
event* child of /sys/class/input/<sysname>/. Confirmed against the live host
sysfs. The unit path can't exercise this (no /dev/uinput in CI) -- armed only.

Bump 0.3.10.
2026-06-24 16:50:05 +03:00
lirent 228dc5af79 fix(input): use the hyphenated QMP object-add/object-del command names
The 0.3.8 bridge sent object_add/object_del (underscores, modeled on the legacy
device_add) but QMP's object commands are hyphenated -- object-add/object-del --
so QEMU rejected them with CommandNotFound and no bridge was set up. Confirmed
against the live monitor: object-add/object-del exist, object_add does not, and
input-linux is a creatable type. The stub QMP in the unit test used the same
wrong string, so it didn't catch this -- only the armed monitor did.

Bump 0.3.9.
2026-06-24 16:41:46 +03:00
14 changed files with 117 additions and 167 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16)
# Single source of truth for the version: CI passes -DVMSIG_VERSION=${TAG#v}, so the project
# version (-> libvgpu-perception SONAME/.so version) and the .deb version come from one tag.
set(VMSIG_VERSION "0.3.8" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag")
set(VMSIG_VERSION "0.3.11" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag")
project(vmsig VERSION ${VMSIG_VERSION} LANGUAGES C)
set(CMAKE_C_STANDARD 17)
+5 -12
View File
@@ -15,10 +15,6 @@ typedef enum {
/* via QEMU virtio-input-host-pci (Linux). uinput != virtio. */
} vmctl_driver;
#define VMCTL_PTR_ABS 1 /* uinput: absolute tablet */
#define VMCTL_PTR_REL 2 /* uinput: relative mouse */
#define VMCTL_PTR_BOTH 3 /* uinput: two devices A=abs B=rel */
typedef struct {
unsigned bustype; /* HID bus type, e.g. 0x0003 (USB) */
unsigned vendor; /* vendor id */
@@ -31,7 +27,6 @@ typedef struct {
vmctl_driver driver;
const char* qmp_path; /* QMP unix socket; required for QMP, optional (passthrough) for UINPUT */
const char* input_bus; /* virtio-input-host-pci bus "pci.0" for passthrough; "" = none */
int ptr_mode; /* UINPUT VMCTL_PTR_*; 0 for QMP */
const vmctl_uinput_id* uinput_id; /* UINPUT only; NULL = built-in defaults */
} vmctl_config;
@@ -39,13 +34,13 @@ vmctl_t* vmctl_open (const vmctl_config* cfg); /* NULL on error */
void vmctl_close(vmctl_t* v); /* safe on NULL */
/* Copy the host evdev node paths of the created uinput devices (UINPUT driver only).
* a[] receives device A, b[] receives device B (empty if not VMCTL_PTR_BOTH); each buffer
* must be >=64 bytes. Returns the count of non-empty paths filled (0/1/2), or -1 if the
* handle's driver is not UINPUT. Paths are valid while the handle is open. */
* a[] receives device A (keyboard), b[] receives device B (relative mouse); both are always
* created, so count==2 in the normal case. Each buffer must be >=64 bytes. Returns the count
* of non-empty paths filled (0/1/2), or -1 if the handle's driver is not UINPUT. Paths are
* valid while the handle is open. */
int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]);
/* ===== Input constants ===== */
#define VMCTL_ABS_MAX 32767 /* abs coordinates 0..VMCTL_ABS_MAX */
#define VMCTL_AXIS_X 0
#define VMCTL_AXIS_Y 1
#define VMCTL_SCROLL_V 0 /* vertical */
@@ -67,13 +62,12 @@ int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]);
typedef struct {
int kind; /* internal event-kind code; set by builders */
int code; /* axis / button / evdev-code (per kind) */
int value; /* abs-value / rel-delta / down(0|1) */
int value; /* rel-delta / down(0|1) */
double scroll; /* scroll magnitude (scroll only) */
} vmctl_event;
typedef struct { vmctl_event ev[VMCTL_BATCH_MAX]; int count; } vmctl_batch;
void vmctl_batch_init (vmctl_batch* b);
void vmctl_batch_abs (vmctl_batch* b, int axis, int value);
void vmctl_batch_rel (vmctl_batch* b, int axis, int delta);
void vmctl_batch_btn (vmctl_batch* b, int btn, int down);
void vmctl_batch_key (vmctl_batch* b, int evdev_code, int down);
@@ -81,7 +75,6 @@ void vmctl_batch_scroll(vmctl_batch* b, int axis, double value);
int vmctl_batch_send (vmctl_t* v, vmctl_batch* b); /* one round-trip; 0=ok, -1=err */
/* ===== Single events (wrappers over a 1-event batch) ===== */
int vmctl_abs (vmctl_t* v, int axis, int value); /* 0..VMCTL_ABS_MAX */
int vmctl_rel (vmctl_t* v, int axis, int delta);
int vmctl_btn (vmctl_t* v, int btn, int down); /* VMCTL_BTN_* */
int vmctl_key (vmctl_t* v, int evdev_code, int down); /* Linux KEY_* */
+7 -4
View File
@@ -159,9 +159,12 @@ enum {
* 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. */
* not two independent axis updates). btn/key/scroll stay single-valued.
*
* Numbering is FROZEN: an external control encodes these on the wire and is not rebuilt from
* this header. Removing a member must NOT shift the others. */
typedef enum {
VMSIG_INPUT_MOVE_ABS = 0, /* absolute pointer: x,y are coordinates (0..VMCTL_ABS_MAX) */
/* 0 reserved (was MOVE_ABS, removed) */
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 */
@@ -175,8 +178,8 @@ typedef struct {
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 */
int32_t x; /* MOVE_REL: dx */
int32_t y; /* MOVE_REL: dy */
double scroll; /* SCROLL magnitude only */
uint32_t flags; /* VMSIG_INPUT_F_* (see above) */
uint32_t _pad; /* reserved; zero on emit */
+6 -11
View File
@@ -22,8 +22,8 @@ typedef struct {
int kind; /* vmsig_input_kind (for cmd==0) */
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 */
int x; /* MOVE_REL: dx */
int 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) */
@@ -57,10 +57,6 @@ static int input_job(void* user, const void* reqp, void* resp) {
/* 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_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);
@@ -68,7 +64,7 @@ static int input_job(void* user, const void* reqp, void* resp) {
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;
default: break;
default: break; /* unknown/0 kind (e.g. retired MOVE_ABS): no-op */
}
r = vmctl_batch_send(a->vmctl, &b);
} else {
@@ -112,15 +108,14 @@ static int in_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg
if (!a->stub) {
/* armed: open the actuator. Injection is ALWAYS uinput; the resulting evdev nodes are
* forwarded into the guest by the vmhost seam's input-linux object (published below).
* PTR_BOTH gives both pointer forms a device (A=kbd+abs tablet, B=rel mouse+buttons+
* wheel) — 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. */
* uinput always creates two devices: A=keyboard, B=relative mouse+buttons+wheel — the
* contract carries MOVE_REL (there is no absolute pointer). qmp_path serves the SERVICE
* power/lifecycle path, not input injection. */
vmctl_config vcfg;
memset(&vcfg, 0, sizeof vcfg);
vcfg.driver = VMCTL_DRIVER_UINPUT;
vcfg.qmp_path = a->qmp_path;
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; }
+1 -1
View File
@@ -10,7 +10,7 @@ typedef struct {
const char* qmp_path;
/* Host->guest input bridge: evdev node paths of the uinput devices (published by the input
* seam). When non-NULL/non-empty, on reaching READY the seam adds an input-linux QMP object
* forwarding them into the guest (A=kbd+abs with grab_all, B=mouse). NULL/"" => no bridge
* forwarding them into the guest (A=keyboard with grab_all, B=relative mouse). NULL/"" => no bridge
* (stub/tests are fail-closed). Pointers are borrowed from the stable per-endpoint home and
* outlive the adapter. */
const char* bridge_evdev_a;
+5 -3
View File
@@ -133,7 +133,9 @@ static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int g
uint32_t qid = ++a->next_id;
char line[320];
int len = snprintf(line, sizeof line,
"{\"execute\":\"object_add\",\"arguments\":{\"qom-type\":\"input-linux\","
/* QMP object commands use HYPHENS (object-add/object-del); only the legacy
* device_add/device_del keep underscores. Underscore here => CommandNotFound. */
"{\"execute\":\"object-add\",\"arguments\":{\"qom-type\":\"input-linux\","
"\"id\":\"%s\",\"evdev\":\"%s\"%s},\"id\":%u}\r\n",
id, evdev, grab_all ? ",\"grab_all\":true" : "", qid);
p->used = 1; p->id = qid; p->origin = 0; p->corr = 0; p->op = VH_OP_BRIDGE_ADD;
@@ -150,7 +152,7 @@ static void bridge_del_fire(struct vmsig_adapter* a, char ab) {
bridge_id(id, sizeof id, a->endpoint, ab);
char line[160];
int len = snprintf(line, sizeof line,
"{\"execute\":\"object_del\",\"arguments\":{\"id\":\"%s\"}}\r\n", id);
"{\"execute\":\"object-del\",\"arguments\":{\"id\":\"%s\"}}\r\n", id);
ssize_t r = write(a->fd, line, (size_t)len);
(void)r;
}
@@ -227,7 +229,7 @@ static void handle_line(struct vmsig_adapter* a, const char* line) {
/* Bridge infrastructure: never surfaces to control. Log a failed add so the
* stand can see it; otherwise silent. */
if (p->op == VH_OP_BRIDGE_ADD && strstr(line, "\"error\""))
fprintf(stderr, "vmsig vmhost: input-linux object_add failed: %s\n", line);
fprintf(stderr, "vmsig vmhost: input-linux object-add failed: %s\n", line);
} else if (p->op == VMSIG_VMOP_QUERY && strstr(line, "\"return\"")) {
char stbuf[32]; uint32_t s = VMSIG_VM_UNKNOWN;
if (jstr(line, "\"status\"", stbuf, sizeof stbuf)) s = status_state(stbuf);
+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_MOVE_ABS; act.x = 100; act.y = 100; /* demo: abs pointer (100,100) */
act.kind = VMSIG_INPUT_MOVE_REL; act.x = 5; act.y = 5; /* demo: relative move (dx=5,dy=5) */
memcpy(in.inln, &act, sizeof act);
printf(" DOWN CMD_INPUT MOVE_ABS x=100 y=100 corr=0x%X\n", (unsigned)in.corr);
printf(" DOWN CMD_INPUT MOVE_REL dx=5 dy=5 corr=0x%X\n", (unsigned)in.corr);
vmsig_inproc_send(d->ctl, &in);
vmsig_event vm;
+3 -4
View File
@@ -8,7 +8,7 @@
* driver switches on (never on magic numbers). */
typedef enum {
VMCTL_EV_ABS, VMCTL_EV_REL, VMCTL_EV_BTN, VMCTL_EV_KEY, VMCTL_EV_SCROLL
VMCTL_EV_REL, VMCTL_EV_BTN, VMCTL_EV_KEY, VMCTL_EV_SCROLL
} vmctl_ev_kind;
typedef struct {
@@ -20,9 +20,8 @@ struct vmctl {
vmctl_driver_ops ops;
vmctl_driver driver;
qmp_conn* qmp; /* control channel; NULL if none */
int ui_fd_a; /* uinput driver: device A; -1 for QMP */
int ui_fd_b; /* uinput driver: device B (BOTH); -1 */
int ptr_mode; /* uinput driver: VMCTL_PTR_*; 0 for QMP */
int ui_fd_a; /* uinput driver: device A (keyboard); -1 for QMP */
int ui_fd_b; /* uinput driver: device B (relative mouse); -1 */
char ui_evdev_a[64]; /* uinput driver: /dev/input/eventN of A ("" if none) */
char ui_evdev_b[64]; /* uinput driver: /dev/input/eventN of B ("" if none) */
+17 -19
View File
@@ -3,39 +3,37 @@
#include "vmctl.h"
/* uinput_layout.h — DECLARATIVE capability split for the uinput driver, kept pure (no ioctl)
* so it is unit-testable without /dev/uinput. The roles are derived from ptr_mode, NOT inferred
* as a side effect of rel_motion; the hot path's button/wheel carrier follows the same rule.
* so it is unit-testable without /dev/uinput. The roles are passed into the driver as DATA, not
* inferred as a side effect of one another; the hot path's button/wheel carrier follows the same
* rule.
*
* Layout: device A always carries the keyboard. Mouse buttons + scroll wheel ride the device
* carrying the relative pointer (B in BOTH, A in REL-only); with no relative pointer (ABS-only)
* they fall back to A. So in BOTH: A=keyboard+abs, B=rel+buttons+wheel. */
* Layout is CONSTANT (no absolute pointer): device A = keyboard only; device B = relative pointer
* + mouse buttons + scroll wheel. Buttons + wheel ride device B (the relative-pointer carrier). */
typedef struct {
int present; /* 1 if this device is created for the given ptr_mode */
int rel_motion; /* advertise relative X/Y (else absolute X/Y) */
int present; /* 1 if this device is created */
int rel_motion; /* advertise relative X/Y (no device advertises abs) */
int want_keyboard; /* advertise the keyboard keymap */
int want_buttons; /* advertise the 8 mouse buttons */
int want_wheel; /* advertise REL_WHEEL / REL_HWHEEL */
} uinput_role;
/* Fill role_a/role_b from ptr_mode (VMCTL_PTR_*). Sets *btn_on_b to 1 when the button/wheel
* carrier on the hot path is device B (only in PTR_BOTH). role_b.present is 0 unless BOTH. */
static inline void vmctl_uinput_layout(int ptr_mode, uinput_role* role_a, uinput_role* role_b,
int* btn_on_b) {
int both = (ptr_mode == VMCTL_PTR_BOTH);
/* Fill role_a/role_b with the constant layout. *btn_on_b is always 1: the button/wheel carrier
* on the hot path is device B (the relative-pointer device). Both devices are always present. */
static inline void vmctl_uinput_layout(uinput_role* role_a, uinput_role* role_b, int* btn_on_b) {
role_a->present = 1;
role_a->rel_motion = (ptr_mode == VMCTL_PTR_REL);
role_a->rel_motion = 0; /* keyboard-only: no pointer on A */
role_a->want_keyboard = 1;
role_a->want_buttons = !both; /* B carries buttons when there are two devices */
role_a->want_wheel = !both;
role_a->want_buttons = 0;
role_a->want_wheel = 0;
role_b->present = both;
role_b->present = 1;
role_b->rel_motion = 1;
role_b->want_keyboard = 0;
role_b->want_buttons = both;
role_b->want_wheel = both;
role_b->want_buttons = 1;
role_b->want_wheel = 1;
if (btn_on_b) *btn_on_b = both;
if (btn_on_b) *btn_on_b = 1;
}
#endif /* VMCTL_UINPUT_LAYOUT_H */
+41 -50
View File
@@ -14,12 +14,10 @@
* connection (the evdev paths are exported via vmctl_uinput_evdev). The driver
* switches on vmctl_ev_kind (never on magic numbers).
*
* Capability layout (VMCTL_PTR_BOTH): keyboard + absolute pointer on device A;
* relative pointer + mouse buttons + scroll wheel on device B. Buttons/wheel ride
* the device carrying the relative pointer (B in BOTH, A in REL-only); with no
* relative pointer (ABS-only) they fall back to A. This split is DECLARATIVE: the
* roles (want_buttons/want_wheel/rel_motion) are passed into uinput_create, not
* inferred from rel_motion as a side effect. */
* Capability layout (constant, no absolute pointer): device A = keyboard only;
* device B = relative pointer + mouse buttons + scroll wheel. Buttons/wheel ride
* device B (the relative-pointer carrier). This split is DECLARATIVE: the roles
* (want_buttons/want_wheel/rel_motion) are passed into uinput_create as data. */
#include "driver.h"
#include "keymap.h"
@@ -31,6 +29,7 @@
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <linux/uinput.h>
#include <linux/input-event-codes.h>
@@ -59,7 +58,7 @@ static void emit(int fd, uint16_t type, uint16_t code, int32_t val) {
static void syn(int fd) { emit(fd, EV_SYN, SYN_REPORT, 0); }
/* The declarative per-device role (uinput_role) and the ptr_mode -> A/B split live in
/* The declarative per-device role (uinput_role) and the constant A/B split live in
* uinput_layout.h so the layout is unit-testable without /dev/uinput. */
static int uinput_create(const uinput_role* role, const vmctl_uinput_id* id,
const char* name, char evdev[64]) {
@@ -90,21 +89,6 @@ static int uinput_create(const uinput_role* role, const vmctl_uinput_id* id,
ioctl(fd, UI_SET_RELBIT, REL_Y);
}
if (!role->rel_motion) {
ioctl(fd, UI_SET_EVBIT, EV_ABS);
ioctl(fd, UI_SET_ABSBIT, ABS_X);
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
struct uinput_abs_setup ax;
memset(&ax, 0, sizeof ax);
ax.code = ABS_X;
ax.absinfo.minimum = 0;
ax.absinfo.maximum = VMCTL_ABS_MAX;
ioctl(fd, UI_ABS_SETUP, &ax);
ax.code = ABS_Y;
ioctl(fd, UI_ABS_SETUP, &ax);
}
struct uinput_setup us;
memset(&us, 0, sizeof us);
us.id.bustype = (uint16_t)id->bustype;
@@ -120,8 +104,28 @@ static int uinput_create(const uinput_role* role, const vmctl_uinput_id* id,
char sysname[64] = {0};
evdev[0] = '\0';
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0)
snprintf(evdev, 64, "/dev/input/%s", sysname);
/* UI_GET_SYSNAME returns the input-class directory name (e.g. "input174"), NOT the usable
* device node: a bare /dev/input/<sysname> does not exist (QEMU input-linux fails to open
* it). The evdev character device is /dev/input/eventN, exposed as the "event*" child of
* /sys/class/input/<sysname>/ — resolve it there. */
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0) {
char dpath[128];
snprintf(dpath, sizeof dpath, "/sys/class/input/%s", sysname);
DIR* dir = opendir(dpath);
if (dir) {
struct dirent* de;
while ((de = readdir(dir)) != NULL)
if (strncmp(de->d_name, "event", 5) == 0) {
size_t nl = strlen(de->d_name);
if (nl + 11 < 64) { /* "/dev/input/" is 11 chars + name + NUL */
memcpy(evdev, "/dev/input/", 11);
memcpy(evdev + 11, de->d_name, nl + 1);
}
break;
}
closedir(dir);
}
}
if (!evdev[0]) {
ioctl(fd, UI_DEV_DESTROY);
@@ -162,14 +166,10 @@ static void qmp_unplug(qmp_conn* qmp, const char* id) {
static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
int fd_a = v->ui_fd_a;
int fd_b = v->ui_fd_b;
int both = (fd_b >= 0);
/* Relative motion, mouse buttons and the scroll wheel all ride ONE carrier device — the
* relative-pointer device. Selected once from the same declarative layout used at create
* time (btn_on_b == carrier is B), so the hot path and the advertised capabilities agree. */
uinput_role ra, rb; int btn_on_b = 0;
vmctl_uinput_layout(v->ptr_mode, &ra, &rb, &btn_on_b);
int fd_rel = btn_on_b ? fd_b : fd_a;
int fd_btn = fd_rel;
/* Relative motion, mouse buttons and the scroll wheel all ride device B (the relative-pointer
* carrier), matching the constant layout used at create time; the keyboard rides device A. */
int fd_rel = fd_b;
int fd_btn = fd_b;
for (int i = 0; i < b->count; i++) {
int code = b->ev[i].code;
@@ -177,13 +177,7 @@ static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
double scl = b->ev[i].scroll;
switch ((vmctl_ev_kind)b->ev[i].kind) {
case VMCTL_EV_ABS:
if (v->ptr_mode == VMCTL_PTR_REL) return -1;
emit(fd_a, EV_ABS, code == VMCTL_AXIS_X ? ABS_X : ABS_Y, value);
syn(fd_a);
break;
case VMCTL_EV_REL: {
if (!both && v->ptr_mode == VMCTL_PTR_ABS) return -1;
emit(fd_rel, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
syn(fd_rel);
break;
@@ -236,13 +230,13 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
const char* base = (cfg->uinput_id && cfg->uinput_id->name && cfg->uinput_id->name[0])
? cfg->uinput_id->name : NULL;
/* A/B suffix is added by the library only when two devices are created
* (VMCTL_PTR_BOTH) and only over a caller-supplied base name. */
/* Two devices are always created (A=keyboard, B=relative mouse); the A/B suffix is added by
* the library over a caller-supplied base name. */
char name_a[UINPUT_MAX_NAME_SIZE];
char name_b[UINPUT_MAX_NAME_SIZE];
const char* dev_a = base ? base : HWID_NAME_A;
const char* dev_b = HWID_NAME_B;
if (cfg->ptr_mode == VMCTL_PTR_BOTH && base) {
if (base) {
int base_max = (int)(sizeof name_a - 1 /*NUL*/ - 2 /*"-A"*/);
snprintf(name_a, sizeof name_a, "%.*s-A", base_max, base);
snprintf(name_b, sizeof name_b, "%.*s-B", base_max, base);
@@ -252,7 +246,7 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
char evdev_a[64], evdev_b[64];
uinput_role role_a, role_b;
vmctl_uinput_layout(cfg->ptr_mode, &role_a, &role_b, NULL); /* declarative A/B split */
vmctl_uinput_layout(&role_a, &role_b, NULL); /* constant A/B split */
v->ui_fd_a = uinput_create(&role_a, id, dev_a, evdev_a);
if (v->ui_fd_a < 0) { free(v); return NULL; }
@@ -284,19 +278,16 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
free(v);
return NULL;
}
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
if (qmp_plug(v->qmp, cfg->input_bus, evdev_b, PLUG_ID_B) < 0) {
qmp_unplug(v->qmp, PLUG_ID_A);
uinput_driver_close(v);
free(v);
return NULL;
}
if (qmp_plug(v->qmp, cfg->input_bus, evdev_b, PLUG_ID_B) < 0) {
qmp_unplug(v->qmp, PLUG_ID_A);
uinput_driver_close(v);
free(v);
return NULL;
}
}
}
v->ops.send = uinput_driver_send;
v->ops.close = uinput_driver_close;
v->ptr_mode = cfg->ptr_mode;
return v;
}
+2 -15
View File
@@ -29,12 +29,6 @@ void vmctl_batch_init(vmctl_batch* b) {
b->count = 0;
}
void vmctl_batch_abs(vmctl_batch* b, int axis, int value) {
if (b->count >= VMCTL_BATCH_MAX) return;
vmctl_event* e = &b->ev[b->count++];
e->kind = VMCTL_EV_ABS; e->code = axis; e->value = value; e->scroll = 0.0;
}
void vmctl_batch_rel(vmctl_batch* b, int axis, int delta) {
if (b->count >= VMCTL_BATCH_MAX) return;
vmctl_event* e = &b->ev[b->count++];
@@ -65,7 +59,7 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
if (rc != 0) return rc; /* not sent = not recorded; never touch the receipt */
/* Record the actuated key/btn down-bits (write-only; the send path above
* never reads this map). abs/rel/scroll have no held state. */
* never reads this map). rel/scroll have no held state. */
for (int i = 0; i < b->count; i++) {
const vmctl_event* e = &b->ev[i];
int down = e->value ? 1 : 0;
@@ -86,7 +80,7 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
else v->btns_held &= ~mask;
break;
}
default: break; /* abs/rel/scroll: no-op for receipt */
default: break; /* rel/scroll: no-op for receipt */
}
}
return rc;
@@ -94,13 +88,6 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
/* ===== Single-event wrappers ===== */
int vmctl_abs(vmctl_t* v, int axis, int value) {
vmctl_batch b;
vmctl_batch_init(&b);
vmctl_batch_abs(&b, axis, value);
return vmctl_batch_send(v, &b);
}
int vmctl_rel(vmctl_t* v, int axis, int delta) {
vmctl_batch b;
vmctl_batch_init(&b);
-6
View File
@@ -29,11 +29,6 @@ static int qmp_driver_send(vmctl_t* v, const vmctl_batch* b) {
double scl = b->ev[i].scroll;
switch ((vmctl_ev_kind)b->ev[i].kind) {
case VMCTL_EV_ABS:
pos += snprintf(json + pos, (int)sizeof json - pos,
"{\"type\":\"abs\",\"data\":{\"axis\":\"%s\",\"value\":%d}}",
code == VMCTL_AXIS_X ? "x" : "y", value);
break;
case VMCTL_EV_REL:
pos += snprintf(json + pos, (int)sizeof json - pos,
"{\"type\":\"rel\",\"data\":{\"axis\":\"%s\",\"value\":%d}}",
@@ -87,7 +82,6 @@ vmctl_t* vmctl_open_qmp_driver(const vmctl_config* cfg) {
v->qmp = qmp;
v->ui_fd_a = -1;
v->ui_fd_b = -1;
v->ptr_mode = 0;
v->ops.send = qmp_driver_send;
v->ops.close = qmp_driver_close;
return v;
+23 -35
View File
@@ -1,10 +1,10 @@
/* test_uinputlayout.c — DECLARATIVE uinput capability split (pure, no /dev/uinput).
*
* Verifies the ptr_mode -> A/B role mapping that drives both device creation and the hot-path
* button/wheel carrier selection: in PTR_BOTH A is keyboard+abs and B is rel+buttons+wheel, and
* the button/wheel carrier is B; single-pointer modes keep buttons+wheel on the sole device.
* The actuation ioctls remain armed-only (they need a real /dev/uinput); this covers the logic
* that decides the layout, which is the part that single-mode regressions would break. */
* Verifies the CONSTANT A/B role mapping that drives both device creation and the hot-path
* button/wheel carrier selection: device A = keyboard only, device B = relative pointer + buttons
* + wheel, and the button/wheel carrier is B. There is no absolute pointer anywhere — the abs role
* has been removed and is unrepresentable (no abs field exists in uinput_role). The actuation
* ioctls remain armed-only (they need a real /dev/uinput); this covers the layout logic. */
#include "vmctl.h"
#include "uinput_layout.h"
#include <stdio.h>
@@ -17,37 +17,25 @@ static int g_fail = 0;
int main(void) {
uinput_role a, b; int btn_on_b;
/* PTR_BOTH: A = keyboard + absolute pointer, no buttons/wheel; B = relative pointer +
* buttons + wheel; carrier is B. This is the requested layout (mouse buttons incl. middle
* and the wheel moved off A onto B). */
vmctl_uinput_layout(VMCTL_PTR_BOTH, &a, &b, &btn_on_b);
CHECK(a.present && b.present, "BOTH: two devices");
CHECK(a.want_keyboard, "BOTH: A has keyboard");
CHECK(!a.rel_motion, "BOTH: A is absolute");
CHECK(!a.want_buttons, "BOTH: A has NO mouse buttons");
CHECK(!a.want_wheel, "BOTH: A has NO wheel");
CHECK(!b.want_keyboard, "BOTH: B has no keyboard");
CHECK(b.rel_motion, "BOTH: B is relative");
CHECK(b.want_buttons, "BOTH: B has mouse buttons");
CHECK(b.want_wheel, "BOTH: B has wheel");
CHECK(btn_on_b == 1, "BOTH: button/wheel carrier is B");
/* Constant layout: A = keyboard only (no pointer, no buttons/wheel); B = relative pointer +
* buttons + wheel; the button/wheel carrier is B. */
vmctl_uinput_layout(&a, &b, &btn_on_b);
CHECK(a.present && b.present, "two devices");
CHECK(a.want_keyboard, "A has keyboard");
CHECK(!a.rel_motion, "A has no pointer (keyboard-only)");
CHECK(!a.want_buttons, "A has NO mouse buttons");
CHECK(!a.want_wheel, "A has NO wheel");
CHECK(!b.want_keyboard, "B has no keyboard");
CHECK(b.rel_motion, "B is relative");
CHECK(b.want_buttons, "B has mouse buttons");
CHECK(b.want_wheel, "B has wheel");
CHECK(btn_on_b == 1, "button/wheel carrier is B");
/* PTR_REL: single relative device A carries motion + buttons + wheel (no B). */
vmctl_uinput_layout(VMCTL_PTR_REL, &a, &b, &btn_on_b);
CHECK(a.present && !b.present, "REL: single device A");
CHECK(a.rel_motion, "REL: A is relative");
CHECK(a.want_buttons, "REL: A has buttons");
CHECK(a.want_wheel, "REL: A has wheel");
CHECK(a.want_keyboard, "REL: A has keyboard");
CHECK(btn_on_b == 0, "REL: carrier is A");
/* PTR_ABS: single absolute device A carries abs + buttons + wheel (the only device). */
vmctl_uinput_layout(VMCTL_PTR_ABS, &a, &b, &btn_on_b);
CHECK(a.present && !b.present, "ABS: single device A");
CHECK(!a.rel_motion, "ABS: A is absolute");
CHECK(a.want_buttons, "ABS: A has buttons (sole device)");
CHECK(a.want_wheel, "ABS: A has wheel (sole device)");
CHECK(btn_on_b == 0, "ABS: carrier is A");
/* No absolute pointer: the abs role is removed and unrepresentable (uinput_role carries no abs
* field). The invariant is that each device is either relative or has no pointer at all — A is
* keyboard-only (no pointer), B is relative. Neither advertises an absolute axis. */
CHECK(!a.rel_motion && !a.want_buttons && !a.want_wheel, "A is keyboard-only (no pointer)");
CHECK(b.rel_motion, "B is the relative pointer (not absolute)");
/* evdev export contract: a NULL handle reports "not a uinput handle" (-1). The populated
* path (real /dev/input/eventN) is armed-only — it needs a created uinput device. */
+4 -4
View File
@@ -142,7 +142,7 @@ int main(void) {
/* On READY the seam adds the input-linux bridge BEFORE the SEAM_UP-driven CMD_VM query
* (bridge ids 1,2; query id 3). Verify both object_add lines and their properties. */
CHECK(srv_expect(c, "object_add"), "seam sent object_add (input-linux bridge)");
CHECK(srv_expect(c, "object-add"), "seam sent object-add (input-linux bridge)");
CHECK(srv_expect(c, "\"input-linux\""), "bridge object uses qom-type input-linux");
CHECK(srv_expect(c, "\"vmsig-in-a-0\""), "bridge A has neutral per-endpoint id");
CHECK(srv_expect(c, "\"vmsig-in-b-0\""), "bridge B has neutral per-endpoint id");
@@ -224,12 +224,12 @@ int main(void) {
pthread_join(th2, NULL);
vmsig_core_free(core2);
for (int i = 0; i < 50 && !strstr(g_rx, "object_del"); i++) {
for (int i = 0; i < 50 && !strstr(g_rx, "object-del"); i++) {
rx_pump(c2);
struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL);
}
const char* d = strstr(g_rx, "object_del");
CHECK(d != NULL, "scenario2: teardown fired object_del");
const char* d = strstr(g_rx, "object-del");
CHECK(d != NULL, "scenario2: teardown fired object-del");
CHECK(strstr(g_rx, "vmsig-in-a-0"), "scenario2: object_del for bridge A");
CHECK(strstr(g_rx, "vmsig-in-b-0"), "scenario2: object_del for bridge B");
close(c2);