mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-25 20:36:36 +03:00
9bde398b6c
- 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
153 lines
6.7 KiB
C
153 lines
6.7 KiB
C
#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 */
|