mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-26 04:36:37 +03:00
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:
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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\"}"); }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 3–5) 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, ®ion_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, ®ion_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 3–5 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 */
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user