5 Commits

Author SHA1 Message Date
lirent 5b89dabc04 fix: pin the vgpu region resident (raise working set, then VirtualLock)
The fallback path committed the region but left it demand-zero (not resident). VirtualLock can pin only up to the process minimum working set, and the default quota is far below the 96 MiB region, so a bare lock fails with ERROR_WORKING_SET_QUOTA. Raise the minimum first via SetProcessWorkingSetSize, then VirtualLock; pre-fault every page as a last resort. Neither needs SE_LOCK_MEMORY (that is for large pages / AWE).
2026-06-22 15:17:37 +03:00
lirent 13a16975e3 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.
2026-06-21 00:03:40 +03:00
lirent 9e32bbd956 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.
2026-06-20 13:20:28 +03:00
lirent bcf708d3cc feat: add graphics-context sensors to producer contract
Append three host-readable sensor groups to the producer block, ABI
frozen by offset asserts (producer still fits page 0):

- cursor Tier-1 (hotspot, glyph dims, shape identity), published under
  the existing cursor_seq gate
- content_change_ns: monotonic stamp of the last scene-content change
- display geometry on its own cache line under a geom_seq seqlock:
  virtual-desktop bbox, captured-output origin, DPI, refresh

Source the cursor from each backend's existing grab metadata instead of
polling GetCursorInfo in the present pump: DDA from the duplication frame
info pointer position (no extra calls), NvFBC visibility from the grab
info plus one GetCursorInfo per frame for position, GDI from
GetCursorInfo. Position sampling now rides the frame rate, not the pump
tick.

Sample display geometry once at startup and re-sample only on backend
session recreate or a captured-mode change. Drop per-tick cursor
sampling (cursor_sample_pos) from the present loop.

Add src/stream/win32/geometry.{c,h}.
2026-06-19 01:32:28 +03:00
lirent 977c056287 feat: report on-screen cursor position in producer contract
Append cursor_seq/cursor_visible/cursor_pos to the producer block
(host-RO ABI; offsets 168/172/176, existing layout unchanged). Position
is reported every pump tick, independent of draw_cursor, so a consumer
can overlay its own cursor even when the producer does not composite it.

x and y pack into one 8-aligned 64-bit field (single atomic store, no
tear); cursor_seq bumps last to gate a consistent read. Adds the
vgpu_publish_cursor() engine setter and a lightweight cursor_sample_pos()
(position/visibility only, no shape extract) called from the pump.
2026-06-18 22:06:55 +03:00
21 changed files with 1588 additions and 14 deletions
+1
View File
@@ -11,6 +11,7 @@ add_executable(vgpu-streamer
src/stream/win32/region.c
src/stream/win32/present.c
src/stream/win32/cursor.c
src/stream/win32/geometry.c
src/stream/win32/capture.c
src/stream/win32/capture_nvfbc.c
src/stream/win32/capture_dda.c
+270
View File
@@ -0,0 +1,270 @@
#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, &region_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 */
+57
View File
@@ -26,6 +26,12 @@ enum { VGPU_FMT_BGRA8888 = 0 };
enum { VGPU_ST_INIT=0, VGPU_ST_CAPTURING=1, VGPU_ST_PAUSED=2, VGPU_ST_STOPPED=3, VGPU_ST_ERROR=4 };
enum { VGPU_BK_NONE=0, VGPU_BK_NVFBC=1, VGPU_BK_DDA=2, VGPU_BK_GDI=3 };
enum { VGPU_CMD_STOP=0, VGPU_CMD_RUN=1, VGPU_CMD_PAUSE=2 };
/* cursor shape identity (wire-uint32); UNKNOWN=0 → custom/unrecognized glyph */
enum { VGPU_CURSOR_ID_UNKNOWN=0, VGPU_CURSOR_ID_ARROW=1, VGPU_CURSOR_ID_IBEAM=2,
VGPU_CURSOR_ID_WAIT=3, VGPU_CURSOR_ID_CROSS=4, VGPU_CURSOR_ID_HAND=5,
VGPU_CURSOR_ID_SIZENS=6, VGPU_CURSOR_ID_SIZEWE=7, VGPU_CURSOR_ID_SIZENWSE=8,
VGPU_CURSOR_ID_SIZENESW=9, VGPU_CURSOR_ID_SIZEALL=10, VGPU_CURSOR_ID_NO=11,
VGPU_CURSOR_ID_APPSTARTING=12 };
/* ===== Per-slot descriptor (under hot.seq[slot]) ===== */
typedef struct {
@@ -69,6 +75,38 @@ typedef struct {
uint32_t supported_formats; /* bitmask (1u<<VGPU_FMT_*) */
uint32_t ctrl_ack; /* echo of control.ctrl_gen (even) applied */
uint32_t full_frame_ack; /* echo of control.full_frame_req honored */
/* --- cursor reporting (host-RO; position is sensor data, independent
* of control.draw_cursor / cursor compositing) --- */
uint32_t cursor_seq; /* @168: monotonic; bumps each cursor publish.
Host reads it last (acquire) to gate a
consistent {cursor_pos,cursor_visible}; lets the
host tell "cursor idle" from "producer stopped
reporting". */
uint32_t cursor_visible; /* @172: 1=cursor shown (CURSOR_SHOWING), 0=hidden */
uint64_t cursor_pos; /* @176: packed screen position, 8-aligned single
atomic MOV. low 32 bits = x, high 32 = y, each a
signed int32 (two's-complement; multi-monitor →
negatives). Pair never tears (one 64-bit store). */
/* --- cursor Tier-1 (host-RO; same cursor_seq gate as cursor_pos/visible) --- */
uint32_t cursor_hotspot; /* @184: low16=hot_x, high16=hot_y (unsigned) */
uint32_t cursor_glyph; /* @188: low16=glyph_w, high16=glyph_h (unsigned) */
uint32_t cursor_id; /* @192: VGPU_CURSOR_ID_* shape identity */
/* --- graphics static-idle: monotonic stamp of last scene-content change --- */
alignas(8) uint64_t content_change_ns; /* @200: host derives idle-ms vs its own clock */
/* --- display geometry (own cache line; geom_seq seqlock; sampled rarely) ---
* captured-surface SIZE is NOT here: it is desc.width/height (authoritative, tight). */
alignas(64)
uint32_t geom_seq; /* @256: even=stable, odd=writing (frame-seqlock) */
int32_t virt_x; /* @260: virtual-desktop origin (signed) */
int32_t virt_y; /* @264 */
uint32_t virt_w; /* @268: virtual-desktop bbox size (interprets neg pos) */
uint32_t virt_h; /* @272 */
int32_t cap_x; /* @276: captured-output origin in virtual-desktop coords */
int32_t cap_y; /* @280: (captured size = desc.width/height, not here) */
uint32_t dpi; /* @284: captured-output effective DPI; 96=100%; 0=unknown */
uint32_t refresh_mhz; /* @288: captured-output refresh in milli-Hz; 0=unknown */
} vgpu_producer_t;
static_assert(alignof(vgpu_producer_t) == 64, "producer align");
static_assert(sizeof(vgpu_producer_t) <= VGPU_PAGE, "producer fits page 0");
@@ -86,6 +124,25 @@ static_assert(offsetof(vgpu_producer_t, applied_fps) == 152, "producer.app
static_assert(offsetof(vgpu_producer_t, supported_formats) == 156, "producer.supported_formats");
static_assert(offsetof(vgpu_producer_t, ctrl_ack) == 160, "producer.ctrl_ack");
static_assert(offsetof(vgpu_producer_t, full_frame_ack) == 164, "producer.full_frame_ack");
static_assert(offsetof(vgpu_producer_t, cursor_seq) == 168, "producer.cursor_seq");
static_assert(offsetof(vgpu_producer_t, cursor_visible) == 172, "producer.cursor_visible");
static_assert(offsetof(vgpu_producer_t, cursor_pos) == 176, "producer.cursor_pos");
/* cursor Tier-1 (cursor line, gated by cursor_seq) */
static_assert(offsetof(vgpu_producer_t, cursor_hotspot) == 184, "producer.cursor_hotspot");
static_assert(offsetof(vgpu_producer_t, cursor_glyph) == 188, "producer.cursor_glyph");
static_assert(offsetof(vgpu_producer_t, cursor_id) == 192, "producer.cursor_id");
/* graphics static-idle */
static_assert(offsetof(vgpu_producer_t, content_change_ns) == 200, "producer.content_change_ns");
/* display geometry (own cache line; captured SIZE is desc.width/height, not here) */
static_assert(offsetof(vgpu_producer_t, geom_seq) == 256, "producer.geom_seq");
static_assert(offsetof(vgpu_producer_t, virt_x) == 260, "producer.virt_x");
static_assert(offsetof(vgpu_producer_t, virt_y) == 264, "producer.virt_y");
static_assert(offsetof(vgpu_producer_t, virt_w) == 268, "producer.virt_w");
static_assert(offsetof(vgpu_producer_t, virt_h) == 272, "producer.virt_h");
static_assert(offsetof(vgpu_producer_t, cap_x) == 276, "producer.cap_x");
static_assert(offsetof(vgpu_producer_t, cap_y) == 280, "producer.cap_y");
static_assert(offsetof(vgpu_producer_t, dpi) == 284, "producer.dpi");
static_assert(offsetof(vgpu_producer_t, refresh_mhz) == 288, "producer.refresh_mhz");
/* ===== Control block (host-RW), own page, generation-guarded ===== */
typedef struct {
+40
View File
@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.18) # add_subdirectory of vmie (needs find_program REQUIRED)
project(vgpu-perception C) # standalone host project, native gcc
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
# vmie is our own library — reference its SOURCES by path, never a third_party copy.
# LIBVMIE_PATH is supplied from OUTSIDE at configure time → no private path in the tree.
set(LIBVMIE_PATH "" CACHE PATH "Path to the vmie library source tree (host memory model)")
if(NOT LIBVMIE_PATH)
message(FATAL_ERROR "vgpu-perception: set -DLIBVMIE_PATH=/path/to/vmie/sources")
endif()
# Build vmie's static lib from its own sources. EXCLUDE_FROM_ALL: only the `vmie`
# target we link is built (not vmie's CLI/scan demos or its guest exe). memmodel.h
# arrives transitively via the vmie target's PUBLIC include dir — no manual include,
# no vendored header to keep in sync.
add_subdirectory(${LIBVMIE_PATH} ${CMAKE_BINARY_DIR}/vmie-build EXCLUDE_FROM_ALL)
set(REPO ${CMAKE_CURRENT_SOURCE_DIR}/../..) # repo root (this lives in src/perception)
add_library(vgpu-perception STATIC
discover.c
sample.c
control.c)
target_include_directories(vgpu-perception
PUBLIC ${REPO}/include # vgpu_perception.h, vgpu_stream.h
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) # perception-internal.h
target_link_libraries(vgpu-perception PUBLIC vmie) # memmodel.h include comes transitively
target_compile_options(vgpu-perception PRIVATE -O2 -Wall -Wextra)
# table-driven test: invariant predicates + flat sampling smoke
enable_testing()
add_executable(vgpu-perception-test test/test_perception.c)
target_include_directories(vgpu-perception-test
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) # memmodel.h via vgpu-perception -> vmie (transitive)
target_link_libraries(vgpu-perception-test PRIVATE vgpu-perception)
target_compile_options(vgpu-perception-test PRIVATE -O2 -Wall -Wextra)
add_test(NAME vgpu-perception-test COMMAND vgpu-perception-test)
+39
View File
@@ -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;
}
+170
View File
@@ -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 35) 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, &region_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, &region_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 35 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 */
+203
View File
@@ -0,0 +1,203 @@
/* 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"
/* 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;
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) {
return -1;
}
if (latest == VGPU_LATEST_NONE || latest >= VGPU_SLOT_COUNT) { 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)) { 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) { 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) {
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) { 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) { 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)) {
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;
}
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;
}
+279
View File
@@ -0,0 +1,279 @@
/* test_perception.c — table-driven invariant predicates + per-cr3 user-AS scan.
*
* Two layers (no proc_list / win32 — that path needs a real Windows kernel
* bring-up and is covered by an out-of-tree integration run, not this unit):
* 1) Invariant predicates as a TABLE of cases over a synthesized producer
* block (pure, no vmie): valid / latest==NONE / torn odd seq / non-BGRA /
* stride!=width*4 / dims out of range — each asserts accept-vs-reject.
* 2) Per-cr3 user-AS scan + sampling under a SYNTHETIC cr3: lay out a real
* region per vgpu_stream.h in a memfd, build a minimal x86-64 identity page
* table (2 MiB large pages) that maps the region at a USER VA (the region
* really lives in a producer's user-AS), open it RO via vmie_mem_from_ro_fd,
* and run vgpup_scan_user_as_for_region + a two-phase heartbeat liveness
* check, then construct a handle (proc_cr3 = synth cr3) and run the real
* frame/cursor/geometry/status reads and the control-write seam under it.
* (cr3 0 over a flat image cannot translate — gva_* needs real page tables —
* so we synthesize them; this exercises the actual translation path the
* caller will use.) The win32 proc_list wrapper is deliberately NOT exercised
* here: vgpup_scan_user_as_for_region is the pure per-cr3 core it calls.
*
* Exit 0 on all-pass; nonzero on the first failure.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include "perception-internal.h"
static int g_fail;
#define CHECK(cond, msg) do { \
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", (msg), __FILE__, __LINE__); ++g_fail; } \
} while (0)
/* ---- layer 1: invariant predicate table ---------------------------------- */
/* Build a baseline VALID producer block (one published BGRA frame in slot 0). */
static void make_valid_producer(vgpu_producer_t* p)
{
memset(p, 0, sizeof *p);
p->latest = 0;
p->frame_id = 1;
p->seq[0] = 2; /* even = stable */
p->desc[0].width = 1920;
p->desc[0].height = 1080;
p->desc[0].stride = 1920 * 4;
p->desc[0].format = VGPU_FMT_BGRA8888;
p->desc[0].frame_id = 1;
p->status = VGPU_ST_CAPTURING;
p->backend = VGPU_BK_DDA;
p->supported_formats = (1u << VGPU_FMT_BGRA8888);
p->heartbeat = 42;
}
typedef struct {
const char* name;
void (*mutate)(vgpu_producer_t*);
int expect; /* expected vgpup_invariants_hold result */
} inv_case;
static void mut_none(vgpu_producer_t* p) { (void)p; }
static void mut_latest_none(vgpu_producer_t* p) { p->latest = VGPU_LATEST_NONE; }
static void mut_latest_oob(vgpu_producer_t* p) { p->latest = VGPU_SLOT_COUNT; }
static void mut_seq_odd(vgpu_producer_t* p) { p->seq[0] = 3; }
static void mut_not_bgra(vgpu_producer_t* p) { p->desc[0].format = 7; }
static void mut_bad_stride(vgpu_producer_t* p) { p->desc[0].stride = 1920 * 4 + 1; }
static void mut_width_zero(vgpu_producer_t* p) { p->desc[0].width = 0; }
static void mut_width_huge(vgpu_producer_t* p) { p->desc[0].width = VGPU_MAX_WIDTH + 1; }
static void mut_height_huge(vgpu_producer_t* p) { p->desc[0].height = VGPU_MAX_HEIGHT + 1; }
static void mut_status_oob(vgpu_producer_t* p) { p->status = VGPU_ST_ERROR + 1; }
static void mut_backend_oob(vgpu_producer_t* p) { p->backend = VGPU_BK_GDI + 1; }
static void mut_no_bgra_support(vgpu_producer_t* p) { p->supported_formats = 0; }
static const inv_case INV_CASES[] = {
{ "valid", mut_none, 1 },
{ "latest==NONE", mut_latest_none, 1 }, /* no frame yet, still valid */
{ "latest out of range", mut_latest_oob, 0 },
{ "torn odd seq", mut_seq_odd, 0 },
{ "non-BGRA format", mut_not_bgra, 0 },
{ "stride != width*4", mut_bad_stride, 0 },
{ "width == 0", mut_width_zero, 0 },
{ "width too large", mut_width_huge, 0 },
{ "height too large", mut_height_huge, 0 },
{ "status out of range", mut_status_oob, 0 },
{ "backend out of range", mut_backend_oob, 0 },
{ "BGRA not supported", mut_no_bgra_support, 0 },
};
static void run_invariant_table(void)
{
size_t i;
for (i = 0; i < sizeof(INV_CASES) / sizeof(INV_CASES[0]); ++i) {
vgpu_producer_t p;
int got;
make_valid_producer(&p);
INV_CASES[i].mutate(&p);
got = vgpup_invariants_hold(&p);
CHECK(got == INV_CASES[i].expect, INV_CASES[i].name);
}
}
/* ---- layer 2: per-cr3 user-AS scan + sampling over a real RO vmie_mem ------ */
/* x86-64 paging entry flags for the synthetic identity table. */
#define PTE_P 0x1u /* present */
#define PTE_RW 0x2u /* writable */
#define PTE_US 0x4u /* user-accessible (the region is in a user-AS) */
#define PTE_PS 0x80u /* page size (2 MiB leaf at PD level) */
#define LARGE_PAGE (2ull * 1024 * 1024)
/* Build a minimal identity page table mapping [0, span) of the image at user VA
* `base` using 2 MiB large pages, with the PML4/PDPT/PD pages laid out right
* after the region in the same image. Every level carries US so the run reports
* VR_W|VR_U (a real user-AS mapping). Returns the cr3 (PML4 GPA). The mapped VA
* range fits one PD (covers up to 1 GiB), which is plenty for the region. */
static uint64_t build_identity_table(uint8_t* img, uint64_t region_bytes,
uint64_t base, uint64_t span)
{
const uint64_t pml4_gpa = region_bytes; /* one page each, after region */
const uint64_t pdpt_gpa = region_bytes + 0x1000;
const uint64_t pd_gpa = region_bytes + 0x2000;
uint64_t* pml4 = (uint64_t*)(img + pml4_gpa);
uint64_t* pdpt = (uint64_t*)(img + pdpt_gpa);
uint64_t* pd = (uint64_t*)(img + pd_gpa);
const unsigned pml4i = (unsigned)((base >> 39) & 0x1ffu);
const unsigned pdpti = (unsigned)((base >> 30) & 0x1ffu);
const unsigned pdi0 = (unsigned)((base >> 21) & 0x1ffu);
uint64_t mapped = 0;
unsigned k = 0;
pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW | PTE_US;
pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW | PTE_US;
while (mapped < span) {
pd[pdi0 + k] = mapped | PTE_P | PTE_RW | PTE_US | PTE_PS; /* VA base+k*2M → GPA mapped */
mapped += LARGE_PAGE;
++k;
}
return pml4_gpa;
}
static void run_flat_smoke(void)
{
const uint64_t region_bytes = VGPU_REGION_BYTES;
/* region rounded up to a 2 MiB boundary for the large-page identity map */
const uint64_t mapped_span = (region_bytes + LARGE_PAGE - 1) & ~(LARGE_PAGE - 1);
const size_t total_bytes = (size_t)region_bytes + 0x3000; /* + PML4/PDPT/PD */
/* a USER VA, 2 MiB-aligned, within [USER_MIN, USER_MAX] — the region lives in
* a producer's user address space, so we map it there (not at a kernel VA). */
const uint64_t base_va = 0x0000000010000000ull;
const uint32_t w = 64, h = 32;
const size_t frame_bytes = (size_t)w * h * 4u;
int fd;
uint8_t* img;
uint64_t cr3;
vmie_mem* m;
vgpu_producer_t p;
uint8_t marker;
fd = memfd_create("vgpu-region", 0);
CHECK(fd >= 0, "memfd_create");
if (fd < 0) { return; }
if (ftruncate(fd, (off_t)total_bytes) != 0) { CHECK(0, "ftruncate"); close(fd); return; }
img = mmap(NULL, total_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
CHECK(img != MAP_FAILED, "mmap");
if (img == MAP_FAILED) { close(fd); return; }
/* lay out a valid producer block with one BGRA frame in slot 0 (at GPA 0) */
make_valid_producer(&p);
p.desc[0].width = w;
p.desc[0].height = h;
p.desc[0].stride = w * 4u;
memcpy(img + VGPU_PRODUCER_OFFSET, &p, sizeof p);
/* fill the slot-0 frame bytes in the RING with a recognizable marker */
marker = 0xA5;
memset(img + VGPU_RING_OFFSET + 0 * VGPU_SLOT_STRIDE, marker, frame_bytes);
/* synthesize an identity table mapping the region at base_va, then open RO */
cr3 = build_identity_table(img, region_bytes, base_va, mapped_span);
m = vmie_mem_from_ro_fd(fd, total_bytes);
CHECK(m != NULL, "vmie_mem_from_ro_fd");
if (!m) { munmap(img, total_bytes); close(fd); return; }
/* per-cr3 user-AS scan: candidate found at the user VA with hb0 == 42 */
{
uint64_t rgva = 0xdead, hb0 = 0;
int rc = vgpup_scan_user_as_for_region(m, cr3, &rgva, &hb0);
CHECK(rc == 0, "scan_user_as rc");
CHECK(rgva == base_va, "scan_user_as region gva");
CHECK(hb0 == 42, "scan_user_as hb0");
/* two-phase liveness: not alive until heartbeat advances */
CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 0, "confirm not-yet-alive");
{ uint64_t hb = 43; memcpy(img + offsetof(vgpu_producer_t, heartbeat), &hb, sizeof hb); }
CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 1, "confirm alive after tick");
}
/* construct a handle directly (the proc_list/win32 path is not unit-testable;
* proc_cr3 is the synthetic cr3 here) and exercise the read API + control seam */
{
vgpup_region rr;
vgpup_region* r = &rr;
uint8_t* dst = malloc(frame_bytes);
vgpup_frame_info fi;
vgpup_cursor cur;
vgpup_geometry geo;
vgpup_status st;
int rc;
memset(&rr, 0, sizeof rr);
rr.proc_cr3 = cr3;
rr.region_gva = base_va;
rr.ctrl_gva = base_va + VGPU_CONTROL_OFFSET;
rr.ring_gva = base_va + VGPU_RING_OFFSET;
CHECK(dst != NULL, "malloc dst");
rc = vgpup_sample_frame(r, m, dst, frame_bytes, &fi);
CHECK(rc == 1, "sample_frame fresh");
if (rc == 1) {
CHECK(fi.desc.width == w && fi.desc.height == h, "sample dims");
CHECK(fi.bytes == frame_bytes, "sample bytes");
CHECK(dst[0] == marker && dst[frame_bytes - 1] == marker, "sample content");
}
/* same frame_id → no fresh frame (dedup) */
CHECK(vgpup_sample_frame(r, m, dst, frame_bytes, &fi) == 0, "sample dedup");
/* too-small buffer → lossy drop (0), not error */
CHECK(vgpup_sample_frame(r, m, dst, 1, &fi) == 0, "sample tiny-cap");
CHECK(vgpup_read_cursor(r, m, &cur) == 1, "read_cursor");
CHECK(vgpup_read_geometry(r, m, &geo) == 1, "read_geometry");
CHECK(vgpup_read_status(r, m, &st) == 0, "read_status");
CHECK(st.status == VGPU_ST_CAPTURING, "status value");
CHECK(st.heartbeat == 43, "status heartbeat");
CHECK(vgpup_run_epoch(r) == st.run_epoch, "run_epoch accessor");
/* control-write seam: builds frame + offsets, writes nothing */
{
vgpup_control_intent in = { VGPU_CMD_RUN, 60, 1, 7 };
vgpu_control_t frame;
uint64_t ctrl_gva = 0;
uint32_t off = 0, len = 0;
int crc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len);
CHECK(crc == 0, "build_control_write rc");
CHECK(frame.desired_state == VGPU_CMD_RUN, "control desired_state");
CHECK(frame.target_fps == 60, "control target_fps");
CHECK(frame.full_frame_req == 7, "control full_frame_req");
CHECK(frame.ctrl_gen == 0, "control ctrl_gen untouched");
CHECK(ctrl_gva == base_va + VGPU_CONTROL_OFFSET, "control gva");
CHECK(off == offsetof(vgpu_control_t, desired_state), "control off");
CHECK(len == offsetof(vgpu_control_t, full_frame_req) + sizeof(uint32_t)
- offsetof(vgpu_control_t, desired_state), "control len");
}
free(dst);
}
vmie_mem_close(m); /* the TEST owns vmie_mem here (it is the caller) */
munmap(img, total_bytes);
close(fd);
}
int main(void)
{
run_invariant_table();
run_flat_smoke();
if (g_fail) {
fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
printf("all checks passed\n");
return 0;
}
+32
View File
@@ -53,4 +53,36 @@ void vgpu_bump_run_epoch(const vgpu_region_view* rv);
void vgpu_tick_heartbeat(const vgpu_region_view* rv);
void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req);
/* Publish the on-screen cursor position (host-RO). Position is sensor data and is
* reported independent of control.draw_cursor (host may draw its own overlay even when the
* producer does not composite the cursor). x,y are screen coords (signed; multi-monitor may
* be negative); visible!=0 when the cursor is shown. Packs x|y into one 8-aligned 64-bit
* field (single atomic store) and bumps cursor_seq last. */
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible);
/* Publish Tier-1 cursor shape data (host-RO), written under the same cursor_seq gate as
* vgpu_publish_cursor: call this BEFORE vgpu_publish_cursor so the position publish bumps
* cursor_seq last and gates the whole cursor line consistently. hot_x/hot_y are the glyph
* hotspot; gw/gh are glyph dims; cursor_id is a VGPU_CURSOR_ID_* shape identity. */
void vgpu_publish_cursor_shape(const vgpu_region_view* rv,
uint32_t hot_x, uint32_t hot_y,
uint32_t gw, uint32_t gh, uint32_t cursor_id);
/* Publish the monotonic timestamp (ns) of the last scene-content change. Single 8-aligned
* atomic store (heartbeat pattern). The producer reports the raw stamp only; the host derives
* "ms idle" by subtracting from its own clock — no behavioural distillation in the producer. */
void vgpu_publish_content_change(const vgpu_region_view* rv, uint64_t change_ns);
/* Publish display geometry under the geom_seq seqlock (odd/even, like the frame seqlock).
* Sampled rarely (session start + reactive resample on desc-size delta / backend recreate),
* read by the host with bounded retry. virt_* is the virtual-desktop bbox (interprets negative
* cursor_pos); cap_x/cap_y is the captured output's origin in virtual-desktop coords (the
* captured surface SIZE comes from desc.width/height, not from here). dpi/refresh_mhz describe
* the captured output (96=100% / milli-Hz; 0=unknown). */
void vgpu_publish_geometry(const vgpu_region_view* rv,
int32_t virt_x, int32_t virt_y,
uint32_t virt_w, uint32_t virt_h,
int32_t cap_x, int32_t cap_y,
uint32_t dpi, uint32_t refresh_mhz);
#endif /* VGPU_STREAM_ENGINE_H */
+45
View File
@@ -116,3 +116,48 @@ void vgpu_tick_heartbeat(const vgpu_region_view* rv) {
void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req) {
vgpu_store_release32(&rv->producer->full_frame_ack, req);
}
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible) {
vgpu_producer_t* p = rv->producer;
/* pack: low 32 = x, high 32 = y (signed → two's-complement bits) */
uint64_t packed = ((uint64_t)(uint32_t)y << 32) | (uint64_t)(uint32_t)x;
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it (heartbeat pattern) */
p->cursor_pos = packed;
vgpu_store_release32(&p->cursor_visible, visible);
/* publish seq last: its release-store gates the pos/visible writes above for the host */
vgpu_store_release32(&p->cursor_seq, p->cursor_seq + 1u);
}
void vgpu_publish_cursor_shape(const vgpu_region_view* rv, uint32_t hot_x, uint32_t hot_y,
uint32_t gw, uint32_t gh, uint32_t cursor_id) {
vgpu_producer_t* p = rv->producer;
/* pack 16|16 strictly unsigned (mask low half so no sign bits bleed into the high half).
* No own seq: the following vgpu_publish_cursor bumps cursor_seq last and gates this line. */
vgpu_store_release32(&p->cursor_hotspot, (hot_y << 16) | (hot_x & 0xFFFFu));
vgpu_store_release32(&p->cursor_glyph, (gh << 16) | (gw & 0xFFFFu));
vgpu_store_release32(&p->cursor_id, cursor_id);
}
void vgpu_publish_content_change(const vgpu_region_view* rv, uint64_t change_ns) {
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it (heartbeat pattern) */
rv->producer->content_change_ns = change_ns;
vgpu_compiler_barrier();
}
void vgpu_publish_geometry(const vgpu_region_view* rv, int32_t virt_x, int32_t virt_y,
uint32_t virt_w, uint32_t virt_h,
int32_t cap_x, int32_t cap_y,
uint32_t dpi, uint32_t refresh_mhz) {
vgpu_producer_t* p = rv->producer;
/* seqlock: even -> odd (writing) */
vgpu_store_release32(&p->geom_seq, p->geom_seq + 1u);
vgpu_compiler_barrier();
p->virt_x = virt_x; p->virt_y = virt_y;
p->virt_w = virt_w; p->virt_h = virt_h;
p->cap_x = cap_x; p->cap_y = cap_y;
p->dpi = dpi; p->refresh_mhz = refresh_mhz;
vgpu_sfence();
/* seqlock: odd -> even (stable) */
vgpu_store_release32(&p->geom_seq, p->geom_seq + 1u);
vgpu_sfence();
}
+71 -2
View File
@@ -9,6 +9,9 @@
#include "capture_dda.h"
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
#include "present.h"
#include "cursor.h" /* cursor_resolve_id + ctx->cursor compose state */
#include "geometry.h" /* reactive geometry resample on recreate */
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
typedef struct {
ID3D11Device* dev;
@@ -17,8 +20,61 @@ typedef struct {
IDXGIOutputDuplication* dup;
ID3D11Texture2D* staging;
UINT W, H;
int32_t cap_x, cap_y; /* captured output origin (virt coords) */
UINT64 last_mouse_update; /* shape-gate by fi.LastMouseUpdateTime */
int seeded; /* cold-start position seed done */
} dda_state;
/* Source the cursor from the already-fetched frame info (0 syscalls for position) and publish
* it under the cursor_seq gate. Position/visibility come from fi.PointerPosition; the shape is
* re-extracted only when fi.LastMouseUpdateTime changed (shape-gate). Cold start: fi is invalid
* until the mouse first moves (LastMouseUpdateTime==0) — seed the position once via one
* GetCursorInfo, then rely on fi. ctx->cursor compose fields are written under ctx->lock; the
* producer-block publish uses release/seq, no lock. */
static void dda_source_cursor(vgpu_ctx* ctx, dda_state* st,
const DXGI_OUTDUPL_FRAME_INFO* fi) {
int vis = fi->PointerPosition.Visible ? 1 : 0;
int x, y;
UINT64 upd = (UINT64)fi->LastMouseUpdateTime.QuadPart;
if (!st->seeded && upd == 0) {
CURSORINFO ci; ci.cbSize = sizeof ci;
if (GetCursorInfo(&ci)) {
vis = (ci.flags & CURSOR_SHOWING) != 0;
x = ci.ptScreenPos.x; y = ci.ptScreenPos.y;
} else {
x = ctx->cursor.x; y = ctx->cursor.y;
}
st->seeded = 1;
} else {
x = fi->PointerPosition.Position.x;
y = fi->PointerPosition.Position.y;
if (upd != 0) st->seeded = 1;
}
/* shape-gate: re-extract only when the mouse-update stamp advanced */
if (upd != 0 && upd != st->last_mouse_update) {
CURSORINFO ci; ci.cbSize = sizeof ci;
if (GetCursorInfo(&ci) && ci.hCursor && ci.hCursor != ctx->cursor.handle) {
EnterCriticalSection(&ctx->lock);
cursor_apply_shape(ctx, ci.hCursor);
LeaveCriticalSection(&ctx->lock);
}
st->last_mouse_update = upd;
}
EnterCriticalSection(&ctx->lock);
ctx->cursor.visible = vis;
ctx->cursor.x = x; ctx->cursor.y = y;
uint32_t hx = (uint32_t)ctx->cursor.hot_x, hy = (uint32_t)ctx->cursor.hot_y;
uint32_t gw = (uint32_t)ctx->cursor.gw, gh = (uint32_t)ctx->cursor.gh;
uint32_t cid = (uint32_t)ctx->cursor.cursor_id;
LeaveCriticalSection(&ctx->lock);
vgpu_publish_cursor_shape(&ctx->view, hx, hy, gw, gh, cid);
vgpu_publish_cursor(&ctx->view, (int32_t)x, (int32_t)y, (uint32_t)vis);
}
static DWORD WINAPI dda_thread(LPVOID param) {
capture_thread_arg* arg = (capture_thread_arg*)param;
vgpu_ctx* ctx = arg->ctx;
@@ -33,12 +89,18 @@ static DWORD WINAPI dda_thread(LPVOID param) {
if (hr == DXGI_ERROR_ACCESS_LOST) {
if (st->dup) { st->dup->lpVtbl->Release(st->dup); st->dup = NULL; }
if (FAILED(st->out1->lpVtbl->DuplicateOutput(st->out1,
(IUnknown*)st->dev, &st->dup)))
(IUnknown*)st->dev, &st->dup))) {
Sleep(200);
} else {
/* display config may have changed across the access loss → resample geometry */
geometry_sample_and_publish(ctx, st->cap_x, st->cap_y);
}
continue;
}
if (FAILED(hr)) { Sleep(50); continue; }
dda_source_cursor(ctx, st, &fi);
ID3D11Texture2D* tex = NULL;
res->lpVtbl->QueryInterface(res, &IID_ID3D11Texture2D, (void**)&tex);
if (tex) {
@@ -76,7 +138,14 @@ int dda_start(vgpu_ctx* ctx, int fps) {
st->dev->lpVtbl->QueryInterface(st->dev, &IID_IDXGIDevice, (void**)&dxgiDev);
if (dxgiDev) dxgiDev->lpVtbl->GetAdapter(dxgiDev, &adapter);
if (adapter) adapter->lpVtbl->EnumOutputs(adapter, 0, &output);
if (output) output->lpVtbl->QueryInterface(output, &IID_IDXGIOutput1, (void**)&st->out1);
if (output) {
DXGI_OUTPUT_DESC od;
if (SUCCEEDED(output->lpVtbl->GetDesc(output, &od))) {
st->cap_x = (int32_t)od.DesktopCoordinates.left;
st->cap_y = (int32_t)od.DesktopCoordinates.top;
}
output->lpVtbl->QueryInterface(output, &IID_IDXGIOutput1, (void**)&st->out1);
}
if (output) output->lpVtbl->Release(output);
if (adapter) adapter->lpVtbl->Release(adapter);
+18
View File
@@ -6,6 +6,9 @@
#include "capture_gdi.h"
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
#include "present.h"
#include "cursor.h" /* cursor_sample (position+shape+id) for compose+publish */
#include "geometry.h" /* reactive geometry resample on capture-size change */
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
static DWORD WINAPI gdi_thread(LPVOID param) {
capture_thread_arg* arg = (capture_thread_arg*)param;
@@ -38,10 +41,25 @@ static DWORD WINAPI gdi_thread(LPVOID param) {
SelectObject(mem, dib);
W = w; H = h;
fprintf(stderr, "eyes(gdi): desktop %dx%d (BitBlt; cursor by presenter)\n", W, H);
/* capture size changed (primary at origin (0,0)) → resample geometry */
geometry_sample_and_publish(ctx, 0, 0);
}
if (BitBlt(mem, 0, 0, W, H, screen, 0, 0, SRCCOPY))
vgpu_present_submit(ctx, (const uint8_t*)bits,
(uint32_t)W, (uint32_t)H, (uint32_t)W * 4u);
/* source the cursor for present's compositing (under ctx->lock) and publish it */
EnterCriticalSection(&ctx->lock);
cursor_sample(ctx);
uint32_t hx = (uint32_t)ctx->cursor.hot_x, hy = (uint32_t)ctx->cursor.hot_y;
uint32_t gw = (uint32_t)ctx->cursor.gw, gh = (uint32_t)ctx->cursor.gh;
uint32_t cid = (uint32_t)ctx->cursor.cursor_id;
int32_t cx = (int32_t)ctx->cursor.x, cy = (int32_t)ctx->cursor.y;
uint32_t cvis = (uint32_t)(ctx->cursor.visible != 0);
LeaveCriticalSection(&ctx->lock);
vgpu_publish_cursor_shape(&ctx->view, hx, hy, gw, gh, cid);
vgpu_publish_cursor(&ctx->view, cx, cy, cvis);
Sleep(interval);
}
return 0; /* unreachable; satisfies -Wreturn-type */
+35 -1
View File
@@ -6,14 +6,45 @@
#include "capture_nvfbc.h"
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
#include "present.h"
#include "cursor.h" /* cursor_apply_shape / ctx->cursor */
#include "geometry.h" /* reactive geometry resample on recreate */
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
#include "nvfbc_tosys_c.h"
typedef struct {
NvFBCToSys_c* fbc;
void* buf;
NvFBC_CreateFunctionExType create;
HCURSOR last_handle; /* shape-gate by HCURSOR change */
} nvfbc_state;
/* Source the cursor for an NvFBC grab and publish it under the cursor_seq gate. NvFBC reports
* only HW-cursor visibility (gi.bHWMouseVisible); position is not exposed, so one GetCursorInfo
* per frame supplies x/y (the minimum possible). Shape is re-extracted only on HCURSOR change.
* NvFBC composites the cursor itself (draw_cursor_cap==0) → present never reads ctx->cursor for
* drawing, so no ctx->lock is required around the compose fields here.
* gi.bProtectedContent / gi.dwSourcePID are available but out of scope (not in the contract). */
static void nvfbc_source_cursor(vgpu_ctx* ctx, nvfbc_state* st,
const NvFBCFrameGrabInfo* gi) {
CURSORINFO ci; ci.cbSize = sizeof ci;
int vis = gi->bHWMouseVisible ? 1 : 0;
int x = ctx->cursor.x, y = ctx->cursor.y;
if (GetCursorInfo(&ci)) {
x = ci.ptScreenPos.x; y = ci.ptScreenPos.y;
if (ci.hCursor && ci.hCursor != st->last_handle) {
cursor_apply_shape(ctx, ci.hCursor);
st->last_handle = ci.hCursor;
}
}
ctx->cursor.visible = vis; ctx->cursor.x = x; ctx->cursor.y = y;
vgpu_publish_cursor_shape(&ctx->view,
(uint32_t)ctx->cursor.hot_x, (uint32_t)ctx->cursor.hot_y,
(uint32_t)ctx->cursor.gw, (uint32_t)ctx->cursor.gh,
(uint32_t)ctx->cursor.cursor_id);
vgpu_publish_cursor(&ctx->view, (int32_t)x, (int32_t)y, (uint32_t)vis);
}
static NvFBCToSys_c* nvfbc_create(NvFBC_CreateFunctionExType pCreate, void** ppBuf) {
NvFBCCreateParams cp; memset(&cp, 0, sizeof cp);
cp.dwVersion = NVFBC_CREATE_PARAMS_VER;
@@ -62,6 +93,8 @@ static DWORD WINAPI nvfbc_thread(LPVOID param) {
fbc = NULL;
while (!(fbc = nvfbc_create(st->create, &buf))) Sleep(200);
st->fbc = fbc; st->buf = buf;
/* grab session was recreated → display config may have changed: resample */
geometry_sample_and_publish(ctx, 0, 0);
} else {
Sleep(50);
}
@@ -70,6 +103,7 @@ static DWORD WINAPI nvfbc_thread(LPVOID param) {
if (gi.dwWidth && gi.dwHeight)
vgpu_present_submit(ctx, (const uint8_t*)buf,
gi.dwWidth, gi.dwHeight, gi.dwBufferWidth * 4u);
nvfbc_source_cursor(ctx, st, &gi);
}
return 0; /* unreachable; satisfies -Wreturn-type */
}
@@ -109,7 +143,7 @@ int nvfbc_start(vgpu_ctx* ctx, int fps) {
nvfbc_state* st = (nvfbc_state*)malloc(sizeof *st);
if (!st) { fbc->lpVtbl->NvFBCToSysRelease(fbc); return 0; }
st->fbc = fbc; st->buf = buf; st->create = pCreate;
st->fbc = fbc; st->buf = buf; st->create = pCreate; st->last_handle = NULL;
capture_thread_arg* arg = (capture_thread_arg*)malloc(sizeof *arg);
if (!arg) { fbc->lpVtbl->NvFBCToSysRelease(fbc); free(st); return 0; }
+1
View File
@@ -30,6 +30,7 @@ typedef struct {
int x, y;
int hot_x, hot_y;
int gw, gh; /* glyph dims */
int cursor_id; /* VGPU_CURSOR_ID_* resolved on shape change */
int mono; /* 1 = AND/XOR monochrome cursor */
uint8_t* bgra; /* color cursor BGRA (arena) */
uint8_t* and_mask; /* mono AND (arena) */
+38
View File
@@ -2,6 +2,7 @@
#include <windows.h>
#include <string.h>
#include "cursor.h"
#include "vgpu_stream.h" /* VGPU_CURSOR_ID_* */
/* Max supported cursor glyph; buffers are pre-arena'd in ctx (no heap here). */
#define VGPU_CURSOR_MAX 256
@@ -85,6 +86,42 @@ static void extract(vgpu_ctx* ctx, HCURSOR hc) {
if (ii.hbmMask) DeleteObject(ii.hbmMask);
}
int cursor_resolve_id(HCURSOR hc) {
/* System-cursor table loaded once (IDC_* are stable per session). Lazy: built on first
* call, then a linear handle compare. UNKNOWN for custom/unrecognized cursors. */
static const struct { LPCTSTR idc; int id; } kSpec[] = {
{ IDC_ARROW, VGPU_CURSOR_ID_ARROW },
{ IDC_IBEAM, VGPU_CURSOR_ID_IBEAM },
{ IDC_WAIT, VGPU_CURSOR_ID_WAIT },
{ IDC_CROSS, VGPU_CURSOR_ID_CROSS },
{ IDC_HAND, VGPU_CURSOR_ID_HAND },
{ IDC_SIZENS, VGPU_CURSOR_ID_SIZENS },
{ IDC_SIZEWE, VGPU_CURSOR_ID_SIZEWE },
{ IDC_SIZENWSE, VGPU_CURSOR_ID_SIZENWSE },
{ IDC_SIZENESW, VGPU_CURSOR_ID_SIZENESW },
{ IDC_SIZEALL, VGPU_CURSOR_ID_SIZEALL },
{ IDC_NO, VGPU_CURSOR_ID_NO },
{ IDC_APPSTARTING, VGPU_CURSOR_ID_APPSTARTING },
};
enum { N = (int)(sizeof kSpec / sizeof kSpec[0]) };
static HCURSOR cache[N];
static int loaded = 0;
if (!loaded) {
for (int i = 0; i < N; i++) cache[i] = LoadCursor(NULL, kSpec[i].idc);
loaded = 1;
}
if (!hc) return VGPU_CURSOR_ID_UNKNOWN;
for (int i = 0; i < N; i++)
if (cache[i] == hc) return kSpec[i].id;
return VGPU_CURSOR_ID_UNKNOWN;
}
void cursor_apply_shape(vgpu_ctx* ctx, HCURSOR hc) {
extract(ctx, hc);
ctx->cursor.cursor_id = cursor_resolve_id(hc);
ctx->cursor.handle = hc;
}
int cursor_sample(vgpu_ctx* ctx) {
vgpu_cursor_t* cur = &ctx->cursor;
CURSORINFO ci; ci.cbSize = sizeof ci;
@@ -99,6 +136,7 @@ int cursor_sample(vgpu_ctx* ctx) {
|| (ci.hCursor != cur->handle);
if (vis && ci.hCursor && ci.hCursor != cur->handle) {
extract(ctx, ci.hCursor);
cur->cursor_id = cursor_resolve_id(ci.hCursor);
cur->handle = ci.hCursor;
}
cur->visible = vis; cur->x = x; cur->y = y;
+10
View File
@@ -10,6 +10,16 @@
* Returns 1 if anything changed since last sample, else 0. */
int cursor_sample(vgpu_ctx* ctx);
/* Resolve a HCURSOR to a VGPU_CURSOR_ID_* by comparing against the system cursor table
* (LoadCursor(NULL, IDC_*) loaded once on first use). Returns VGPU_CURSOR_ID_UNKNOWN for
* custom cursors. Not hot-path: called only under the shape-change gate. */
int cursor_resolve_id(HCURSOR hc);
/* Extract glyph/hotspot/dims for hc into ctx->cursor, resolve its cursor_id, and record it as
* the current handle. For backends that source position elsewhere (DDA from frame info) and
* only need the shape on a shape-change gate. Caller serializes ctx->cursor writes. */
void cursor_apply_shape(vgpu_ctx* ctx, HCURSOR hc);
/* Alpha/AND-XOR compose the sampled cursor onto a tight BGRA frame. */
void cursor_draw(vgpu_ctx* ctx, uint8_t* bgra, uint32_t width, uint32_t height);
+52
View File
@@ -0,0 +1,52 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "geometry.h"
#include "stream.h" /* vgpu_publish_geometry */
/* GetDpiForMonitor lives in Shcore.dll (per-monitor DPI awareness API). Loaded dynamically so
* the binary does not hard-depend on it; absence degrades dpi to "unknown" (0). */
typedef HRESULT (WINAPI *GetDpiForMonitor_t)(HMONITOR, int /*MDT_*/, UINT*, UINT*);
#define VGPU_MDT_EFFECTIVE_DPI 0
static UINT monitor_dpi(HMONITOR mon) {
static GetDpiForMonitor_t fn = NULL;
static int tried = 0;
if (!tried) {
HMODULE lib = LoadLibraryA("Shcore.dll");
if (lib) fn = (GetDpiForMonitor_t)(void*)GetProcAddress(lib, "GetDpiForMonitor");
tried = 1;
}
if (!fn || !mon) return 0u;
UINT dx = 0, dy = 0;
if (fn(mon, VGPU_MDT_EFFECTIVE_DPI, &dx, &dy) != S_OK || dx == 0u)
return 0u;
return dx;
}
static uint32_t monitor_refresh_mhz(HMONITOR mon) {
MONITORINFOEXW mi; mi.cbSize = sizeof mi;
if (!mon || !GetMonitorInfoW(mon, (MONITORINFO*)&mi))
return 0u;
DEVMODEW dm; ZeroMemory(&dm, sizeof dm); dm.dmSize = sizeof dm;
if (!EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm))
return 0u;
if (dm.dmDisplayFrequency <= 1u) /* 0/1 = hardware default, not a real rate */
return 0u;
return (uint32_t)dm.dmDisplayFrequency * 1000u; /* whole Hz -> milli-Hz */
}
void geometry_sample_and_publish(vgpu_ctx* ctx, int32_t cap_x, int32_t cap_y) {
int32_t virt_x = (int32_t)GetSystemMetrics(SM_XVIRTUALSCREEN);
int32_t virt_y = (int32_t)GetSystemMetrics(SM_YVIRTUALSCREEN);
uint32_t virt_w = (uint32_t)GetSystemMetrics(SM_CXVIRTUALSCREEN);
uint32_t virt_h = (uint32_t)GetSystemMetrics(SM_CYVIRTUALSCREEN);
POINT origin = { cap_x, cap_y };
HMONITOR mon = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
uint32_t dpi = monitor_dpi(mon);
uint32_t refresh = monitor_refresh_mhz(mon);
vgpu_publish_geometry(&ctx->view, virt_x, virt_y, virt_w, virt_h,
cap_x, cap_y, dpi, refresh);
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef VGPU_GEOMETRY_H
#define VGPU_GEOMETRY_H
/* geometry.h — win32 display-geometry sampler. Samples the virtual-desktop bbox plus the
* captured output's origin / DPI / refresh and publishes them under the geom_seq seqlock.
* Not per-frame: called once at session start and reactively on backend recreate / capture-
* size change (the captured surface SIZE itself travels in desc.width/height, not here). */
#include <stdint.h>
#include "ctx.h" /* win32 vgpu_ctx (region-view) */
/* Sample display geometry for the captured output whose top-left origin is (cap_x,cap_y) in
* virtual-desktop coordinates, and publish it. cap_x/cap_y is (0,0) for primary/full-screen
* backends and the duplicated output's DesktopCoordinates for DDA. The captured size is taken
* from desc.width/height and is not sampled here. */
void geometry_sample_and_publish(vgpu_ctx* ctx, int32_t cap_x, int32_t cap_y);
#endif /* VGPU_GEOMETRY_H */
+27 -7
View File
@@ -5,6 +5,7 @@
#include "present.h"
#include "stream.h" /* OS-agnostic publish / control API + region-view */
#include "cursor.h"
#include "geometry.h" /* one-shot display-geometry sample at session start */
/* cursor arena sizing */
#define VGPU_CUR_MAX 256u
@@ -78,6 +79,9 @@ void vgpu_present_submit(vgpu_ctx* ctx, const uint8_t* src,
ctx->content_h = H;
ctx->content_seq++;
LeaveCriticalSection(&ctx->lock);
/* static-idle: stamp the moment the source delivered new content (the raw perception;
* the host derives "ms idle" from its own clock). Single 8-aligned MOV, off the lock. */
vgpu_publish_content_change(&ctx->view, now_ns());
SetEvent(ctx->submit_event);
}
@@ -89,6 +93,13 @@ void vgpu_present_run(vgpu_ctx* ctx) {
uint32_t last_ff_ack = rv->producer->full_frame_ack;
DWORD last_beat = GetTickCount();
uint64_t last_publish_ns = 0; /* 0 → first eligible frame publishes immediately */
int last_cur_x = 0, last_cur_y = 0, last_cur_vis = 0;
HCURSOR last_cur_handle = NULL;
/* one-shot display geometry: publish once before the loop (flat pull contract). The
* captured-output origin is (0,0) for the primary/full-screen capture path; backends
* resample reactively on recreate / capture-size change. No periodic poll in the loop. */
geometry_sample_and_publish(ctx, 0, 0);
for (;;) {
WaitForSingleObject(ctx->submit_event, poll_ms);
@@ -146,18 +157,25 @@ void vgpu_present_run(vgpu_ctx* ctx) {
/* --- compose + publish on content change OR forced full frame, but
* rate-limited to the applied fps cap (the single publish point
* contract-level cap, independent of the capture backend). A
* force_full bypasses the cap (due=1). --- */
int cur_changed = (ctx->draw_cursor_cap && draw_cursor)
? cursor_sample(ctx) : 0;
* force_full bypasses the cap (due=1). present does NOT sample the
* cursor (capture threads source it); it only reads ctx->cursor under
* ctx->lock for compositing, and detects cursor motion via a delta so
* a pure cursor move over static desktop still recomposes. --- */
uint64_t interval_ns = fps > 0 ? (1000000000ull / fps) : 0;
uint64_t now = now_ns();
int due = force_full || interval_ns == 0
|| (now - last_publish_ns) >= interval_ns;
int compose_cursor = (ctx->draw_cursor_cap && draw_cursor);
EnterCriticalSection(&ctx->lock);
int64_t seq = ctx->content_seq;
uint32_t W = ctx->content_w, H = ctx->content_h;
int cur_changed = compose_cursor
&& ((ctx->cursor.visible != last_cur_vis)
|| (ctx->cursor.x != last_cur_x)
|| (ctx->cursor.y != last_cur_y)
|| (ctx->cursor.handle != last_cur_handle));
int have = (W && H);
int content_new = have && (seq != last_seq || cur_changed || force_full);
/* take the frame ONLY when due — so we never drop the latest content;
@@ -166,6 +184,11 @@ void vgpu_present_run(vgpu_ctx* ctx) {
if (dirty) {
memcpy(ctx->frame_buf, ctx->content_buf, (size_t)W * H * 4u);
last_seq = seq;
if (compose_cursor)
cursor_draw(ctx, ctx->frame_buf, W, H);
last_cur_vis = ctx->cursor.visible;
last_cur_x = ctx->cursor.x; last_cur_y = ctx->cursor.y;
last_cur_handle = ctx->cursor.handle;
}
LeaveCriticalSection(&ctx->lock);
@@ -176,9 +199,6 @@ void vgpu_present_run(vgpu_ctx* ctx) {
continue;
}
if (ctx->draw_cursor_cap && draw_cursor)
cursor_draw(ctx, ctx->frame_buf, W, H);
if (vgpu_publish_frame(rv, ctx->frame_buf, W, H, now) == 0) {
last_publish_ns = now;
if (force_full) {
+30 -4
View File
@@ -106,14 +106,40 @@ int vgpu_region_create(vgpu_region_t* out) {
}
uintptr_t addr = (uintptr_t)p;
uintptr_t aligned = (addr + VGPU_2MB - 1) & ~(uintptr_t)(VGPU_2MB - 1);
if (!VirtualLock((void*)aligned, (SIZE_T)bytes)) {
fprintf(stderr, "region: VirtualLock failed (%lu) — pages may not be pinned\n",
GetLastError());
/* The region must be RESIDENT, not merely committed: the host reads it out
* of guest RAM and only PRESENT pages are visible to it a committed but
* demand-zero page has no PTE, so it is unreadable from the host. VirtualLock
* pins the pages into the working set, but it can lock at most the process
* MINIMUM working set, and the default quota is far below the region size
* (so a bare VirtualLock fails with ERROR_WORKING_SET_QUOTA). Raise the
* minimum first. NB: VirtualLock / SetProcessWorkingSetSize do NOT need
* SE_LOCK_MEMORY that privilege is only for large pages / AWE. */
SIZE_T ws_min = (SIZE_T)(bytes + 64ull * 1024 * 1024); /* region + headroom */
SIZE_T ws_max = ws_min + 128ull * 1024 * 1024;
SIZE_T cur_min = 0, cur_max = 0;
if (GetProcessWorkingSetSize(GetCurrentProcess(), &cur_min, &cur_max)) {
if (cur_min > ws_min) ws_min = cur_min; /* never shrink an existing quota */
if (cur_max > ws_max) ws_max = cur_max;
}
if (!SetProcessWorkingSetSize(GetCurrentProcess(), ws_min, ws_max))
fprintf(stderr, "region: SetProcessWorkingSetSize(%llu MiB) failed (%lu)\n",
(unsigned long long)(ws_min / (1024 * 1024)), GetLastError());
if (!VirtualLock((void*)aligned, (SIZE_T)bytes)) {
fprintf(stderr, "region: VirtualLock failed (%lu) — pre-faulting region\n",
GetLastError());
/* Last resort: fault every page so it is at least PRESENT now. Without
* the lock the trimmer may evict it under pressure, but the raised
* minimum working set above makes eviction far less likely. */
volatile uint8_t* q = (volatile uint8_t*)aligned;
for (uint64_t off = 0; off < bytes; off += 4096u) q[off] = q[off];
}
os_base = p;
base = (uint8_t*)aligned;
os_total = total;
fprintf(stderr, "region: fallback VirtualAlloc+VirtualLock %llu MiB, aligned at %p\n",
fprintf(stderr, "region: fallback VirtualAlloc+lock %llu MiB, aligned at %p\n",
(unsigned long long)(bytes / (1024 * 1024)), (void*)aligned);
}