feat: add host-side vgpu-perception library

Read-only consumer of the vgpu region: discovers it by structural invariants, samples frames and reads cursor/geometry/status under seqlock, and builds (never writes) the control frame. Built as a separate host CMake project; the memory-model dependency source path is supplied via -DLIBVMIE_PATH at configure time.
This commit is contained in:
2026-06-20 13:20:28 +03:00
parent bcf708d3cc
commit 9e32bbd956
7 changed files with 1019 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
#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.
*
* What this core does NOT do (by design — those belong to the caller):
* - It does NOT own the vmie_mem / coherent address-space root: (m, kcr3) are
* BORROWED. The core never opens or closes a vmie_mem; the caller opens it
* for the current guest address-space mapping and closes it when that
* mapping goes stale.
* - 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.
*
* Ownership convention:
* - vmie_mem* m, uintptr_t kcr3 — 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 (m, kcr3).
*
* Conventions (mirror memmodel.h):
* - kcr3 is the System address space CR3 (the region is a pinned device
* shared-section visible as a kernel VA). A "GVA" is a 64-bit guest VA.
* - 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 / mapping stale — the caller re-discovers).
*
* Example (the caller drives the two-phase liveness and the read loop):
*
* // caller already opened a RO vmie_mem for the current address-space mapping:
* vmie_mem* m = caller_mem; // BORROWED by the core
* uintptr_t kcr3 = caller_kcr3; // System AS
*
* vgpup_region* r = vgpup_open(m, kcr3); // phase 1: candidate + hb0
* if (!r) { return; } // no region under this AS
*
* // phase 2 is the caller's: it waits >= VGPU_HEARTBEAT_PERIOD_MS, then
* uint64_t region_gva, hb0;
* vgpup_discover_candidate(m, kcr3, &region_gva, &hb0); // (or reuse open's)
* // ... the caller sleeps here, NOT the core ...
* int alive = vgpup_confirm_alive(m, kcr3, region_gva, hb0);
*
* // sampling (lossy pull):
* static uint8_t buf[VGPU_SLOT_STRIDE];
* vgpup_frame_info fi;
* if (vgpup_sample_frame(r, m, kcr3, buf, sizeof buf, &fi) == 1) {
* // route fi.desc + buf[0..fi.bytes) to the chosen transport
* }
*
* vgpup_close(r); // frees core state only; (m, kcr3) stay with the caller
*/
#include <stdint.h>
#include <stddef.h>
#include "vgpu_stream.h" /* region ABI: producer/control types, slot geometry */
#include "memmodel.h" /* vmie_mem, gva_* (BORROWED access primitives) */
/* Opaque found vgpu region under (vmie_mem, kcr3). Heap-owned by the core; holds
* only small private state (region GVA, last frame_id, last run_epoch). It does
* NOT own (m, kcr3) — those are passed back in on every read. */
typedef struct vgpup_region vgpup_region;
/* ---- handle / lifecycle (the core does NOT own vmie_mem) ------------------ */
/* Phase-1 discover + bind: find the region by structural invariants, snapshot
* hb0, and build a handle. (m, kcr3) are BORROWED — the core reads them but
* never closes them. Returns a heap-owned vgpup_region*, or NULL if no region
* is found under this address space. 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. */
vgpup_region* vgpup_open(vmie_mem* m, uintptr_t kcr3);
/* Release ONLY the core state. Does NOT touch (m, kcr3) — 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 candidate by structural invariants (no liveness). On success
* writes the region base GVA (== producer-block GVA) to *out_region_gva and the
* heartbeat snapshot to *out_hb0, and returns 0. Returns <0 if no candidate is
* found or a read fails. Pure; does NOT wait. */
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
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 and returns 1 if it advanced
* (alive producer), 0 if it did not tick (dead / not the region), <0 on a read
* error. Pure; does NOT wait — the inter-phase delay is the caller's. */
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3,
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) -------------------- */
/* 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, uintptr_t kcr3,
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, uintptr_t kcr3,
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, uintptr_t kcr3,
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, uintptr_t kcr3,
vgpup_status* out);
/* The run_epoch from the last vgpup_read_status — a session-break detector for
* the caller while kcr3 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)
* 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 */