vmsig: management daemon, runtime endpoint lifecycle, roster, discovery, in-tree drivers, packaging

- core: runtime attach/detach of a per-endpoint adapter trio (runtime-safe add_adapter + vmsig_core_detach_endpoint, deferred reap)
- roster: VMSIG_EV_ROSTER + CAP_ROSTER, retained per-endpoint and replayed to late subscribers
- discovery: inotify trigger dir, vmid/endpoint slot allocator, host probe; vmsigd daemon with config + per-uid admission
- input driver and vgpu perception built in-tree; vgpu perception as a separate library
- memctx: own the supplied ro_fd (closed at detach)
- deb packaging: install rules, systemd unit, tmpfiles, default config
This commit is contained in:
2026-06-22 17:25:06 +03:00
parent 0d387a4249
commit 9bde398b6c
55 changed files with 4703 additions and 61 deletions
+39
View File
@@ -0,0 +1,39 @@
#ifndef VMCTL_DRIVER_H
#define VMCTL_DRIVER_H
#include "vmctl.h"
#include "qmp.h"
/* driver.h — input-driver vtable, the concrete vmctl handle, and the shared
* event-kind enum. The event kind is the SINGLE source of truth that every
* 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_kind;
typedef struct {
int (*send)(vmctl_t* v, const vmctl_batch* b); /* deliver an input batch */
void (*close)(vmctl_t* v); /* release driver resources */
} vmctl_driver_ops;
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 */
/* Held-state receipt: key/btn down-bits as THIS handle last actuated them
* (not guest truth). Written only after a successful send in
* vmctl_batch_send; the send path never reads them. Zero-initialised by
* calloc at open = all up. Single-threaded (one handle owner): no locks. */
unsigned char keys_held[VMCTL_KEYS_SNAPSHOT_BYTES]; /* evdev-indexed key down-bits */
unsigned btns_held; /* VMCTL_BTN_* 0..7 down-bits */
};
/* driver factories (called from open.c per cfg->driver) */
vmctl_t* vmctl_open_qmp_driver (const vmctl_config* cfg);
vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg);
#endif /* VMCTL_DRIVER_H */
+18
View File
@@ -0,0 +1,18 @@
#ifndef VMCTL_KEYMAP_H
#define VMCTL_KEYMAP_H
#include <stddef.h>
/* keymap.h — the single source of truth for keyboard keys. One descriptor maps
* a Linux evdev code to a QEMU QKeyCode name. Both the QMP and uinput drivers
* derive everything from this table. */
/* NOTE: named vmctl_keymap, not vmctl_key — the public API uses the ordinary
* identifier vmctl_key for the key-injection function (include/vmctl.h), and a
* typedef would collide with it. */
typedef struct { int evdev; const char* qcode; } vmctl_keymap;
extern const vmctl_keymap VMCTL_KEYS[]; /* sorted by evdev (for bsearch) */
extern const int VMCTL_KEYS_LEN;
const char* vmctl_evdev_to_qcode(int evdev); /* NULL if absent */
#endif /* VMCTL_KEYMAP_H */
+14
View File
@@ -0,0 +1,14 @@
#ifndef VMCTL_QMP_H
#define VMCTL_QMP_H
#include <stddef.h>
/* qmp.h — minimal QMP client over an AF_UNIX socket: connect (with capability
* negotiation), disconnect, and synchronous command execution. */
typedef struct qmp_conn qmp_conn;
qmp_conn* qmp_connect(const char* sock_path); /* connect + qmp_capabilities; NULL on error */
void qmp_disconnect(qmp_conn* c);
int qmp_exec(qmp_conn* c, const char* cmd, char* resp, size_t cap); /* 0=return, -1=error */
#endif /* VMCTL_QMP_H */
+115
View File
@@ -0,0 +1,115 @@
/* keymap.c — the single source of truth for keyboard keys. VMCTL_KEYS maps
* Linux evdev codes to QEMU QKeyCode names (sorted by evdev for bsearch);
* vmctl_evdev_to_qcode is the sole lookup, consumed by the QMP driver. */
#include "keymap.h"
#include <linux/input-event-codes.h>
#include <stdlib.h>
const vmctl_keymap VMCTL_KEYS[] = {
{ KEY_ESC, "esc" },
{ KEY_1, "1" },
{ KEY_2, "2" },
{ KEY_3, "3" },
{ KEY_4, "4" },
{ KEY_5, "5" },
{ KEY_6, "6" },
{ KEY_7, "7" },
{ KEY_8, "8" },
{ KEY_9, "9" },
{ KEY_0, "0" },
{ KEY_MINUS, "minus" },
{ KEY_EQUAL, "equal" },
{ KEY_BACKSPACE, "backspace" },
{ KEY_TAB, "tab" },
{ KEY_Q, "q" },
{ KEY_W, "w" },
{ KEY_E, "e" },
{ KEY_R, "r" },
{ KEY_T, "t" },
{ KEY_Y, "y" },
{ KEY_U, "u" },
{ KEY_I, "i" },
{ KEY_O, "o" },
{ KEY_P, "p" },
{ KEY_LEFTBRACE, "bracket_left" },
{ KEY_RIGHTBRACE, "bracket_right" },
{ KEY_ENTER, "ret" },
{ KEY_LEFTCTRL, "ctrl" },
{ KEY_A, "a" },
{ KEY_S, "s" },
{ KEY_D, "d" },
{ KEY_F, "f" },
{ KEY_G, "g" },
{ KEY_H, "h" },
{ KEY_J, "j" },
{ KEY_K, "k" },
{ KEY_L, "l" },
{ KEY_SEMICOLON, "semicolon" },
{ KEY_APOSTROPHE, "apostrophe" },
{ KEY_GRAVE, "grave_accent" },
{ KEY_LEFTSHIFT, "shift" },
{ KEY_BACKSLASH, "backslash" },
{ KEY_Z, "z" },
{ KEY_X, "x" },
{ KEY_C, "c" },
{ KEY_V, "v" },
{ KEY_B, "b" },
{ KEY_N, "n" },
{ KEY_M, "m" },
{ KEY_COMMA, "comma" },
{ KEY_DOT, "dot" },
{ KEY_SLASH, "slash" },
{ KEY_RIGHTSHIFT, "shift_r" },
{ KEY_LEFTALT, "alt" },
{ KEY_SPACE, "spc" },
{ KEY_CAPSLOCK, "caps_lock" },
{ KEY_F1, "f1" },
{ KEY_F2, "f2" },
{ KEY_F3, "f3" },
{ KEY_F4, "f4" },
{ KEY_F5, "f5" },
{ KEY_F6, "f6" },
{ KEY_F7, "f7" },
{ KEY_F8, "f8" },
{ KEY_F9, "f9" },
{ KEY_F10, "f10" },
{ KEY_NUMLOCK, "num_lock" },
{ KEY_SCROLLLOCK, "scroll_lock" },
{ KEY_102ND, "less" },
{ KEY_F11, "f11" },
{ KEY_F12, "f12" },
{ KEY_RIGHTCTRL, "ctrl_r" },
{ KEY_SYSRQ, "print" },
{ KEY_RIGHTALT, "alt_r" },
{ KEY_HOME, "home" },
{ KEY_UP, "up" },
{ KEY_PAGEUP, "pgup" },
{ KEY_LEFT, "left" },
{ KEY_RIGHT, "right" },
{ KEY_END, "end" },
{ KEY_DOWN, "down" },
{ KEY_PAGEDOWN, "pgdn" },
{ KEY_INSERT, "insert" },
{ KEY_DELETE, "delete" },
{ KEY_POWER, "power" },
{ KEY_PAUSE, "pause" },
{ KEY_LEFTMETA, "meta_l" },
{ KEY_RIGHTMETA, "meta_r" },
{ KEY_SLEEP, "sleep" },
{ KEY_WAKEUP, "wake" },
};
const int VMCTL_KEYS_LEN = (int)(sizeof VMCTL_KEYS / sizeof VMCTL_KEYS[0]);
static int key_cmp(const void* a, const void* b) {
return ((const vmctl_keymap*)a)->evdev - ((const vmctl_keymap*)b)->evdev;
}
const char* vmctl_evdev_to_qcode(int evdev) {
vmctl_keymap k = { .evdev = evdev, .qcode = NULL };
const vmctl_keymap* e = bsearch(&k, VMCTL_KEYS, (size_t)VMCTL_KEYS_LEN,
sizeof VMCTL_KEYS[0], key_cmp);
return e ? e->qcode : NULL;
}
+274
View File
@@ -0,0 +1,274 @@
/* uinput_driver.c — Linux uinput input driver (host source) plus optional
* passthrough into the guest. TWO distinct layers, not to be confused:
*
* (1) uinput — the host side: the library creates a /dev/input/eventN node
* and writes struct input_event into it on the hot path (uinput_driver_send).
*
* (2) virtio-input-host-pci — a QEMU device that forwards that host evdev node
* into the guest. It is an OPTIONAL setup step performed over QMP at open
* (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). */
#include "driver.h"
#include "keymap.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/uinput.h>
#include <linux/input-event-codes.h>
/* HID identity of the synthesized device (values preserved — behaviour unchanged). */
#define HWID_BUS 0x0003
#define HWID_VENDOR 0x046D
#define HWID_PRODUCT 0xC52B
#define HWID_VERSION 0x0111
#define HWID_NAME_A "VMInput-A"
#define HWID_NAME_B "VMInput-B"
/* Hotplug device ids for virtio-input-host-pci passthrough. */
#define PLUG_ID_A "vmctl-a"
#define PLUG_ID_B "vmctl-b"
static const uint16_t BTN_CODES[8] = {
0x110, 0x111, 0x112, 0x113, 0x114, 0x115, 0x116, 0x117
};
static void emit(int fd, uint16_t type, uint16_t code, int32_t val) {
struct input_event e = {.type = type, .code = code, .value = val};
ssize_t r = write(fd, &e, sizeof e);
(void)r;
}
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]) {
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]);
ioctl(fd, UI_SET_EVBIT, EV_REL);
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
if (rel_motion) {
ioctl(fd, UI_SET_RELBIT, REL_X);
ioctl(fd, UI_SET_RELBIT, REL_Y);
}
if (!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;
us.id.vendor = (uint16_t)id->vendor;
us.id.product = (uint16_t)id->product;
us.id.version = (uint16_t)id->version;
strncpy(us.name, name, sizeof us.name - 1);
if (ioctl(fd, UI_DEV_SETUP, &us) < 0 || ioctl(fd, UI_DEV_CREATE) < 0) {
close(fd);
return -1;
}
char sysname[64] = {0};
evdev[0] = '\0';
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0)
snprintf(evdev, 64, "/dev/input/%s", sysname);
if (!evdev[0]) {
ioctl(fd, UI_DEV_DESTROY);
close(fd);
return -1;
}
return fd;
}
/* ===== virtio-input-host-pci passthrough (layer 2, optional, QMP setup) ===== */
static int qmp_plug(qmp_conn* qmp, const char* bus, const char* evdev, const char* id) {
char cmd[512], resp[1024];
snprintf(cmd, sizeof cmd,
"{\"execute\":\"device_del\",\"arguments\":{\"id\":\"%s\"}}", id);
qmp_exec(qmp, cmd, resp, sizeof resp);
snprintf(cmd, sizeof cmd,
"{\"execute\":\"device_add\",\"arguments\":{"
"\"driver\":\"virtio-input-host-pci\","
"\"id\":\"%s\","
"\"evdev\":\"%s\","
"\"bus\":\"%s\"}}",
id, evdev, bus);
return qmp_exec(qmp, cmd, resp, sizeof resp);
}
static void qmp_unplug(qmp_conn* qmp, const char* id) {
char cmd[256], resp[1024];
snprintf(cmd, sizeof cmd,
"{\"execute\":\"device_del\",\"arguments\":{\"id\":\"%s\"}}", id);
qmp_exec(qmp, cmd, resp, sizeof resp);
}
/* ===== hot path (layer 1, uinput write) ===== */
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);
for (int i = 0; i < b->count; i++) {
int code = b->ev[i].code;
int value = b->ev[i].value;
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;
int fd = both ? fd_b : fd_a;
emit(fd, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
syn(fd);
break;
}
case VMCTL_EV_BTN:
if (code < 0 || code >= 8) return -1;
emit(fd_a, EV_KEY, BTN_CODES[code], value);
syn(fd_a);
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);
break;
default:
return -1;
}
}
return 0;
}
static void uinput_driver_close(vmctl_t* v) {
if (v->qmp) {
qmp_unplug(v->qmp, PLUG_ID_A);
if (v->ui_fd_b >= 0) qmp_unplug(v->qmp, PLUG_ID_B);
qmp_disconnect(v->qmp);
}
if (v->ui_fd_a >= 0) { ioctl(v->ui_fd_a, UI_DEV_DESTROY); close(v->ui_fd_a); }
if (v->ui_fd_b >= 0) { ioctl(v->ui_fd_b, UI_DEV_DESTROY); close(v->ui_fd_b); }
}
vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
vmctl_t* v = calloc(1, sizeof *v);
if (!v) return NULL;
v->driver = VMCTL_DRIVER_UINPUT;
v->ui_fd_a = -1;
v->ui_fd_b = -1;
/* HID identity: NULL config selects the built-in defaults verbatim; a
* non-NULL config supplies all numeric fields literally (zeros included). */
const vmctl_uinput_id DEFAULT_ID = {
HWID_BUS, HWID_VENDOR, HWID_PRODUCT, HWID_VERSION, HWID_NAME_A
};
const vmctl_uinput_id* id = cfg->uinput_id ? cfg->uinput_id : &DEFAULT_ID;
/* Base name: caller's non-empty name, else NULL = use default A/B names. */
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. */
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) {
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);
dev_a = name_a;
dev_b = name_b;
}
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; }
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
v->ui_fd_b = uinput_create(1, 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;
}
}
if (cfg->qmp_path) {
v->qmp = qmp_connect(cfg->qmp_path);
if (!v->qmp) {
if (v->ui_fd_b >= 0) { ioctl(v->ui_fd_b, UI_DEV_DESTROY); close(v->ui_fd_b); }
ioctl(v->ui_fd_a, UI_DEV_DESTROY);
close(v->ui_fd_a);
free(v);
return NULL;
}
if (cfg->input_bus && cfg->input_bus[0]) {
if (qmp_plug(v->qmp, cfg->input_bus, evdev_a, PLUG_ID_A) < 0) {
uinput_driver_close(v);
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;
}
}
}
}
v->ops.send = uinput_driver_send;
v->ops.close = uinput_driver_close;
v->ptr_mode = cfg->ptr_mode;
return v;
}
+156
View File
@@ -0,0 +1,156 @@
/* open.c — handle lifecycle and the input batch API. vmctl_open dispatches to a
* driver factory by cfg->driver; vmctl_close releases via ops.close. The batch
* builders set vmctl_event.kind (the single event-kind code that drivers read),
* and the single-event wrappers are thin batches of one. */
#include "driver.h"
#include <stdlib.h>
#include <string.h>
vmctl_t* vmctl_open(const vmctl_config* cfg) {
if (!cfg) return NULL;
switch (cfg->driver) {
case VMCTL_DRIVER_QMP: return vmctl_open_qmp_driver(cfg);
case VMCTL_DRIVER_UINPUT: return vmctl_open_uinput_driver(cfg);
default: return NULL;
}
}
void vmctl_close(vmctl_t* v) {
if (!v) return;
v->ops.close(v);
free(v);
}
/* ===== Batch builders ===== */
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++];
e->kind = VMCTL_EV_REL; e->code = axis; e->value = delta; e->scroll = 0.0;
}
void vmctl_batch_btn(vmctl_batch* b, int btn, int down) {
if (b->count >= VMCTL_BATCH_MAX) return;
vmctl_event* e = &b->ev[b->count++];
e->kind = VMCTL_EV_BTN; e->code = btn; e->value = down; e->scroll = 0.0;
}
void vmctl_batch_key(vmctl_batch* b, int evdev_code, int down) {
if (b->count >= VMCTL_BATCH_MAX) return;
vmctl_event* e = &b->ev[b->count++];
e->kind = VMCTL_EV_KEY; e->code = evdev_code; e->value = down; e->scroll = 0.0;
}
void vmctl_batch_scroll(vmctl_batch* b, int axis, double value) {
if (b->count >= VMCTL_BATCH_MAX) return;
vmctl_event* e = &b->ev[b->count++];
e->kind = VMCTL_EV_SCROLL; e->code = axis; e->value = 0; e->scroll = value;
}
int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
if (b->count == 0) return 0;
int rc = v->ops.send(v, 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. */
for (int i = 0; i < b->count; i++) {
const vmctl_event* e = &b->ev[i];
int down = e->value ? 1 : 0;
switch (e->kind) {
case VMCTL_EV_KEY: {
int code = e->code;
if (code < 0 || code > VMCTL_KEY_CODE_MAX) break; /* out of range: ignore */
unsigned char mask = (unsigned char)(1u << (code & 7));
if (down) v->keys_held[code >> 3] |= mask;
else v->keys_held[code >> 3] &= (unsigned char)~mask;
break;
}
case VMCTL_EV_BTN: {
int btn = e->code;
if (btn < 0 || btn >= 8) break; /* out of range: ignore */
unsigned mask = 1u << btn;
if (down) v->btns_held |= mask;
else v->btns_held &= ~mask;
break;
}
default: break; /* abs/rel/scroll: no-op for receipt */
}
}
return rc;
}
/* ===== 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);
vmctl_batch_rel(&b, axis, delta);
return vmctl_batch_send(v, &b);
}
int vmctl_btn(vmctl_t* v, int btn, int down) {
vmctl_batch b;
vmctl_batch_init(&b);
vmctl_batch_btn(&b, btn, down);
return vmctl_batch_send(v, &b);
}
int vmctl_key(vmctl_t* v, int evdev_code, int down) {
vmctl_batch b;
vmctl_batch_init(&b);
vmctl_batch_key(&b, evdev_code, down);
return vmctl_batch_send(v, &b);
}
int vmctl_scroll(vmctl_t* v, int axis, double value) {
vmctl_batch b;
vmctl_batch_init(&b);
vmctl_batch_scroll(&b, axis, value);
return vmctl_batch_send(v, &b);
}
/* ===== Held-state receipt (read-only) =====
* Reads of the actuator's own last output; never mutate driver state. The
* in-range predicate matches the write path in vmctl_batch_send. */
int vmctl_key_held(vmctl_t* v, int evdev_code) {
if (!v || evdev_code < 0 || evdev_code > VMCTL_KEY_CODE_MAX) return 0;
return (v->keys_held[evdev_code >> 3] >> (evdev_code & 7)) & 1;
}
int vmctl_btn_held(vmctl_t* v, int btn) {
if (!v || btn < 0 || btn >= 8) return 0;
return (int)((v->btns_held >> btn) & 1u);
}
int vmctl_keys_snapshot(vmctl_t* v, unsigned char* bits, size_t nbytes) {
if (!v || !bits) return -1;
size_t n = nbytes < VMCTL_KEYS_SNAPSHOT_BYTES ? nbytes : VMCTL_KEYS_SNAPSHOT_BYTES;
memcpy(bits, v->keys_held, n);
return (int)n;
}
unsigned vmctl_btns_snapshot(vmctl_t* v) {
if (!v) return 0;
return v->btns_held;
}
+18
View File
@@ -0,0 +1,18 @@
/* power.c — QMP power/lifecycle actuation. This plane is orthogonal to the
* input driver and always rides the shared QMP channel; every entry returns -1
* when there is no connection. */
#include "driver.h"
/* QMP responses are small; a stack buffer suffices. */
static int qmp_simple(vmctl_t* v, const char* cmd) {
if (!v->qmp) return -1;
char resp[1024];
return qmp_exec(v->qmp, cmd, resp, sizeof resp);
}
int vmctl_powerdown(vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"system_powerdown\"}"); }
int vmctl_reset (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"system_reset\"}"); }
int vmctl_wakeup (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"system_wakeup\"}"); }
int vmctl_pause (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"stop\"}"); }
int vmctl_resume (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"cont\"}"); }
+113
View File
@@ -0,0 +1,113 @@
/* qmp.c — AF_UNIX QMP client: connect + capability handshake, line-based recv
* with a poll timeout, and synchronous command execution. */
#include "qmp.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>
#define QMP_TIMEOUT_MS 5000
#define QMP_BUF_SIZE 4096
struct qmp_conn {
int fd;
};
static int recv_line(int fd, char* buf, size_t cap) {
size_t n = 0;
while (n + 1 < cap) {
struct pollfd pfd = { .fd = fd, .events = POLLIN };
if (poll(&pfd, 1, QMP_TIMEOUT_MS) <= 0) return -1;
char c;
if (read(fd, &c, 1) != 1) return -1;
buf[n++] = c;
if (c == '\n') break;
}
buf[n] = '\0';
return (int)n;
}
static int send_all(int fd, const char* s, size_t len) {
while (len > 0) {
ssize_t w = write(fd, s, len);
if (w <= 0) return -1;
s += w;
len -= (size_t)w;
}
return 0;
}
qmp_conn* qmp_connect(const char* sock_path) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return NULL;
struct sockaddr_un addr;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, sock_path, sizeof addr.sun_path - 1);
if (connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0) {
close(fd);
return NULL;
}
char buf[QMP_BUF_SIZE];
if (recv_line(fd, buf, sizeof buf) < 0) {
close(fd);
return NULL;
}
const char* cap_cmd = "{\"execute\":\"qmp_capabilities\"}\r\n";
if (send_all(fd, cap_cmd, strlen(cap_cmd)) < 0) {
close(fd);
return NULL;
}
if (recv_line(fd, buf, sizeof buf) < 0) {
close(fd);
return NULL;
}
qmp_conn* c = malloc(sizeof *c);
if (!c) {
close(fd);
return NULL;
}
c->fd = fd;
return c;
}
void qmp_disconnect(qmp_conn* c) {
if (!c) return;
close(c->fd);
free(c);
}
int qmp_exec(qmp_conn* c, const char* cmd, char* resp, size_t cap) {
size_t cmdlen = strlen(cmd);
if (send_all(c->fd, cmd, cmdlen) < 0) return -1;
if (send_all(c->fd, "\r\n", 2) < 0) return -1;
char line[QMP_BUF_SIZE];
for (;;) {
if (recv_line(c->fd, line, sizeof line) < 0) return -1;
if (strstr(line, "\"return\"")) {
if (resp && cap > 0) {
strncpy(resp, line, cap - 1);
resp[cap - 1] = '\0';
}
return 0;
}
if (strstr(line, "\"error\"")) {
if (resp && cap > 0) {
strncpy(resp, line, cap - 1);
resp[cap - 1] = '\0';
}
return -1;
}
}
}
+94
View File
@@ -0,0 +1,94 @@
/* qmp_driver.c — QMP input driver: serialises an input batch into a single
* input-send-event command and sends it in one round-trip. No guest driver is
* required. Switches on vmctl_ev_kind (never on magic numbers). */
#include "driver.h"
#include "keymap.h"
#include <stdlib.h>
#include <stdio.h>
static const char* btn_names[] = {
"left", "right", "middle", "side", "extra", "forward", "back", "task"
};
#define BTN_NAMES_LEN ((int)(sizeof btn_names / sizeof btn_names[0]))
static int qmp_driver_send(vmctl_t* v, const vmctl_batch* b) {
char json[8192];
int pos = 0;
pos += snprintf(json + pos, (int)sizeof json - pos,
"{\"execute\":\"input-send-event\",\"arguments\":{\"events\":[");
for (int i = 0; i < b->count; i++) {
if (i > 0)
pos += snprintf(json + pos, (int)sizeof json - pos, ",");
int code = b->ev[i].code;
int value = b->ev[i].value;
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}}",
code == VMCTL_AXIS_X ? "x" : "y", value);
break;
case VMCTL_EV_BTN:
if (code < 0 || code >= BTN_NAMES_LEN) return -1;
pos += snprintf(json + pos, (int)sizeof json - pos,
"{\"type\":\"btn\",\"data\":{\"button\":\"%s\",\"down\":%s}}",
btn_names[code], value ? "true" : "false");
break;
case VMCTL_EV_KEY: {
const char* qcode = vmctl_evdev_to_qcode(code);
if (!qcode) return -1;
pos += snprintf(json + pos, (int)sizeof json - pos,
"{\"type\":\"key\",\"data\":{\"key\":{\"type\":\"qcode\","
"\"data\":\"%s\"},\"down\":%s}}",
qcode, value ? "true" : "false");
break;
}
case VMCTL_EV_SCROLL:
pos += snprintf(json + pos, (int)sizeof json - pos,
"{\"type\":\"scl\",\"data\":{\"axis\":\"%s\",\"value\":%g}}",
code == VMCTL_SCROLL_V ? "vertical" : "horizontal", scl);
break;
default:
return -1;
}
}
pos += snprintf(json + pos, (int)sizeof json - pos, "]}}");
char resp[4096];
return qmp_exec(v->qmp, json, resp, sizeof resp);
}
static void qmp_driver_close(vmctl_t* v) {
qmp_disconnect(v->qmp);
}
vmctl_t* vmctl_open_qmp_driver(const vmctl_config* cfg) {
qmp_conn* qmp = qmp_connect(cfg->qmp_path);
if (!qmp) return NULL;
vmctl_t* v = calloc(1, sizeof *v);
if (!v) {
qmp_disconnect(qmp);
return NULL;
}
v->driver = VMCTL_DRIVER_QMP;
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;
}
+39
View File
@@ -0,0 +1,39 @@
/* control.c — control-write SEAM ONLY (this never writes guest memory).
*
* The actual write is performed elsewhere, by a component that holds read-write
* access to the region; this only builds the desired vgpu_control_t image from
* the intent and computes the GVA + offset/length of the significant field range
* for that atomic write under the ctrl_gen seqlock. There is no gva_write here
* and there must not be — the source is a RO fd that would fault on a store anyway.
*
* The reported out_ctrl_gva is a GVA in the PRODUCER's user address space
* (region base + VGPU_CONTROL_OFFSET, cached as r->ctrl_gva): the external write
* MUST be performed under r->proc_cr3, NOT the System kcr3.
*/
#include "perception-internal.h"
int vgpup_build_control_write(vgpup_region* r, const vgpup_control_intent* in,
vgpu_control_t* out_frame, uint64_t* out_ctrl_gva,
uint32_t* out_off, uint32_t* out_len)
{
if (!r || !in || !out_frame || !out_ctrl_gva || !out_off || !out_len) { return -1; }
/* Fill the desired control image. ctrl_gen stays 0: the writer owns it under
* the seqlock. consumer_tick/attached carry separate heartbeat/intent
* semantics and are not part of this intent. */
memset(out_frame, 0, sizeof *out_frame);
out_frame->desired_state = in->desired_state;
out_frame->target_fps = in->target_fps;
out_frame->draw_cursor = in->draw_cursor;
out_frame->full_frame_req = in->full_frame_req;
*out_ctrl_gva = r->ctrl_gva; /* region base + VGPU_CONTROL_OFFSET (cached) */
/* Significant range: desired_state .. full_frame_req (contiguous in the ABI),
* i.e. offsetof(desired_state) through the end of full_frame_req. */
*out_off = (uint32_t)offsetof(vgpu_control_t, desired_state);
*out_len = (uint32_t)(offsetof(vgpu_control_t, full_frame_req) + sizeof(uint32_t)
- offsetof(vgpu_control_t, desired_state));
return 0;
}
+170
View File
@@ -0,0 +1,170 @@
/* discover.c — process discovery + user-AS region scan (NO magic) + handle.
*
* The region is a RW shared mapping projected into the USER address space of a
* producer PROCESS — NOT a kernel VA in the System address space. So discovery
* works by PROCESS: enumerate processes (proc_list) over the RO win32 context,
* and for each one scan its user-AS under process.cr3 in [USER_MIN, USER_MAX]
* for a contiguous RW run >= VGPU_REGION_BYTES, read the producer block at its
* base, and accept it iff the whole structural-invariant table holds. The System
* kcr3 is needed only to open the context and walk processes (the caller already
* baked it into v); the region itself is always read under the producer's cr3.
*
* There is NO magic field in the ABI and the owner forbids inventing one. The
* discriminator is the cheap RW-run filter + the invariant table + two-phase
* heartbeat liveness — and the inter-phase WAIT is the caller's (the core never
* sleeps). Discovery is STRUCTURAL: never filtered by process.name.
*
* Layering: the win32 dependency (proc_list, vmie_win32_mem) lives ONLY in this
* file, in the per-process loop. The per-cr3 scan (vgpup_scan_user_as_for_region)
* is pure gva_* so it stays win32-agnostic and unit-testable under a synthetic
* cr3. A <0 read after binding means the producer process may have restarted
* (its pages are gone); the core only reports it — re-discovery is the caller's.
*/
#include <stdlib.h>
#include "perception-internal.h"
/* How many region runs to ask for per process when probing its user-AS. A user
* address space has many runs; this is generous, and the scan early-exits on the
* first accepted candidate anyway. */
#define VGPUP_MAX_REGIONS 256
/* How many processes to enumerate. proc_list stops at this; raising it would see
* more, but a producer is an ordinary user process well within this bound. */
#define VGPUP_MAX_PROCS 512
/* Read the producer block at `region_gva` under `cr3` into *out (one gva_read of
* the whole block). 0 on success, <0 on read error. */
static int read_producer_block(vmie_mem* m, uint64_t cr3, uint64_t region_gva,
vgpu_producer_t* out)
{
return gva_read(m, (uintptr_t)cr3, (uintptr_t)region_gva, out, sizeof *out) < 0 ? -1 : 0;
}
/* Scan ONE process user-AS (steps 35) under `cr3`: walk the RW runs in
* [USER_MIN, USER_MAX] and, for each contiguous run >= VGPU_REGION_BYTES, test
* the producer block at the run base against the invariant table. On the first
* accepted candidate write its base GVA + heartbeat snapshot and return 0;
* <0 if none is found / a read fails. Pure gva_* — no proc_list, no win32.
*
* Adjacent same-protection runs are coalesced: gva_regions reports VA-contiguous
* runs, but a region can land as one run or as touching neighbours, so we extend
* a running span while the next run starts exactly where the current one ends.
* The window [USER_MIN, USER_MAX] lies in one canonical half, as gva_regions
* requires. The RW filter (VR_R|VR_W) matches the shared mapping's protection
* and is cheap — it reads region metadata, not the 98 MiB of region bytes. */
int vgpup_scan_user_as_for_region(vmie_mem* m, uint64_t cr3,
uint64_t* out_region_gva, uint64_t* out_hb0)
{
vregion runs[VGPUP_MAX_REGIONS];
int n, i;
if (!m || !out_region_gva || !out_hb0) { return -1; }
n = gva_regions(m, (uintptr_t)cr3, USER_MIN, USER_MAX, VR_R | VR_W, runs, VGPUP_MAX_REGIONS);
if (n < 0) { return -1; }
if (n > VGPUP_MAX_REGIONS) { n = VGPUP_MAX_REGIONS; } /* truncated; probe what we got */
for (i = 0; i < n; ++i) {
uint64_t span_base = runs[i].va;
uint64_t span_len = runs[i].len;
int j = i;
/* coalesce adjacent RW runs into one contiguous span */
while (j + 1 < n && runs[j + 1].va == runs[j].va + runs[j].len) {
span_len += runs[j + 1].len;
++j;
}
if (span_len >= VGPU_REGION_BYTES) {
vgpu_producer_t p;
if (read_producer_block(m, cr3, span_base, &p) == 0 &&
vgpup_invariants_hold(&p)) {
*out_region_gva = span_base;
*out_hb0 = p.heartbeat;
return 0;
}
}
}
return -1;
}
/* Phase 1: enumerate processes and scan each one's user-AS for the region. The
* win32 dependency is confined here: vmie_win32_mem(v) for the generic gva_*,
* proc_list(v, skip_system=1, ...) to drop PEB-less System/kernel-only entries
* (a producer is never one). On the first process that yields a candidate write
* its proc_cr3 + region base GVA + heartbeat snapshot and return 0; <0 if no
* process yields one or proc_list / the context is not ready. */
int vgpup_discover_candidate(vmie_win32* v, uint64_t* out_proc_cr3,
uint64_t* out_region_gva, uint64_t* out_hb0)
{
process procs[VGPUP_MAX_PROCS];
vmie_mem* m;
int np, i;
if (!v || !out_proc_cr3 || !out_region_gva || !out_hb0) { return -1; }
m = vmie_win32_mem(v);
if (!m) { return -1; }
np = proc_list(v, /*skip_system=*/1, procs, VGPUP_MAX_PROCS);
if (np < 0) { return -1; }
if (np > VGPUP_MAX_PROCS) { np = VGPUP_MAX_PROCS; } /* truncated; probe what we got */
for (i = 0; i < np; ++i) {
uint64_t region_gva = 0, hb0 = 0;
if (vgpup_scan_user_as_for_region(m, procs[i].cr3, &region_gva, &hb0) == 0) {
*out_proc_cr3 = procs[i].cr3;
*out_region_gva = region_gva;
*out_hb0 = hb0;
return 0;
}
}
return -1;
}
/* Phase 2: re-read heartbeat at region_gva under proc_cr3 and report whether it
* advanced. The caller must have waited >= VGPU_HEARTBEAT_PERIOD_MS since phase
* 1. <0 here can also mean the producer process restarted (pages gone). */
int vgpup_confirm_alive(vmie_mem* m, uint64_t proc_cr3,
uint64_t region_gva, uint64_t hb0)
{
uint64_t hb_now;
if (!m) { return -1; }
if (gva_read(m, (uintptr_t)proc_cr3,
(uintptr_t)region_gva + offsetof(vgpu_producer_t, heartbeat),
&hb_now, sizeof hb_now) < 0) {
return -1;
}
return (hb_now - hb0) > 0u ? 1 : 0;
}
vgpup_region* vgpup_open(vmie_win32* v)
{
uint64_t proc_cr3 = 0, region_gva = 0, hb0 = 0;
vgpup_region* r;
if (vgpup_discover_candidate(v, &proc_cr3, &region_gva, &hb0) != 0) { return NULL; }
r = (vgpup_region*)calloc(1, sizeof *r);
if (!r) { return NULL; }
r->proc_cr3 = proc_cr3;
r->region_gva = region_gva;
r->ctrl_gva = region_gva + VGPU_CONTROL_OFFSET;
r->ring_gva = region_gva + VGPU_RING_OFFSET;
r->last_frame_id = 0;
r->run_epoch = 0;
return r;
}
void vgpup_close(vgpup_region* r)
{
free(r); /* core state only; v / m belong to the caller */
}
uint32_t vgpup_run_epoch(const vgpup_region* r)
{
return r ? r->run_epoch : 0u;
}
@@ -0,0 +1,152 @@
#ifndef VGPU_PERCEPTION_INTERNAL_H
#define VGPU_PERCEPTION_INTERNAL_H
/* perception-internal.h — private consumer-side helpers (NOT a public surface).
*
* Holds the core's private state type, the consumer-side seqlock read discipline
* (the mirror of the producer's atomic-shim accessors, but an independent body —
* we read into local copies via gva_read, never sharing producer code), the
* structural-invariant validator table used by discovery, and the bit unpackers
* for the packed cursor fields. Included only by the perception TUs.
*
* Consumer seqlock discipline: every guest read goes through gva_read into a
* local copy, so the compiler cannot reorder a data read across the seq read —
* each gva_read is an opaque call. We still bump the seq read into its own
* gva_read and treat odd seq / changed seq as "writer in flight → retry".
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "vgpu_stream.h"
#include "memmodel.h"
#include "vgpu_perception.h"
/* Bounded seqlock retry. Producer windows are short (a single slot publish), so
* a small count suffices; spinning longer would be a behavioural timing choice
* (control's job), which does not belong in the sensor. Exhausted → lossy skip. */
#define VGPUP_SEQLOCK_RETRIES 8u
/* Private core state. Owns nothing of the address space — only where the region
* lives (in the producer's user-AS, keyed by proc_cr3) and the last-seen
* monotonic markers for dedup / session-break. */
struct vgpup_region {
uint64_t proc_cr3; /* producer process cr3 — key to its user-AS */
uint64_t region_gva; /* producer-block GVA == region base */
uint64_t ctrl_gva; /* region_gva + VGPU_CONTROL_OFFSET (cached) */
uint64_t ring_gva; /* region_gva + VGPU_RING_OFFSET (cached) */
uint64_t last_frame_id; /* dedup: only frames with a greater id are "fresh" */
uint32_t run_epoch; /* last run_epoch seen via vgpup_read_status */
};
/* Per-cr3 user-AS region scan (discovery steps 35 for ONE address space): scan
* gva_regions over [USER_MIN, USER_MAX] under `cr3` for a contiguous RW run of
* >= VGPU_REGION_BYTES, read the producer block at its base, and accept it iff
* the structural-invariant table holds. On the first hit writes the region base
* GVA to *out_region_gva and the heartbeat snapshot to *out_hb0 and returns 0;
* <0 if none is found / a read fails. Pure gva_* (no proc_list / win32) so it is
* testable under a synthetic cr3; vgpup_discover_candidate calls it per process. */
int vgpup_scan_user_as_for_region(vmie_mem* m, uint64_t cr3,
uint64_t* out_region_gva, uint64_t* out_hb0);
/* ---- seqlock primitives -------------------------------------------------- */
static inline int vgpup_seq_is_writing(uint32_t seq) { return (seq & 1u) != 0u; }
/* Read one 32-bit seq field at `gva` into *out under `cr3` (the producer's
* user-AS cr3). 0 on success, <0 on read error. */
static inline int vgpup_read_seq(vmie_mem* m, uintptr_t cr3, uint64_t gva,
uint32_t* out)
{
return gva_read(m, cr3, (uintptr_t)gva, out, sizeof *out) < 0 ? -1 : 0;
}
/* ---- packed-field unpackers (cursor line) -------------------------------- */
static inline int32_t vgpup_cursor_x(uint64_t pos) { return (int32_t)(uint32_t)(pos & 0xFFFFFFFFu); }
static inline int32_t vgpup_cursor_y(uint64_t pos) { return (int32_t)(uint32_t)(pos >> 32); }
static inline uint16_t vgpup_lo16(uint32_t v) { return (uint16_t)(v & 0xFFFFu); }
static inline uint16_t vgpup_hi16(uint32_t v) { return (uint16_t)(v >> 16); }
/* ---- structural-invariant validator (discovery, BY TABLE — no magic) ------
*
* Discovery has no magic field in the ABI (the owner forbids one). The
* discriminator is the conjunction of structural invariants derived from the
* ABI bounds in vgpu_stream.h, plus the two-phase heartbeat liveness handled by
* the caller. The predicates run cheap→costly with early exit; each takes a
* decoded producer-block snapshot and returns 1 (holds) / 0 (rejects). */
typedef int (*vgpup_inv_fn)(const vgpu_producer_t* p);
/* Is `latest` a valid slot index, or the legitimate "no frame yet" sentinel?
* latest == NONE is NOT a rejection (a freshly-started region has no frame). */
static inline int vgpup_inv_latest_in_range(const vgpu_producer_t* p)
{
return p->latest == VGPU_LATEST_NONE || p->latest < VGPU_SLOT_COUNT;
}
/* If a frame is published, its slot seq must be even (stable, not mid-write). */
static inline int vgpup_inv_latest_seq_stable(const vgpu_producer_t* p)
{
if (p->latest == VGPU_LATEST_NONE) { return 1; }
return !vgpup_seq_is_writing(p->seq[p->latest]);
}
/* If a frame is published, its descriptor must be a tight BGRA frame within the
* ABI dimension bounds. */
static inline int vgpup_inv_latest_desc_valid(const vgpu_producer_t* p)
{
const vgpu_desc_t* d;
if (p->latest == VGPU_LATEST_NONE) { return 1; }
d = &p->desc[p->latest];
if (d->format != VGPU_FMT_BGRA8888) { return 0; }
if (d->width == 0u || d->width > VGPU_MAX_WIDTH) { return 0; }
if (d->height == 0u || d->height > VGPU_MAX_HEIGHT) { return 0; }
if (d->stride != d->width * 4u) { return 0; }
return 1;
}
/* Cold-line status enum must be in the ABI range. */
static inline int vgpup_inv_status_in_range(const vgpu_producer_t* p)
{
return p->status <= VGPU_ST_ERROR;
}
/* Cold-line backend enum must be in the ABI range. */
static inline int vgpup_inv_backend_in_range(const vgpu_producer_t* p)
{
return p->backend <= VGPU_BK_GDI;
}
/* The producer must advertise the one wire format we consume. */
static inline int vgpup_inv_supports_bgra(const vgpu_producer_t* p)
{
return (p->supported_formats & (1u << VGPU_FMT_BGRA8888)) != 0u;
}
/* The invariant table, cheap→costly. A candidate is accepted (phase 1) iff
* every predicate holds; the table is the single discriminator, no scattered
* ifs and no hardcoded numbers (all bounds come from vgpu_stream.h). */
static const vgpup_inv_fn VGPUP_INVARIANTS[] = {
vgpup_inv_latest_in_range,
vgpup_inv_status_in_range,
vgpup_inv_backend_in_range,
vgpup_inv_supports_bgra,
vgpup_inv_latest_seq_stable,
vgpup_inv_latest_desc_valid,
};
#define VGPUP_INVARIANT_COUNT (sizeof(VGPUP_INVARIANTS) / sizeof(VGPUP_INVARIANTS[0]))
/* Run the whole invariant table over a decoded producer-block snapshot.
* Returns 1 if every predicate holds, 0 on the first rejection. */
static inline int vgpup_invariants_hold(const vgpu_producer_t* p)
{
size_t i;
for (i = 0; i < VGPUP_INVARIANT_COUNT; ++i) {
if (!VGPUP_INVARIANTS[i](p)) { return 0; }
}
return 1;
}
#endif /* VGPU_PERCEPTION_INTERNAL_H */
+228
View File
@@ -0,0 +1,228 @@
/* sample.c — consumer seqlock reads: frame sampling, cursor, geometry, status.
*
* Every guest read goes through gva_read into a local copy; we never hold a
* gva_ptr across a seqlock window (it is borrowed and not atomic for re-check).
* The discipline is the mirror of the producer's publish order in atomic-shim.h,
* but an independent body — this is consumer code, not shared producer code.
*
* Lossy by contract: when a writer keeps a window busy past VGPUP_SEQLOCK_RETRIES
* we return 0 (skip), never block. Blocking longer would be behavioural timing
* (control's concern), which has no place in the sensor.
*
* All reads go under r->proc_cr3 (the producer's user-AS cr3, cached in the
* handle at discovery), NOT the System kcr3. A <0 from any gva_read means a page
* is gone — the producer process may have restarted; we propagate <0 and the
* caller re-discovers (see vgpu_perception.h "Two epochs + producer restart").
*/
#include "perception-internal.h"
#include <stdio.h> /* TEMP debug (revert): stderr skip-reason trace */
/* Read one cold-line / packed field at producer offset `off` into dst under the
* producer's user-AS cr3. */
static int read_field(vmie_mem* m, uintptr_t cr3, uint64_t region_gva,
size_t off, void* dst, size_t n)
{
return gva_read(m, cr3, (uintptr_t)region_gva + off, dst, n) < 0 ? -1 : 0;
}
int vgpup_sample_frame(vgpup_region* r, vmie_mem* m,
uint8_t* dst, size_t cap, vgpup_frame_info* info)
{
unsigned attempt;
static unsigned long _dc = 0; /* TEMP debug: 1/240 call gate */
int _dbg = ((_dc++ % 240u) == 0u);
if (!r || !m || !dst || !info) { return -1; }
for (attempt = 0; attempt < VGPUP_SEQLOCK_RETRIES; ++attempt) {
uint32_t latest = 0, seq_before = 0, seq_after = 0;
vgpu_desc_t d;
uint64_t slot_gva, seq_gva, desc_gva;
size_t frame_bytes;
/* latest (acquire-equivalent: its own read) */
if (read_field(m, r->proc_cr3, r->region_gva,
offsetof(vgpu_producer_t, latest), &latest, sizeof latest) < 0) {
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=-1 latest-read-fail\n");
return -1;
}
if (latest == VGPU_LATEST_NONE || latest >= VGPU_SLOT_COUNT) {
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=0 A latest=%u\n", latest);
return 0;
}
seq_gva = r->region_gva + offsetof(vgpu_producer_t, seq) + (uint64_t)latest * sizeof(uint32_t);
desc_gva = r->region_gva + offsetof(vgpu_producer_t, desc) + (uint64_t)latest * sizeof(vgpu_desc_t);
if (vgpup_read_seq(m, r->proc_cr3, seq_gva, &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) {
if (_dbg) fprintf(stderr, "VGPUP_DBG cont B att=%u latest=%u seqB=%u (writing)\n", attempt, latest, seq_before);
continue; /* writer in slot */
}
if (gva_read(m, (uintptr_t)r->proc_cr3, (uintptr_t)desc_gva, &d, sizeof d) < 0) { return -1; }
/* dedup by frame_id: nothing newer than what we already sampled */
if (d.frame_id <= r->last_frame_id) {
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=0 C dedup dfid=%llu last=%llu\n",
(unsigned long long)d.frame_id, (unsigned long long)r->last_frame_id);
return 0;
}
/* descriptor sanity within the read window (tight BGRA, bounded dims) */
if (d.format != VGPU_FMT_BGRA8888 || d.stride != d.width * 4u ||
d.width == 0u || d.width > VGPU_MAX_WIDTH ||
d.height == 0u || d.height > VGPU_MAX_HEIGHT) {
if (_dbg) fprintf(stderr, "VGPUP_DBG cont D torn att=%u w=%u h=%u s=%u f=%u\n",
attempt, d.width, d.height, d.stride, d.format);
continue; /* likely a torn read; retry */
}
frame_bytes = (size_t)d.height * d.stride;
if (frame_bytes > VGPU_SLOT_STRIDE) { return 0; } /* impossible-large → skip */
if (frame_bytes > cap) {
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=0 F fbytes=%zu cap=%zu\n", frame_bytes, cap);
return 0; /* would not fit → lossy drop */
}
slot_gva = r->ring_gva + (uint64_t)latest * VGPU_SLOT_STRIDE;
if (gva_read(m, (uintptr_t)r->proc_cr3, (uintptr_t)slot_gva, dst, frame_bytes) < 0) {
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=-1 G slot-read-fail latest=%u fbytes=%zu\n", latest, frame_bytes);
return -1;
}
/* re-check the slot seq: unchanged and still even → snapshot consistent */
if (vgpup_read_seq(m, r->proc_cr3, seq_gva, &seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) {
if (_dbg) fprintf(stderr, "VGPUP_DBG cont H att=%u latest=%u seqB=%u seqA=%u\n",
attempt, latest, seq_before, seq_after);
continue; /* the slot was rewritten under us — retry */
}
info->desc.width = d.width;
info->desc.height = d.height;
info->desc.stride = d.stride;
info->desc.format = d.format;
info->desc.frame_id = d.frame_id;
info->desc.timestamp_ns = d.timestamp_ns;
info->bytes = frame_bytes;
r->last_frame_id = d.frame_id;
return 1;
}
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=0 I retry-exhaust (%u attempts all busy)\n", VGPUP_SEQLOCK_RETRIES);
return 0; /* writer kept the slot busy past the retry limit — skip */
}
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out)
{
unsigned attempt;
if (!r || !m || !out) { return -1; }
/* The producer bumps cursor_seq LAST (acquire), so we read the cursor line
* first and gate on cursor_seq being even and unchanged across the window. */
for (attempt = 0; attempt < VGPUP_SEQLOCK_RETRIES; ++attempt) {
uint32_t seq_before = 0, seq_after = 0;
uint32_t visible = 0, hotspot = 0, glyph = 0, id = 0;
uint64_t pos = 0;
if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, cursor_seq),
&seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; }
if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) {
return -1;
}
if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, cursor_seq),
&seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { continue; }
out->seq = seq_after;
out->visible = visible;
out->x = vgpup_cursor_x(pos);
out->y = vgpup_cursor_y(pos);
out->hot_x = vgpup_lo16(hotspot);
out->hot_y = vgpup_hi16(hotspot);
out->glyph_w = vgpup_lo16(glyph);
out->glyph_h = vgpup_hi16(glyph);
out->id = id;
return 1;
}
return 0;
}
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out)
{
unsigned attempt;
if (!r || !m || !out) { return -1; }
for (attempt = 0; attempt < VGPUP_SEQLOCK_RETRIES; ++attempt) {
uint32_t seq_before = 0, seq_after = 0;
int32_t virt_x = 0, virt_y = 0, cap_x = 0, cap_y = 0;
uint32_t virt_w = 0, virt_h = 0, dpi = 0, refresh_mhz = 0;
if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, geom_seq),
&seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; }
if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) {
return -1;
}
if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, geom_seq),
&seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { continue; }
out->virt_x = virt_x;
out->virt_y = virt_y;
out->virt_w = virt_w;
out->virt_h = virt_h;
out->cap_x = cap_x;
out->cap_y = cap_y;
out->dpi = dpi;
out->refresh_mhz = refresh_mhz;
return 1;
}
return 0;
}
int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out)
{
vgpu_producer_t p;
if (!r || !m || !out) { return -1; }
/* Cold line: single naturally-aligned atomic fields with no seqlock. Read
* the whole producer block once and pick the cold fields — "fresh enough"
* by the lossy contract. */
if (gva_read(m, (uintptr_t)r->proc_cr3, (uintptr_t)r->region_gva, &p, sizeof p) < 0) { return -1; }
out->heartbeat = p.heartbeat;
out->run_epoch = p.run_epoch;
out->status = p.status;
out->backend = p.backend;
out->error_code = p.error_code;
out->applied_fps = p.applied_fps;
out->supported_formats = p.supported_formats;
out->ctrl_ack = p.ctrl_ack;
out->full_frame_ack = p.full_frame_ack;
out->content_change_ns = p.content_change_ns;
r->run_epoch = p.run_epoch; /* feed the session-break detector */
return 0;
}