mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-26 04:36:37 +03:00
271 lines
15 KiB
C
271 lines
15 KiB
C
|
|
#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 */
|