mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-25 20:36:36 +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,270 @@
|
||||
#ifndef VGPU_PERCEPTION_H
|
||||
#define VGPU_PERCEPTION_H
|
||||
|
||||
/* vgpu_perception.h — host-side, read-only perception over the vgpu region.
|
||||
*
|
||||
* A pure functional core that builds vgpu semantics ON TOP OF a guest
|
||||
* address-space root handed in by the caller. It only PERCEIVES: it discovers
|
||||
* the region by structural invariants, samples frames and reads cursor /
|
||||
* geometry / lifecycle, and returns SNAPSHOTS (POD values). It never owns
|
||||
* coherence, never opens RW guest memory, never decides control or behavioural
|
||||
* timing, never emits events upward.
|
||||
*
|
||||
* Where the region lives (the correction that shapes this API): 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 the core is handed a RO win32
|
||||
* context (which the caller opened with the System kcr3), enumerates processes
|
||||
* with proc_list, and finds the region in a process user-AS under that process's
|
||||
* own cr3 (process.cr3). The System kcr3 is needed ONLY to open the context and
|
||||
* walk processes; once the region is found, it is always read under the
|
||||
* producer's process.cr3 (cached in the handle). The handle carries proc_cr3.
|
||||
*
|
||||
* What this core does NOT do (by design — those belong to the caller):
|
||||
* - It does NOT own the vmie_win32 context / vmie_mem: both are BORROWED. The
|
||||
* caller opens the RO win32 context (its lifetime is tied to the guest
|
||||
* address-space mapping epoch) and closes it when that mapping goes stale.
|
||||
* The core never opens or closes either.
|
||||
* - It does NOT sleep / poll / spawn threads / arm timers: the two-phase
|
||||
* liveness handshake is two calls; the WAIT between them is the caller's.
|
||||
* - It does NOT transport frames. Frame transport is the caller's concern;
|
||||
* the core is a PULL source — the caller takes desc+bytes from
|
||||
* vgpup_sample_frame and routes them. No sink callback here.
|
||||
* - It does NOT write control. vgpup_build_control_write only BUILDS the
|
||||
* desired frame + offsets; the actual write is performed elsewhere, by a
|
||||
* component that holds read-write access to the region.
|
||||
*
|
||||
* Two epochs + producer restart (the caller owns the policy; the core only
|
||||
* reports facts — this is a flat pull model, no polling from below):
|
||||
* - Address-space invalidation (new kcr3 / new epoch): the caller closes the
|
||||
* win32 context, drops the old vgpup_region, opens a fresh context on the
|
||||
* new epoch and re-discovers (vgpup_open). The old handle is invalid (a
|
||||
* different address space entirely).
|
||||
* - vgpu run_epoch advance while the context stays live (session break, same
|
||||
* process): vgpup_read_status records r->run_epoch; vgpup_run_epoch reports
|
||||
* it. The caller compares and decides whether to reset vgpu state — the
|
||||
* region/process are unchanged. The core holds no reset policy.
|
||||
* - Producer process restart (new pid/cr3 under the same live kcr3): the win32
|
||||
* context is still valid (kernel alive), but the old handle's proc_cr3 /
|
||||
* region_gva point at a dead process address space. Symptom: a read under
|
||||
* r->proc_cr3 returns <0 (the process pages are gone). The core only REPORTS
|
||||
* this (<0 from a read); the DECISION to re-discover is the caller's — it
|
||||
* calls vgpup_close(old) + vgpup_open(v) so a fresh proc_list finds the
|
||||
* restarted producer with its new cr3.
|
||||
*
|
||||
* Ownership convention:
|
||||
* - vmie_win32* v, vmie_mem* m — BORROWED. The caller owns their lifecycle
|
||||
* (tied to the address-space mapping). The core only reads through them.
|
||||
* - vgpup_region* — heap-owned by the core (small private state). Create with
|
||||
* vgpup_open, release with vgpup_close. Closing it does NOT touch v / m.
|
||||
*
|
||||
* Conventions (mirror memmodel.h):
|
||||
* - The System kcr3 opens the RO win32 context; the REGION lives in the USER
|
||||
* address space of the producer process and is read under its process.cr3
|
||||
* (cached in the handle as proc_cr3). A "GVA" is a 64-bit guest VA in that
|
||||
* process address space.
|
||||
* - All guest reads go through gva_read into a local copy; no borrowed
|
||||
* pointer into guest memory ever escapes a seqlock window or this API.
|
||||
* - Integer returns: 0 success / negative failure for deterministic calls.
|
||||
* Lossy read calls (sample/cursor/geometry) are tristate: 1 = consistent
|
||||
* snapshot produced, 0 = no fresh data / writer kept it busy past the retry
|
||||
* limit / would not fit (a SKIP, never an error — do not block), <0 = a
|
||||
* hard memory-read error (page gone / process restarted — the caller
|
||||
* re-discovers; see "Two epochs + producer restart" above).
|
||||
*
|
||||
* Example (the caller drives the two-phase liveness and the read loop):
|
||||
*
|
||||
* // caller already opened a RO win32 context with the System kcr3:
|
||||
* vmie_win32* v = caller_ctx; // BORROWED by the core
|
||||
* vmie_mem* m = vmie_win32_mem(v); // BORROWED; for the generic gva_*
|
||||
*
|
||||
* vgpup_region* r = vgpup_open(v); // phase 1: find producer + candidate
|
||||
* if (!r) { return; } // no region in any process
|
||||
*
|
||||
* // phase 2 is the caller's: it waits >= VGPU_HEARTBEAT_PERIOD_MS, then
|
||||
* uint64_t proc_cr3, region_gva, hb0;
|
||||
* vgpup_discover_candidate(v, &proc_cr3, ®ion_gva, &hb0); // (or reuse open's)
|
||||
* // ... the caller sleeps here, NOT the core ...
|
||||
* int alive = vgpup_confirm_alive(m, proc_cr3, region_gva, hb0);
|
||||
*
|
||||
* // sampling (lossy pull):
|
||||
* static uint8_t buf[VGPU_SLOT_STRIDE];
|
||||
* vgpup_frame_info fi;
|
||||
* if (vgpup_sample_frame(r, m, buf, sizeof buf, &fi) == 1) {
|
||||
* // route fi.desc + buf[0..fi.bytes) to the chosen transport
|
||||
* }
|
||||
*
|
||||
* vgpup_close(r); // frees core state only; v / m stay with the caller
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "vgpu_stream.h" /* region ABI: producer/control types, slot geometry */
|
||||
#include "win32.h" /* vmie_win32*, proc_list, process, vmie_win32_mem;
|
||||
* pulls in memmodel.h for vmie_mem / gva_* — the
|
||||
* producer is found via proc_list under the System
|
||||
* kcr3, then the region is read under process.cr3 */
|
||||
|
||||
/* Opaque found vgpu region in a producer's user address space. Heap-owned by the
|
||||
* core; holds only small private state (proc_cr3, region/ctrl/ring GVA, last
|
||||
* frame_id, last run_epoch). It does NOT own v / m — those are passed back in on
|
||||
* every read. */
|
||||
typedef struct vgpup_region vgpup_region;
|
||||
|
||||
/* ---- handle / lifecycle (the core does NOT own the win32 context) --------- */
|
||||
|
||||
/* Phase-1 discover + bind: enumerate processes (proc_list) over the BORROWED RO
|
||||
* win32 context v, scan each process user-AS by structural invariants, snapshot
|
||||
* hb0, and build a handle carrying the producer's proc_cr3 + region/ctrl/ring
|
||||
* GVA. v is BORROWED — the core reads through it but never closes it (its
|
||||
* lifetime is the caller's, tied to the address-space mapping epoch). Returns a
|
||||
* heap-owned vgpup_region*, or NULL if no region is found in any process.
|
||||
* Liveness is NOT
|
||||
* yet proven: the caller must call vgpup_confirm_alive after waiting
|
||||
* >= VGPU_HEARTBEAT_PERIOD_MS. Sampling before confirmation is allowed (lossy);
|
||||
* "producer alive" is true only after a positive confirm.
|
||||
*
|
||||
* If a later read returns <0, the producer process may have restarted (its
|
||||
* pages are gone): the caller re-discovers via vgpup_close(r) + vgpup_open(v). */
|
||||
vgpup_region* vgpup_open(vmie_win32* v);
|
||||
|
||||
/* Release ONLY the core state. Does NOT touch v / m — the caller closes those
|
||||
* (their lifetime is the caller's). Safe on NULL. */
|
||||
void vgpup_close(vgpup_region* r);
|
||||
|
||||
/* ---- two-phase discovery (the WAIT belongs to the caller) ----------------- */
|
||||
|
||||
/* Phase 1: find a producer and a candidate region in its user-AS (no liveness).
|
||||
* Walks proc_list over v and, for each process, scans its user-AS under
|
||||
* process.cr3 by structural invariants. On the first hit writes the producer's
|
||||
* cr3 to *out_proc_cr3, the region base GVA to *out_region_gva and the heartbeat
|
||||
* snapshot to *out_hb0, and returns 0. Returns <0 if no candidate is found in
|
||||
* any process or a read fails. Pure; does NOT wait. Needs v for proc_list. */
|
||||
int vgpup_discover_candidate(vmie_win32* v, uint64_t* out_proc_cr3,
|
||||
uint64_t* out_region_gva, uint64_t* out_hb0);
|
||||
|
||||
/* Phase 2: confirm liveness. The caller calls this >= VGPU_HEARTBEAT_PERIOD_MS
|
||||
* after phase 1. Re-reads heartbeat at region_gva under proc_cr3 and returns 1
|
||||
* if it advanced (alive producer), 0 if it did not tick (dead / not the region),
|
||||
* <0 on a read error. Takes vmie_mem* m (== vmie_win32_mem(v)) and proc_cr3 —
|
||||
* the win32 surface is no longer needed here, only gva_read. Pure; does NOT
|
||||
* wait — the inter-phase delay is the caller's. */
|
||||
int vgpup_confirm_alive(vmie_mem* m, uint64_t proc_cr3,
|
||||
uint64_t region_gva, uint64_t hb0);
|
||||
|
||||
/* ---- snapshots (POD values; read under their seqlock discipline) ---------- */
|
||||
|
||||
/* Snapshot of the last published frame's descriptor (read under seq[slot]). */
|
||||
typedef struct {
|
||||
uint32_t width, height, stride, format;
|
||||
uint64_t frame_id;
|
||||
uint64_t timestamp_ns;
|
||||
} vgpup_frame_desc;
|
||||
|
||||
/* Result of a frame sample: the descriptor plus the count of bytes copied into
|
||||
* the caller's buffer (== height*stride, tight). */
|
||||
typedef struct {
|
||||
vgpup_frame_desc desc;
|
||||
size_t bytes;
|
||||
} vgpup_frame_info;
|
||||
|
||||
/* Cursor snapshot (read under the cursor_seq acquire gate). seq lets the caller
|
||||
* tell "cursor idle" from "producer stopped reporting". */
|
||||
typedef struct {
|
||||
uint32_t seq; /* cursor_seq observed for this snapshot */
|
||||
uint32_t visible; /* 1 = shown, 0 = hidden */
|
||||
int32_t x, y; /* unpacked from cursor_pos (signed) */
|
||||
uint16_t hot_x, hot_y; /* unpacked from cursor_hotspot */
|
||||
uint16_t glyph_w, glyph_h; /* unpacked from cursor_glyph */
|
||||
uint32_t id; /* VGPU_CURSOR_ID_* */
|
||||
} vgpup_cursor;
|
||||
|
||||
/* Display-geometry snapshot (read under the geom_seq seqlock). */
|
||||
typedef struct {
|
||||
int32_t virt_x, virt_y;
|
||||
uint32_t virt_w, virt_h;
|
||||
int32_t cap_x, cap_y;
|
||||
uint32_t dpi, refresh_mhz;
|
||||
} vgpup_geometry;
|
||||
|
||||
/* Lifecycle / status snapshot (cold line; single naturally-aligned atomic
|
||||
* fields, no seqlock — "fresh enough" by the lossy contract). */
|
||||
typedef struct {
|
||||
uint64_t heartbeat;
|
||||
uint32_t run_epoch;
|
||||
uint32_t status; /* VGPU_ST_* */
|
||||
uint32_t backend; /* VGPU_BK_* */
|
||||
uint32_t error_code;
|
||||
uint32_t applied_fps;
|
||||
uint32_t supported_formats;
|
||||
uint32_t ctrl_ack;
|
||||
uint32_t full_frame_ack;
|
||||
uint64_t content_change_ns;
|
||||
} vgpup_status;
|
||||
|
||||
/* ---- read API (lossy; seqlock discipline lives inside) -------------------- *
|
||||
* All read functions read under r->proc_cr3 (the producer's cr3, cached in the
|
||||
* handle at discovery). m is a BORROWED vmie_mem* (== vmie_win32_mem(v)); the
|
||||
* cr3 is NOT in the signature — it travels in the handle. A <0 return is a hard
|
||||
* memory-read error: the producer process may have restarted, so the caller
|
||||
* re-discovers (see "Two epochs + producer restart" in the file header). */
|
||||
|
||||
/* Sample the latest frame. Seqlock-reads latest/seq[slot]/desc, copies the slot
|
||||
* bytes out of the RING via gva_read, then re-checks seq[slot] in one window.
|
||||
* dst is the caller's buffer, cap its capacity. Returns 1 = a fresh frame was
|
||||
* copied (info filled), 0 = no new frame / writer busy past the retry limit /
|
||||
* frame would not fit cap (lossy SKIP, not an error), <0 = a memory-read error.
|
||||
* "Fresh" dedups by frame_id: a frame_id <= the last sampled one returns 0. */
|
||||
int vgpup_sample_frame(vgpup_region* r, vmie_mem* m,
|
||||
uint8_t* dst, size_t cap, vgpup_frame_info* info);
|
||||
|
||||
/* Read the cursor under the cursor_seq acquire gate. 1 = consistent snapshot,
|
||||
* 0 = writer busy past the retry limit, <0 = read error. */
|
||||
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out);
|
||||
|
||||
/* Read display geometry under the geom_seq seqlock. Returns as read_cursor. */
|
||||
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out);
|
||||
|
||||
/* Read the cold-line status/lifecycle. 0 = success, <0 = read error. The single
|
||||
* atomic fields carry no seqlock; the snapshot is "fresh enough" (lossy). */
|
||||
int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out);
|
||||
|
||||
/* The run_epoch from the last vgpup_read_status — a session-break detector for
|
||||
* the caller while the address space stays live. The core only reports the raw
|
||||
* value; it holds no reset policy (what to reset is the caller's decision). */
|
||||
uint32_t vgpup_run_epoch(const vgpup_region* r);
|
||||
|
||||
/* ---- control-write — SEAM ONLY (this never writes) ------------------------ */
|
||||
|
||||
/* Desired control-block value (host-RW fields). The caller builds it and later
|
||||
* forwards it to the writer; the actual gva_write is performed elsewhere, by the
|
||||
* component that holds read-write access to the region. */
|
||||
typedef struct {
|
||||
uint32_t desired_state; /* VGPU_CMD_* */
|
||||
uint32_t target_fps; /* 0 = producer default */
|
||||
uint32_t draw_cursor; /* 0/1 */
|
||||
uint32_t full_frame_req; /* edge counter (caller bumps vs the previous) */
|
||||
} vgpup_control_intent;
|
||||
|
||||
/* Build a control frame WITHOUT writing: fill a vgpu_control_t image from `in`,
|
||||
* and report the control-block GVA plus the offset/length of the significant
|
||||
* field range, so an external read-write writer can perform an atomic write
|
||||
* under the ctrl_gen seqlock. This NEVER touches guest memory (the RO fd would
|
||||
* not allow it anyway). ctrl_gen is left zero here: the writer owns it under the
|
||||
* seqlock. The significant range is desired_state .. full_frame_req;
|
||||
* consumer_tick/attached carry separate heartbeat/intent semantics and are NOT
|
||||
* part of this intent.
|
||||
* out_frame — filled vgpu_control_t (significant fields from `in`)
|
||||
* out_ctrl_gva — control-block GVA (region base + VGPU_CONTROL_OFFSET). This
|
||||
* GVA is valid in the PRODUCER's user address space: the
|
||||
* external write MUST be performed under r->proc_cr3, NOT the
|
||||
* System kcr3.
|
||||
* out_off — offset of the first significant field (offsetof desired_state)
|
||||
* out_len — length of the significant range (through full_frame_req)
|
||||
* Returns 0 on success, <0 if r is NULL. The write itself is performed
|
||||
* elsewhere; there is no live gva_write here and there must not be. */
|
||||
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);
|
||||
|
||||
#endif /* VGPU_PERCEPTION_H */
|
||||
@@ -0,0 +1,169 @@
|
||||
#ifndef VGPU_STREAM_H
|
||||
#define VGPU_STREAM_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h> /* offsetof */
|
||||
#include <stdalign.h> /* alignas */
|
||||
#include <assert.h> /* static_assert */
|
||||
|
||||
/* ===== Geometry — single source of truth (bare ABI, both ends agree) ===== */
|
||||
#define VGPU_PAGE 4096u
|
||||
#define VGPU_SLOT_COUNT 3u
|
||||
#define VGPU_SLOT_STRIDE (32u * 1024u * 1024u)
|
||||
#define VGPU_RING_OFFSET (2u * 1024u * 1024u)
|
||||
#define VGPU_PRODUCER_OFFSET 0u
|
||||
#define VGPU_CONTROL_OFFSET VGPU_PAGE
|
||||
#define VGPU_REGION_BYTES (VGPU_RING_OFFSET + (uint64_t)VGPU_SLOT_COUNT * VGPU_SLOT_STRIDE)
|
||||
#define VGPU_MAX_WIDTH 3840u
|
||||
#define VGPU_MAX_HEIGHT 2160u
|
||||
#define VGPU_HEARTBEAT_PERIOD_MS 250u /* producer ticks heartbeat >= 4 Hz always */
|
||||
#define VGPU_LATEST_NONE 0xFFFFFFFFu
|
||||
|
||||
static_assert((uint64_t)VGPU_MAX_WIDTH * VGPU_MAX_HEIGHT * 4u <= VGPU_SLOT_STRIDE,
|
||||
"max-mode tight BGRA must fit one slot");
|
||||
|
||||
/* enum values travel as uint32 wire-values (not as enum fields → no width instability) */
|
||||
enum { VGPU_FMT_BGRA8888 = 0 };
|
||||
enum { VGPU_ST_INIT=0, VGPU_ST_CAPTURING=1, VGPU_ST_PAUSED=2, VGPU_ST_STOPPED=3, VGPU_ST_ERROR=4 };
|
||||
enum { VGPU_BK_NONE=0, VGPU_BK_NVFBC=1, VGPU_BK_DDA=2, VGPU_BK_GDI=3 };
|
||||
enum { VGPU_CMD_STOP=0, VGPU_CMD_RUN=1, VGPU_CMD_PAUSE=2 };
|
||||
/* cursor shape identity (wire-uint32); UNKNOWN=0 → custom/unrecognized glyph */
|
||||
enum { VGPU_CURSOR_ID_UNKNOWN=0, VGPU_CURSOR_ID_ARROW=1, VGPU_CURSOR_ID_IBEAM=2,
|
||||
VGPU_CURSOR_ID_WAIT=3, VGPU_CURSOR_ID_CROSS=4, VGPU_CURSOR_ID_HAND=5,
|
||||
VGPU_CURSOR_ID_SIZENS=6, VGPU_CURSOR_ID_SIZEWE=7, VGPU_CURSOR_ID_SIZENWSE=8,
|
||||
VGPU_CURSOR_ID_SIZENESW=9, VGPU_CURSOR_ID_SIZEALL=10, VGPU_CURSOR_ID_NO=11,
|
||||
VGPU_CURSOR_ID_APPSTARTING=12 };
|
||||
|
||||
/* ===== Per-slot descriptor (under hot.seq[slot]) ===== */
|
||||
typedef struct {
|
||||
uint32_t width; /* pixels */
|
||||
uint32_t height; /* pixels */
|
||||
uint32_t stride; /* bytes/row; INVARIANT: == width*4 (tight) */
|
||||
uint32_t format; /* VGPU_FMT_* */
|
||||
uint64_t frame_id; /* == producer.frame_id at publish time */
|
||||
uint64_t timestamp_ns; /* capture time, monotonic */
|
||||
} vgpu_desc_t;
|
||||
static_assert(sizeof(vgpu_desc_t) == 32, "desc layout");
|
||||
static_assert(offsetof(vgpu_desc_t, width) == 0, "desc.width");
|
||||
static_assert(offsetof(vgpu_desc_t, height) == 4, "desc.height");
|
||||
static_assert(offsetof(vgpu_desc_t, stride) == 8, "desc.stride");
|
||||
static_assert(offsetof(vgpu_desc_t, format) == 12, "desc.format");
|
||||
static_assert(offsetof(vgpu_desc_t, frame_id) == 16, "desc.frame_id");
|
||||
static_assert(offsetof(vgpu_desc_t, timestamp_ns) == 24, "desc.timestamp_ns");
|
||||
|
||||
/* ===== Producer block (host-RO): hot publish line + cold status line ===== */
|
||||
typedef struct {
|
||||
/* --- hot publish line --- */
|
||||
alignas(64)
|
||||
uint32_t latest; /* index of last; VGPU_LATEST_NONE until 1st frame */
|
||||
uint32_t _r0;
|
||||
uint64_t frame_id; /* monotonic frame counter (8-aligned) */
|
||||
uint32_t seq[VGPU_SLOT_COUNT]; /* per-slot seqlock: even=stable, odd=writing */
|
||||
uint32_t _r1;
|
||||
vgpu_desc_t desc[VGPU_SLOT_COUNT]; /* self-describing slots */
|
||||
|
||||
/* --- cold status line --- */
|
||||
alignas(64)
|
||||
uint64_t heartbeat; /* monotonic; ticks always (even STOPPED/PAUSED) */
|
||||
uint32_t run_epoch; /* +1 per start (session break for host) */
|
||||
uint32_t status; /* VGPU_ST_* */
|
||||
uint32_t backend; /* VGPU_BK_* */
|
||||
uint32_t error_code; /* 0=none; else fatal detail */
|
||||
uint32_t applied_fps; /* publish-rate cap the producer actually applies;
|
||||
actual rate may be lower on static content or
|
||||
backend limits — host measures real fps from
|
||||
desc.timestamp_ns */
|
||||
uint32_t supported_formats; /* bitmask (1u<<VGPU_FMT_*) */
|
||||
uint32_t ctrl_ack; /* echo of control.ctrl_gen (even) applied */
|
||||
uint32_t full_frame_ack; /* echo of control.full_frame_req honored */
|
||||
/* --- cursor reporting (host-RO; position is sensor data, independent
|
||||
* of control.draw_cursor / cursor compositing) --- */
|
||||
uint32_t cursor_seq; /* @168: monotonic; bumps each cursor publish.
|
||||
Host reads it last (acquire) to gate a
|
||||
consistent {cursor_pos,cursor_visible}; lets the
|
||||
host tell "cursor idle" from "producer stopped
|
||||
reporting". */
|
||||
uint32_t cursor_visible; /* @172: 1=cursor shown (CURSOR_SHOWING), 0=hidden */
|
||||
uint64_t cursor_pos; /* @176: packed screen position, 8-aligned single
|
||||
atomic MOV. low 32 bits = x, high 32 = y, each a
|
||||
signed int32 (two's-complement; multi-monitor →
|
||||
negatives). Pair never tears (one 64-bit store). */
|
||||
/* --- cursor Tier-1 (host-RO; same cursor_seq gate as cursor_pos/visible) --- */
|
||||
uint32_t cursor_hotspot; /* @184: low16=hot_x, high16=hot_y (unsigned) */
|
||||
uint32_t cursor_glyph; /* @188: low16=glyph_w, high16=glyph_h (unsigned) */
|
||||
uint32_t cursor_id; /* @192: VGPU_CURSOR_ID_* shape identity */
|
||||
|
||||
/* --- graphics static-idle: monotonic stamp of last scene-content change --- */
|
||||
alignas(8) uint64_t content_change_ns; /* @200: host derives idle-ms vs its own clock */
|
||||
|
||||
/* --- display geometry (own cache line; geom_seq seqlock; sampled rarely) ---
|
||||
* captured-surface SIZE is NOT here: it is desc.width/height (authoritative, tight). */
|
||||
alignas(64)
|
||||
uint32_t geom_seq; /* @256: even=stable, odd=writing (frame-seqlock) */
|
||||
int32_t virt_x; /* @260: virtual-desktop origin (signed) */
|
||||
int32_t virt_y; /* @264 */
|
||||
uint32_t virt_w; /* @268: virtual-desktop bbox size (interprets neg pos) */
|
||||
uint32_t virt_h; /* @272 */
|
||||
int32_t cap_x; /* @276: captured-output origin in virtual-desktop coords */
|
||||
int32_t cap_y; /* @280: (captured size = desc.width/height, not here) */
|
||||
uint32_t dpi; /* @284: captured-output effective DPI; 96=100%; 0=unknown */
|
||||
uint32_t refresh_mhz; /* @288: captured-output refresh in milli-Hz; 0=unknown */
|
||||
} vgpu_producer_t;
|
||||
static_assert(alignof(vgpu_producer_t) == 64, "producer align");
|
||||
static_assert(sizeof(vgpu_producer_t) <= VGPU_PAGE, "producer fits page 0");
|
||||
/* host-read field layout frozen as ABI */
|
||||
static_assert(offsetof(vgpu_producer_t, latest) == 0, "producer.latest");
|
||||
static_assert(offsetof(vgpu_producer_t, frame_id) == 8, "producer.frame_id");
|
||||
static_assert(offsetof(vgpu_producer_t, seq) == 16, "producer.seq");
|
||||
static_assert(offsetof(vgpu_producer_t, desc) == 32, "producer.desc");
|
||||
static_assert(offsetof(vgpu_producer_t, heartbeat) == 128, "producer.heartbeat");
|
||||
static_assert(offsetof(vgpu_producer_t, run_epoch) == 136, "producer.run_epoch");
|
||||
static_assert(offsetof(vgpu_producer_t, status) == 140, "producer.status");
|
||||
static_assert(offsetof(vgpu_producer_t, backend) == 144, "producer.backend");
|
||||
static_assert(offsetof(vgpu_producer_t, error_code) == 148, "producer.error_code");
|
||||
static_assert(offsetof(vgpu_producer_t, applied_fps) == 152, "producer.applied_fps");
|
||||
static_assert(offsetof(vgpu_producer_t, supported_formats) == 156, "producer.supported_formats");
|
||||
static_assert(offsetof(vgpu_producer_t, ctrl_ack) == 160, "producer.ctrl_ack");
|
||||
static_assert(offsetof(vgpu_producer_t, full_frame_ack) == 164, "producer.full_frame_ack");
|
||||
static_assert(offsetof(vgpu_producer_t, cursor_seq) == 168, "producer.cursor_seq");
|
||||
static_assert(offsetof(vgpu_producer_t, cursor_visible) == 172, "producer.cursor_visible");
|
||||
static_assert(offsetof(vgpu_producer_t, cursor_pos) == 176, "producer.cursor_pos");
|
||||
/* cursor Tier-1 (cursor line, gated by cursor_seq) */
|
||||
static_assert(offsetof(vgpu_producer_t, cursor_hotspot) == 184, "producer.cursor_hotspot");
|
||||
static_assert(offsetof(vgpu_producer_t, cursor_glyph) == 188, "producer.cursor_glyph");
|
||||
static_assert(offsetof(vgpu_producer_t, cursor_id) == 192, "producer.cursor_id");
|
||||
/* graphics static-idle */
|
||||
static_assert(offsetof(vgpu_producer_t, content_change_ns) == 200, "producer.content_change_ns");
|
||||
/* display geometry (own cache line; captured SIZE is desc.width/height, not here) */
|
||||
static_assert(offsetof(vgpu_producer_t, geom_seq) == 256, "producer.geom_seq");
|
||||
static_assert(offsetof(vgpu_producer_t, virt_x) == 260, "producer.virt_x");
|
||||
static_assert(offsetof(vgpu_producer_t, virt_y) == 264, "producer.virt_y");
|
||||
static_assert(offsetof(vgpu_producer_t, virt_w) == 268, "producer.virt_w");
|
||||
static_assert(offsetof(vgpu_producer_t, virt_h) == 272, "producer.virt_h");
|
||||
static_assert(offsetof(vgpu_producer_t, cap_x) == 276, "producer.cap_x");
|
||||
static_assert(offsetof(vgpu_producer_t, cap_y) == 280, "producer.cap_y");
|
||||
static_assert(offsetof(vgpu_producer_t, dpi) == 284, "producer.dpi");
|
||||
static_assert(offsetof(vgpu_producer_t, refresh_mhz) == 288, "producer.refresh_mhz");
|
||||
|
||||
/* ===== Control block (host-RW), own page, generation-guarded ===== */
|
||||
typedef struct {
|
||||
alignas(64)
|
||||
uint32_t ctrl_gen; /* generation seqlock: even=stable, odd=writing (host writes) */
|
||||
uint32_t desired_state; /* VGPU_CMD_* (STOP/RUN/PAUSE) */
|
||||
uint32_t target_fps; /* desired fps; 0=producer default */
|
||||
uint32_t draw_cursor; /* 1=compose cursor */
|
||||
uint32_t full_frame_req; /* edge counter: bump → force fresh full frame */
|
||||
uint32_t consumer_tick; /* host heartbeat (producer watches with timeout) */
|
||||
uint32_t attached; /* 1=host attached (intent, not death-proof) */
|
||||
} vgpu_control_t;
|
||||
static_assert(alignof(vgpu_control_t) == 64, "control align");
|
||||
static_assert(sizeof(vgpu_control_t) <= VGPU_PAGE, "control fits page 1");
|
||||
/* host-write field layout frozen as ABI */
|
||||
static_assert(offsetof(vgpu_control_t, ctrl_gen) == 0, "control.ctrl_gen");
|
||||
static_assert(offsetof(vgpu_control_t, desired_state) == 4, "control.desired_state");
|
||||
static_assert(offsetof(vgpu_control_t, target_fps) == 8, "control.target_fps");
|
||||
static_assert(offsetof(vgpu_control_t, draw_cursor) == 12, "control.draw_cursor");
|
||||
static_assert(offsetof(vgpu_control_t, full_frame_req) == 16, "control.full_frame_req");
|
||||
static_assert(offsetof(vgpu_control_t, consumer_tick) == 20, "control.consumer_tick");
|
||||
static_assert(offsetof(vgpu_control_t, attached) == 24, "control.attached");
|
||||
|
||||
#endif
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
#ifndef VMCTL_H
|
||||
#define VMCTL_H
|
||||
#include <stddef.h>
|
||||
|
||||
/* vmctl.h — public API for a QEMU VM Input layer (actuator): input injection +
|
||||
* power/lifecycle actuation. One handle; the input driver is selected
|
||||
* declaratively through vmctl_config. OS-agnostic surface. */
|
||||
|
||||
typedef struct vmctl vmctl_t; /* opaque handle */
|
||||
|
||||
/* ===== Input drivers + open ===== */
|
||||
typedef enum {
|
||||
VMCTL_DRIVER_QMP, /* QMP input-send-event (no guest driver required) */
|
||||
VMCTL_DRIVER_UINPUT /* host uinput source; optional passthrough into guest */
|
||||
/* via QEMU virtio-input-host-pci (Linux). uinput != virtio. */
|
||||
} vmctl_driver;
|
||||
|
||||
#define VMCTL_PTR_ABS 1 /* uinput: absolute tablet */
|
||||
#define VMCTL_PTR_REL 2 /* uinput: relative mouse */
|
||||
#define VMCTL_PTR_BOTH 3 /* uinput: two devices A=abs B=rel */
|
||||
|
||||
typedef struct {
|
||||
unsigned bustype; /* HID bus type, e.g. 0x0003 (USB) */
|
||||
unsigned vendor; /* vendor id */
|
||||
unsigned product; /* product id */
|
||||
unsigned version; /* device version */
|
||||
const char* name; /* device name; library copies it */
|
||||
} vmctl_uinput_id;
|
||||
|
||||
typedef struct {
|
||||
vmctl_driver driver;
|
||||
const char* qmp_path; /* QMP unix socket; required for QMP, optional (passthrough) for UINPUT */
|
||||
const char* input_bus; /* virtio-input-host-pci bus "pci.0" for passthrough; "" = none */
|
||||
int ptr_mode; /* UINPUT VMCTL_PTR_*; 0 for QMP */
|
||||
const vmctl_uinput_id* uinput_id; /* UINPUT only; NULL = built-in defaults */
|
||||
} vmctl_config;
|
||||
|
||||
vmctl_t* vmctl_open (const vmctl_config* cfg); /* NULL on error */
|
||||
void vmctl_close(vmctl_t* v); /* safe on NULL */
|
||||
|
||||
/* ===== Input constants ===== */
|
||||
#define VMCTL_ABS_MAX 32767 /* abs coordinates 0..VMCTL_ABS_MAX */
|
||||
#define VMCTL_AXIS_X 0
|
||||
#define VMCTL_AXIS_Y 1
|
||||
#define VMCTL_SCROLL_V 0 /* vertical */
|
||||
#define VMCTL_SCROLL_H 1 /* horizontal */
|
||||
#define VMCTL_BTN_LEFT 0
|
||||
#define VMCTL_BTN_RIGHT 1
|
||||
#define VMCTL_BTN_MIDDLE 2
|
||||
#define VMCTL_BTN_SIDE 3
|
||||
#define VMCTL_BTN_EXTRA 4
|
||||
#define VMCTL_BTN_FORWARD 5
|
||||
#define VMCTL_BTN_BACK 6
|
||||
#define VMCTL_BTN_TASK 7
|
||||
|
||||
#define VMCTL_KEY_CODE_MAX 0x2ff /* highest supported evdev key code (inclusive) */
|
||||
#define VMCTL_KEYS_SNAPSHOT_BYTES ((VMCTL_KEY_CODE_MAX + 1) / 8) /* bytes for vmctl_keys_snapshot */
|
||||
|
||||
/* ===== Event batch (value-type, stack; build ONLY via builders — ev[] is not API) ===== */
|
||||
#define VMCTL_BATCH_MAX 64
|
||||
typedef struct {
|
||||
int kind; /* internal event-kind code; set by builders */
|
||||
int code; /* axis / button / evdev-code (per kind) */
|
||||
int value; /* abs-value / rel-delta / down(0|1) */
|
||||
double scroll; /* scroll magnitude (scroll only) */
|
||||
} vmctl_event;
|
||||
typedef struct { vmctl_event ev[VMCTL_BATCH_MAX]; int count; } vmctl_batch;
|
||||
|
||||
void vmctl_batch_init (vmctl_batch* b);
|
||||
void vmctl_batch_abs (vmctl_batch* b, int axis, int value);
|
||||
void vmctl_batch_rel (vmctl_batch* b, int axis, int delta);
|
||||
void vmctl_batch_btn (vmctl_batch* b, int btn, int down);
|
||||
void vmctl_batch_key (vmctl_batch* b, int evdev_code, int down);
|
||||
void vmctl_batch_scroll(vmctl_batch* b, int axis, double value);
|
||||
int vmctl_batch_send (vmctl_t* v, vmctl_batch* b); /* one round-trip; 0=ok, -1=err */
|
||||
|
||||
/* ===== Single events (wrappers over a 1-event batch) ===== */
|
||||
int vmctl_abs (vmctl_t* v, int axis, int value); /* 0..VMCTL_ABS_MAX */
|
||||
int vmctl_rel (vmctl_t* v, int axis, int delta);
|
||||
int vmctl_btn (vmctl_t* v, int btn, int down); /* VMCTL_BTN_* */
|
||||
int vmctl_key (vmctl_t* v, int evdev_code, int down); /* Linux KEY_* */
|
||||
int vmctl_scroll(vmctl_t* v, int axis, double value); /* VMCTL_SCROLL_* */
|
||||
|
||||
/* ===== Held-state receipt (read-only) =====
|
||||
* "held" = key/button state as THIS handle last actuated it, not guest truth.
|
||||
* It is the actuator's record of its own last output (sensing the guest belongs
|
||||
* to the sensors layer, not here). Updated only after a successful send; the
|
||||
* send path NEVER reads this map (no dedup, no auto-release, no autorepeat). */
|
||||
|
||||
int vmctl_key_held (vmctl_t* v, int evdev_code); /* Linux KEY_*; 1=down 0=up */
|
||||
int vmctl_btn_held (vmctl_t* v, int btn); /* VMCTL_BTN_*; 1=down 0=up */
|
||||
int vmctl_keys_snapshot(vmctl_t* v, unsigned char* bits, size_t nbytes);
|
||||
/* copy key down-bits (EVIOCGKEY-style);
|
||||
returns bytes written, -1 on bad args */
|
||||
unsigned vmctl_btns_snapshot(vmctl_t* v); /* VMCTL_BTN_* down-bits as a mask (bits 0..7) */
|
||||
|
||||
/* ===== Power/lifecycle actuation (requires a QMP connection; -1 if there is none) ===== */
|
||||
int vmctl_powerdown(vmctl_t* v); /* system_powerdown (ACPI soft-off) */
|
||||
int vmctl_reset (vmctl_t* v); /* system_reset */
|
||||
int vmctl_wakeup (vmctl_t* v); /* system_wakeup (from S3/S4) */
|
||||
int vmctl_pause (vmctl_t* v); /* stop */
|
||||
int vmctl_resume (vmctl_t* v); /* cont */
|
||||
|
||||
/* Transfer sequencing/context belongs to signaling; timing and decisions to
|
||||
* control; reading VM state to sensors. Here, in the Input layer, only atomic
|
||||
* actuation. */
|
||||
|
||||
#endif /* VMCTL_H */
|
||||
@@ -37,6 +37,9 @@ typedef struct {
|
||||
#define VMSIG_CAP_MEMWRITE 0x100u /* CMD_MEMWRITE: atomic write-signaled mutation of shared guest memory
|
||||
* (separate from the freed CAP_MEMREAD bit — read != write; fresh bit
|
||||
* avoids stale-grant aliasing to this privileged cap). */
|
||||
#define VMSIG_CAP_ROSTER 0x200u /* SUBSCRIPTION to the VM roster (UP VMSIG_EV_ROSTER): which VMs occupy
|
||||
* which endpoints, by name/state. Distinct from CAP_OBSERVE — this is
|
||||
* host-wide inventory enumeration, not observing one VM's content. */
|
||||
|
||||
typedef struct {
|
||||
uint32_t principal; /* id for auditing (uid/token) */
|
||||
|
||||
+11
-1
@@ -51,10 +51,20 @@ void vmsig_core_set_arb_policy(vmsig_core* c, vmsig_arb_policy cb, void*
|
||||
|
||||
/* Register an adapter for VM `endpoint`: open(cfg,endpoint) -> attach(...),
|
||||
* enroll each yielded fd into epoll and into the dispatch table fd->(adapter,cookie).
|
||||
* Returns the adapter id (>=0) or -1. */
|
||||
* Returns the adapter id (>=0) or -1. Runtime-safe: may be called AFTER vmsig_core_run
|
||||
* has started, from a loop-thread callback (e.g. a discovery SLOT_SOURCE), to hot-plug
|
||||
* a VM's adapters; a freed adapter slot is reused so churn does not exhaust the table. */
|
||||
int vmsig_core_add_adapter(vmsig_core* c, const vmsig_adapter_ops* ops,
|
||||
const void* cfg, uint32_t endpoint);
|
||||
|
||||
/* Request runtime detach of EVERY adapter currently attached to `endpoint` (the whole
|
||||
* VM trio). Deferred: the teardown (epoch settle + SEAM_DOWN + lease release + epoll DEL
|
||||
* + ops->close) runs after the current event batch, like core_request_drop for controls.
|
||||
* Safe to call from a loop-thread callback (e.g. inotify discovery). No-op if endpoint
|
||||
* is not attached or >= 64. The composing of the trio at attach is the caller's job
|
||||
* (3x add_adapter); detach is by endpoint so the caller needs no per-adapter ids. */
|
||||
void vmsig_core_detach_endpoint(vmsig_core* c, uint32_t endpoint);
|
||||
|
||||
/* Attach a control endpoint (in-process or socket) with a GRANT (capability set).
|
||||
* grant == NULL => default-deny (poller inert). The core sees only the neutral
|
||||
* vtable + grant + (opt.) fd. Returns the control id (>=0) or -1. */
|
||||
|
||||
@@ -63,6 +63,11 @@ typedef enum {
|
||||
/* --- UP: cursor (vgpu sensor; emitted by the vgpu-perception shell-as-control) --- */
|
||||
VMSIG_EV_CURSOR_STATE = 37, /* cursor position/visibility; inln=vmsig_cursor; cap OBSERVE|INPUT */
|
||||
|
||||
/* --- UP: VM roster (inventory coherence; daemon-originated, source=CORE) --- */
|
||||
VMSIG_EV_ROSTER = 38, /* which VM occupies this endpoint: inln=vmsig_roster
|
||||
* {vmid,state,action,name}, endpoint in the header; retained
|
||||
* per-endpoint + replayed to late subscribers; cap ROSTER */
|
||||
|
||||
/* --- UP: input/lifecycle ack (INPUT seam) --- */
|
||||
VMSIG_EV_ACT_ACK = 48, /* down-command completed (ok/err) */
|
||||
VMSIG_EV_VM_LIFECYCLE = 49, /* power/lifecycle state report */
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef VMSIG_ROSTER_H
|
||||
#define VMSIG_ROSTER_H
|
||||
#include <stdint.h>
|
||||
|
||||
/* vmsig_roster.h — NEUTRAL inventory-coherence contract.
|
||||
*
|
||||
* The signaling daemon owns the discovery namespace and assigns each running VM a stable
|
||||
* ENDPOINT slot [0,64). The roster is the per-endpoint datum "which VM currently occupies
|
||||
* this slot, by what name, in what coarse lifecycle state". It is published as an UP event
|
||||
* VMSIG_EV_ROSTER (source=CORE), retained per endpoint and replayed to a late subscriber —
|
||||
* exactly like the MEMCTX datum, but carrying identity rather than an address-space handle.
|
||||
*
|
||||
* This is COHERENCE of shared state (the endpoint roster is shared across all controls),
|
||||
* NOT perception and NOT access-brokering. A consumer decodes it WITHOUT any host/Proxmox
|
||||
* knowledge: `endpoint` rides in the event header (ev->endpoint), the rest in inln[48].
|
||||
* CAP_ROSTER gates RECEIVING the datum (subscription), not access — access stays OS-DAC. */
|
||||
|
||||
/* Roster transition (entry->action). */
|
||||
enum {
|
||||
VMSIG_ROSTER_ATTACH = 0, /* endpoint is now occupied by `vmid` */
|
||||
VMSIG_ROSTER_DETACH = 1, /* endpoint vacated (the slot bit is being released) */
|
||||
VMSIG_ROSTER_UPDATE = 2 /* same vmid on the slot; state and/or name changed */
|
||||
};
|
||||
|
||||
/* roster->flags bits */
|
||||
#define VMSIG_ROSTER_NAME_TRUNC 0x1u /* the VM name did not fit and was truncated */
|
||||
|
||||
#define VMSIG_ROSTER_NAME_MAX 32 /* inline, NUL-terminated, truncated name */
|
||||
|
||||
/* The roster datum, carried inline (inln[48]). `endpoint` is NOT here — it is the event
|
||||
* header's ev->endpoint (where every event carries it, and what the wire serializes). */
|
||||
typedef struct {
|
||||
uint32_t vmid; /* host VM id (e.g. Proxmox vmid 100..1e9) — does NOT fit endpoint */
|
||||
uint32_t state; /* coarse lifecycle: VMSIG_VM_* (vmsig_event.h), from the host plane */
|
||||
uint32_t action; /* VMSIG_ROSTER_ATTACH/DETACH/UPDATE */
|
||||
uint32_t flags; /* VMSIG_ROSTER_* (e.g. NAME_TRUNC) */
|
||||
char name[VMSIG_ROSTER_NAME_MAX]; /* NUL-terminated, truncated display name */
|
||||
} vmsig_roster; /* 4+4+4+4+32 = 48 — exactly inln[48] */
|
||||
|
||||
#endif /* VMSIG_ROSTER_H */
|
||||
Reference in New Issue
Block a user