feat(input): daemon sets up the host->guest input-linux bridge via QMP

The uinput devices the input adapter creates were never forwarded into the
guest: the input-linux bridge was external (manual monitor object_add), so it
was lost on every reconfigure and whenever the kernel-assigned device numbers
changed. Make the vmhost seam -- which already owns the VM's single QMP
connection -- add the input-linux objects itself on reaching READY (A=keyboard
+abs with grab_all, B=mouse) and object_del them on teardown. The input adapter
publishes the uinput evdev paths into a per-endpoint home; discovery attaches
input before vmhost so the paths are ready. No second QMP socket or connection.

Also move the mouse buttons (incl. middle) and the wheel onto device B, so B is
a complete relative mouse and A is keyboard+abs only.

Bump 0.3.8.
This commit is contained in:
2026-06-24 16:31:23 +03:00
parent 929bcf0e74
commit d6c45ddb04
13 changed files with 436 additions and 55 deletions
+2
View File
@@ -23,6 +23,8 @@ struct vmctl {
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 */
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) */
/* Held-state receipt: key/btn down-bits as THIS handle last actuated them
* (not guest truth). Written only after a successful send in
+41
View File
@@ -0,0 +1,41 @@
#ifndef VMCTL_UINPUT_LAYOUT_H
#define VMCTL_UINPUT_LAYOUT_H
#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.
*
* 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. */
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 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);
role_a->present = 1;
role_a->rel_motion = (ptr_mode == VMCTL_PTR_REL);
role_a->want_keyboard = 1;
role_a->want_buttons = !both; /* B carries buttons when there are two devices */
role_a->want_wheel = !both;
role_b->present = both;
role_b->rel_motion = 1;
role_b->want_keyboard = 0;
role_b->want_buttons = both;
role_b->want_wheel = both;
if (btn_on_b) *btn_on_b = both;
}
#endif /* VMCTL_UINPUT_LAYOUT_H */
+55 -27
View File
@@ -9,12 +9,21 @@
* (device_add) and undone at close (device_del). It is NOT a per-event
* mechanism and lives entirely in the hotplug helpers below.
*
* uinput != virtio. Without qmp_path/input_bus the uinput device is created
* orphaned (an external layer may forward it). The driver switches on
* vmctl_ev_kind (never on magic numbers). */
* uinput != virtio. The created uinput evdev nodes are forwarded into the guest
* by an input-linux QMP object that the signaling vmhost seam adds over its own
* 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. */
#include "driver.h"
#include "keymap.h"
#include "uinput_layout.h"
#include <stdint.h>
#include <stdio.h>
@@ -50,29 +59,38 @@ 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); }
static int uinput_create(int rel_motion, const vmctl_uinput_id* id, const char* name, char evdev[64]) {
/* The declarative per-device role (uinput_role) and the ptr_mode -> 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]) {
int fd = open("/dev/uinput", O_RDWR | O_CLOEXEC);
if (fd < 0) return -1;
ioctl(fd, UI_SET_EVBIT, EV_SYN);
ioctl(fd, UI_SET_EVBIT, EV_KEY);
/* Keyboard keybits come from the single source of truth: every key in
* VMCTL_KEYS, so a key in the table always works through uinput too. */
for (int i = 0; i < VMCTL_KEYS_LEN; i++)
ioctl(fd, UI_SET_KEYBIT, VMCTL_KEYS[i].evdev);
for (int b = 0; b < 8; b++)
ioctl(fd, UI_SET_KEYBIT, (int)BTN_CODES[b]);
if (role->want_keyboard) {
/* Keyboard keybits come from the single source of truth: every key in
* VMCTL_KEYS, so a key in the table always works through uinput too. */
for (int i = 0; i < VMCTL_KEYS_LEN; i++)
ioctl(fd, UI_SET_KEYBIT, VMCTL_KEYS[i].evdev);
}
if (role->want_buttons) {
for (int b = 0; b < 8; b++)
ioctl(fd, UI_SET_KEYBIT, (int)BTN_CODES[b]);
}
ioctl(fd, UI_SET_EVBIT, EV_REL);
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
if (rel_motion) {
if (role->want_wheel || role->rel_motion) ioctl(fd, UI_SET_EVBIT, EV_REL);
if (role->want_wheel) {
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
}
if (role->rel_motion) {
ioctl(fd, UI_SET_RELBIT, REL_X);
ioctl(fd, UI_SET_RELBIT, REL_Y);
}
if (!rel_motion) {
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);
@@ -145,6 +163,13 @@ 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;
for (int i = 0; i < b->count; i++) {
int code = b->ev[i].code;
@@ -159,23 +184,22 @@ static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
break;
case VMCTL_EV_REL: {
if (!both && v->ptr_mode == VMCTL_PTR_ABS) return -1;
int fd = both ? fd_b : fd_a;
emit(fd, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
syn(fd);
emit(fd_rel, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
syn(fd_rel);
break;
}
case VMCTL_EV_BTN:
if (code < 0 || code >= 8) return -1;
emit(fd_a, EV_KEY, BTN_CODES[code], value);
syn(fd_a);
emit(fd_btn, EV_KEY, BTN_CODES[code], value);
syn(fd_btn);
break;
case VMCTL_EV_KEY:
emit(fd_a, EV_KEY, (uint16_t)code, value);
syn(fd_a);
break;
case VMCTL_EV_SCROLL:
emit(fd_a, EV_REL, code == VMCTL_SCROLL_V ? REL_WHEEL : REL_HWHEEL, (int32_t)scl);
syn(fd_a);
emit(fd_btn, EV_REL, code == VMCTL_SCROLL_V ? REL_WHEEL : REL_HWHEEL, (int32_t)scl);
syn(fd_btn);
break;
default:
return -1;
@@ -227,18 +251,22 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
}
char evdev_a[64], evdev_b[64];
int rel_a = (cfg->ptr_mode == VMCTL_PTR_REL);
v->ui_fd_a = uinput_create(rel_a, id, dev_a, evdev_a);
if (v->ui_fd_a < 0) { free(v); return NULL; }
uinput_role role_a, role_b;
vmctl_uinput_layout(cfg->ptr_mode, &role_a, &role_b, NULL); /* declarative A/B split */
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
v->ui_fd_b = uinput_create(1, id, dev_b, evdev_b);
v->ui_fd_a = uinput_create(&role_a, id, dev_a, evdev_a);
if (v->ui_fd_a < 0) { free(v); return NULL; }
memcpy(v->ui_evdev_a, evdev_a, sizeof v->ui_evdev_a);
if (role_b.present) {
v->ui_fd_b = uinput_create(&role_b, id, dev_b, evdev_b);
if (v->ui_fd_b < 0) {
ioctl(v->ui_fd_a, UI_DEV_DESTROY);
close(v->ui_fd_a);
free(v);
return NULL;
}
memcpy(v->ui_evdev_b, evdev_b, sizeof v->ui_evdev_b);
}
if (cfg->qmp_path) {
+10
View File
@@ -154,3 +154,13 @@ unsigned vmctl_btns_snapshot(vmctl_t* v) {
if (!v) return 0;
return v->btns_held;
}
/* ===== uinput evdev export (UINPUT-only) ===== */
int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]) {
if (!v || v->driver != VMCTL_DRIVER_UINPUT) return -1;
int n = 0;
if (a) { memcpy(a, v->ui_evdev_a, sizeof v->ui_evdev_a); if (a[0]) n++; }
if (b) { memcpy(b, v->ui_evdev_b, sizeof v->ui_evdev_b); if (b[0]) n++; }
return n;
}