mirror of
https://dev.lirent.ru/Vatrog/vm-vgpu-streamer.git
synced 2026-07-08 23:46:36 +03:00
fix: discover the vgpu region in the producer process user space
The region is a shared mapping in a producer process's user address space, not a kernel VA: open a read-only process context, enumerate processes, and locate it by structural invariants under each process cr3. Frames, cursor, geometry and status are read under the producer's cr3.
This commit is contained in:
+103
-54
@@ -10,11 +10,20 @@
|
||||
* 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_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 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;
|
||||
@@ -24,88 +33,123 @@
|
||||
* 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_mem* m, uintptr_t kcr3 — BORROWED. The caller owns their lifecycle
|
||||
* - 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 (m, kcr3).
|
||||
* vgpup_open, release with vgpup_close. Closing it does NOT touch v / m.
|
||||
*
|
||||
* 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.
|
||||
* - 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 / mapping stale — the caller re-discovers).
|
||||
* 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 vmie_mem for the current address-space mapping:
|
||||
* vmie_mem* m = caller_mem; // BORROWED by the core
|
||||
* uintptr_t kcr3 = caller_kcr3; // System AS
|
||||
* // 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(m, kcr3); // phase 1: candidate + hb0
|
||||
* if (!r) { return; } // no region under this AS
|
||||
* 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 region_gva, hb0;
|
||||
* vgpup_discover_candidate(m, kcr3, ®ion_gva, &hb0); // (or reuse open's)
|
||||
* 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, kcr3, region_gva, hb0);
|
||||
* 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, kcr3, buf, sizeof buf, &fi) == 1) {
|
||||
* 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; (m, kcr3) stay with the caller
|
||||
* 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 "memmodel.h" /* vmie_mem, gva_* (BORROWED access primitives) */
|
||||
#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 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. */
|
||||
/* 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 vmie_mem) ------------------ */
|
||||
/* ---- handle / lifecycle (the core does NOT own the win32 context) --------- */
|
||||
|
||||
/* 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);
|
||||
/* 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 (m, kcr3) — the caller closes
|
||||
* those (their lifetime is the caller's). Safe on NULL. */
|
||||
/* 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 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,
|
||||
/* 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 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,
|
||||
* 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) ---------- */
|
||||
@@ -158,7 +202,12 @@ typedef struct {
|
||||
uint64_t content_change_ns;
|
||||
} vgpup_status;
|
||||
|
||||
/* ---- read API (lossy; seqlock discipline lives inside) -------------------- */
|
||||
/* ---- 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.
|
||||
@@ -166,26 +215,23 @@ typedef struct {
|
||||
* 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,
|
||||
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, uintptr_t kcr3,
|
||||
vgpup_cursor* out);
|
||||
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, uintptr_t kcr3,
|
||||
vgpup_geometry* out);
|
||||
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, uintptr_t kcr3,
|
||||
vgpup_status* out);
|
||||
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 kcr3 stays live. The core only reports the raw value; it
|
||||
* holds no reset policy (what to reset is the caller's decision). */
|
||||
* 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) ------------------------ */
|
||||
@@ -209,7 +255,10 @@ typedef struct {
|
||||
* 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_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
|
||||
|
||||
Reference in New Issue
Block a user