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 @@
|
||||
/* 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