2 Commits

Author SHA1 Message Date
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
20 changed files with 1390 additions and 28 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
+221
View File
@@ -0,0 +1,221 @@
#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.
*
* What this core does NOT do (by design — those belong to the caller):
* - It does NOT own the vmie_mem / coherent address-space root: (m, kcr3) are
* BORROWED. The core never opens or closes a vmie_mem; the caller opens it
* for the current guest address-space mapping and closes it when that
* mapping goes stale.
* - It does NOT 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.
*
* Ownership convention:
* - vmie_mem* m, uintptr_t kcr3 — BORROWED. The caller owns their lifecycle
* (tied to the address-space mapping). The core only reads through them.
* - vgpup_region* — heap-owned by the core (small private state). Create with
* vgpup_open, release with vgpup_close. Closing it does NOT touch (m, kcr3).
*
* Conventions (mirror memmodel.h):
* - kcr3 is the System address space CR3 (the region is a pinned device
* shared-section visible as a kernel VA). A "GVA" is a 64-bit guest VA.
* - All guest reads go through gva_read into a local copy; no borrowed
* pointer into guest memory ever escapes a seqlock window or this API.
* - Integer returns: 0 success / negative failure for deterministic calls.
* Lossy read calls (sample/cursor/geometry) are tristate: 1 = consistent
* snapshot produced, 0 = no fresh data / writer kept it busy past the retry
* limit / would not fit (a SKIP, never an error — do not block), <0 = a
* hard memory-read error (page gone / mapping stale — the caller re-discovers).
*
* Example (the caller drives the two-phase liveness and the read loop):
*
* // caller already opened a RO vmie_mem for the current address-space mapping:
* vmie_mem* m = caller_mem; // BORROWED by the core
* uintptr_t kcr3 = caller_kcr3; // System AS
*
* vgpup_region* r = vgpup_open(m, kcr3); // phase 1: candidate + hb0
* if (!r) { return; } // no region under this AS
*
* // phase 2 is the caller's: it waits >= VGPU_HEARTBEAT_PERIOD_MS, then
* uint64_t region_gva, hb0;
* vgpup_discover_candidate(m, kcr3, &region_gva, &hb0); // (or reuse open's)
* // ... the caller sleeps here, NOT the core ...
* int alive = vgpup_confirm_alive(m, kcr3, region_gva, hb0);
*
* // sampling (lossy pull):
* static uint8_t buf[VGPU_SLOT_STRIDE];
* vgpup_frame_info fi;
* if (vgpup_sample_frame(r, m, kcr3, buf, sizeof buf, &fi) == 1) {
* // route fi.desc + buf[0..fi.bytes) to the chosen transport
* }
*
* vgpup_close(r); // frees core state only; (m, kcr3) stay with the caller
*/
#include <stdint.h>
#include <stddef.h>
#include "vgpu_stream.h" /* region ABI: producer/control types, slot geometry */
#include "memmodel.h" /* vmie_mem, gva_* (BORROWED access primitives) */
/* Opaque found vgpu region under (vmie_mem, kcr3). Heap-owned by the core; holds
* only small private state (region GVA, last frame_id, last run_epoch). It does
* NOT own (m, kcr3) — those are passed back in on every read. */
typedef struct vgpup_region vgpup_region;
/* ---- handle / lifecycle (the core does NOT own vmie_mem) ------------------ */
/* Phase-1 discover + bind: find the region by structural invariants, snapshot
* hb0, and build a handle. (m, kcr3) are BORROWED — the core reads them but
* never closes them. Returns a heap-owned vgpup_region*, or NULL if no region
* is found under this address space. Liveness is NOT yet proven: the caller must
* call vgpup_confirm_alive after waiting >= VGPU_HEARTBEAT_PERIOD_MS. Sampling
* before confirmation is allowed (lossy); "producer alive" is true only after a
* positive confirm. */
vgpup_region* vgpup_open(vmie_mem* m, uintptr_t kcr3);
/* Release ONLY the core state. Does NOT touch (m, kcr3) — the caller closes
* those (their lifetime is the caller's). Safe on NULL. */
void vgpup_close(vgpup_region* r);
/* ---- two-phase discovery (the WAIT belongs to the caller) ----------------- */
/* Phase 1: find a candidate by structural invariants (no liveness). On success
* writes the region base GVA (== producer-block GVA) to *out_region_gva and the
* heartbeat snapshot to *out_hb0, and returns 0. Returns <0 if no candidate is
* found or a read fails. Pure; does NOT wait. */
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
uint64_t* out_region_gva, uint64_t* out_hb0);
/* Phase 2: confirm liveness. The caller calls this >= VGPU_HEARTBEAT_PERIOD_MS
* after phase 1. Re-reads heartbeat at region_gva and returns 1 if it advanced
* (alive producer), 0 if it did not tick (dead / not the region), <0 on a read
* error. Pure; does NOT wait — the inter-phase delay is the caller's. */
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3,
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) -------------------- */
/* 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, uintptr_t kcr3,
uint8_t* dst, size_t cap, vgpup_frame_info* info);
/* Read the cursor under the cursor_seq acquire gate. 1 = consistent snapshot,
* 0 = writer busy past the retry limit, <0 = read error. */
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
vgpup_cursor* out);
/* Read display geometry under the geom_seq seqlock. Returns as read_cursor. */
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
vgpup_geometry* out);
/* Read the cold-line status/lifecycle. 0 = success, <0 = read error. The single
* atomic fields carry no seqlock; the snapshot is "fresh enough" (lossy). */
int vgpup_read_status(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
vgpup_status* out);
/* The run_epoch from the last vgpup_read_status — a session-break detector for
* the caller while kcr3 stays live. The core only reports the raw value; it
* holds no reset policy (what to reset is the caller's decision). */
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)
* 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 */
+42
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 {
@@ -81,6 +87,26 @@ typedef struct {
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");
@@ -101,6 +127,22 @@ static_assert(offsetof(vgpu_producer_t, full_frame_ack) == 164, "producer.ful
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)
+35
View File
@@ -0,0 +1,35 @@
/* 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.
*/
#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;
}
+114
View File
@@ -0,0 +1,114 @@
/* discover.c — region discovery by structural invariants (NO magic) + handle.
*
* The region is a pinned device shared-section projected into the System address
* space under kcr3, so we scan the KERNEL canonical half [KERN_MIN, ~0]. We find
* a contiguous readable run >= VGPU_REGION_BYTES (the region is GVA-contiguous,
* possibly spread across adjacent same-protection runs), read the producer block
* at its base, and accept it iff the whole structural-invariant table holds.
*
* There is NO magic field in the ABI and the owner forbids inventing one. The
* discriminator is the invariant table plus two-phase heartbeat liveness — and
* the inter-phase WAIT is the caller's (the core never sleeps).
*/
#include <stdlib.h>
#include "perception-internal.h"
/* How many region records to ask for when probing the System AS. The kernel half
* has few large same-protection runs; this is generous for the shared-section. */
#define VGPUP_MAX_REGIONS 256
/* Read the producer block at `region_gva` into *out (one gva_read of the whole
* block). 0 on success, <0 on read error. */
static int read_producer_block(vmie_mem* m, uintptr_t kcr3, uint64_t region_gva,
vgpu_producer_t* out)
{
return gva_read(m, kcr3, (uintptr_t)region_gva, out, sizeof *out) < 0 ? -1 : 0;
}
/* Walk the region runs in the kernel half and, for each contiguous readable span
* of >= VGPU_REGION_BYTES, test the producer block at the span base against the
* invariant table. On the first accepted candidate, write its base GVA + the
* heartbeat snapshot and return 0. Returns <0 if none is found / a read fails.
*
* 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. */
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
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, kcr3, KERN_MIN, ~0ull, VR_R, 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 readable 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, kcr3, span_base, &p) == 0 &&
vgpup_invariants_hold(&p)) {
*out_region_gva = span_base;
*out_hb0 = p.heartbeat;
return 0;
}
}
}
return -1;
}
/* Phase 2: re-read heartbeat at region_gva and report whether it advanced. The
* caller must have waited >= VGPU_HEARTBEAT_PERIOD_MS since phase 1. */
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3,
uint64_t region_gva, uint64_t hb0)
{
uint64_t hb_now;
if (!m) { return -1; }
if (gva_read(m, kcr3, (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_mem* m, uintptr_t kcr3)
{
uint64_t region_gva = 0, hb0 = 0;
vgpup_region* r;
if (vgpup_discover_candidate(m, kcr3, &region_gva, &hb0) != 0) { return NULL; }
r = (vgpup_region*)calloc(1, sizeof *r);
if (!r) { return NULL; }
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; (m, kcr3) belong to the caller */
}
uint32_t vgpup_run_epoch(const vgpup_region* r)
{
return r ? r->run_epoch : 0u;
}
@@ -0,0 +1,139 @@
#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 and the last-seen monotonic markers for dedup / session-break. */
struct vgpup_region {
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 */
};
/* ---- 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. 0 on success, <0 on read error. */
static inline int vgpup_read_seq(vmie_mem* m, uintptr_t kcr3, uint64_t gva,
uint32_t* out)
{
return gva_read(m, kcr3, (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 */
+200
View File
@@ -0,0 +1,200 @@
/* 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.
*/
#include "perception-internal.h"
/* Read one cold-line / packed field at producer offset `off` into dst. */
static int read_field(vmie_mem* m, uintptr_t kcr3, uint64_t region_gva,
size_t off, void* dst, size_t n)
{
return gva_read(m, kcr3, (uintptr_t)region_gva + off, dst, n) < 0 ? -1 : 0;
}
int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
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, kcr3, 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, kcr3, seq_gva, &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } /* writer in slot */
if (gva_read(m, kcr3, (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, kcr3, (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, kcr3, 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, uintptr_t kcr3,
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, kcr3, 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, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) {
return -1;
}
if (vgpup_read_seq(m, kcr3, 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, uintptr_t kcr3,
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, kcr3, 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, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) {
return -1;
}
if (vgpup_read_seq(m, kcr3, 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, uintptr_t kcr3,
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, kcr3, (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;
}
+270
View File
@@ -0,0 +1,270 @@
/* test_perception.c — table-driven invariant predicates + flat sampling smoke.
*
* Two layers:
* 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) Flat sampling smoke: 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 kernel VA, open it RO via vmie_mem_from_ro_fd, and run the
* real gva_*-driven discovery + frame/cursor/geometry/status reads and a
* two-phase heartbeat liveness check under that cr3. (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.)
*
* 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: flat sampling smoke 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_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 kernel
* VA `base` using 2 MiB large pages, with the PML4/PDPT/PD pages laid out right
* after the region in the same image. 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;
pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW;
while (mapped < span) {
pd[pdi0 + k] = mapped | PTE_P | PTE_RW | 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 */
const uint64_t base_va = KERN_MIN; /* kernel VA */
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; }
/* discovery: candidate found at the kernel VA with hb0 == 42 */
{
uint64_t rgva = 0xdead, hb0 = 0;
int rc = vgpup_discover_candidate(m, cr3, &rgva, &hb0);
CHECK(rc == 0, "discover_candidate rc");
CHECK(rgva == base_va, "discover_candidate region gva");
CHECK(hb0 == 42, "discover_candidate 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");
}
/* open handle + read API */
{
vgpup_region* r = vgpup_open(m, cr3);
CHECK(r != NULL, "vgpup_open");
if (r) {
uint8_t* dst = malloc(frame_bytes);
vgpup_frame_info fi;
vgpup_cursor cur;
vgpup_geometry geo;
vgpup_status st;
int rc;
CHECK(dst != NULL, "malloc dst");
rc = vgpup_sample_frame(r, m, cr3, 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, cr3, dst, frame_bytes, &fi) == 0, "sample dedup");
/* too-small buffer → lossy drop (0), not error */
CHECK(vgpup_sample_frame(r, m, cr3, dst, 1, &fi) == 0, "sample tiny-cap");
CHECK(vgpup_read_cursor(r, m, cr3, &cur) == 1, "read_cursor");
CHECK(vgpup_read_geometry(r, m, cr3, &geo) == 1, "read_geometry");
CHECK(vgpup_read_status(r, m, cr3, &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");
free(dst);
vgpup_close(r);
}
}
/* control-write seam: builds frame + offsets, writes nothing */
{
vgpup_region* r = vgpup_open(m, cr3);
if (r) {
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 rc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len);
CHECK(rc == 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");
vgpup_close(r);
}
}
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;
}
+25
View File
@@ -60,4 +60,29 @@ void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req);
* 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 */
+34
View File
@@ -127,3 +127,37 @@ void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint3
/* 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 -9
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,21 +136,13 @@ 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;
return changed;
}
void cursor_sample_pos(vgpu_ctx* ctx) {
vgpu_cursor_t* cur = &ctx->cursor;
CURSORINFO ci; ci.cbSize = sizeof ci;
if (!GetCursorInfo(&ci)) { cur->visible = 0; return; }
cur->visible = (ci.flags & CURSOR_SHOWING) != 0;
cur->x = ci.ptScreenPos.x;
cur->y = ci.ptScreenPos.y;
}
void cursor_draw(vgpu_ctx* ctx, uint8_t* dst, uint32_t W, uint32_t H) {
vgpu_cursor_t* cur = &ctx->cursor;
if (!cur->visible || cur->gw == 0) return;
+9 -3
View File
@@ -10,9 +10,15 @@
* Returns 1 if anything changed since last sample, else 0. */
int cursor_sample(vgpu_ctx* ctx);
/* Sample only cursor position/visibility (no shape extract). Updates
* ctx->cursor.{visible,x,y}. Cheap (one GetCursorInfo); safe to call every pump tick. */
void cursor_sample_pos(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 -13
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);
@@ -100,12 +111,6 @@ void vgpu_present_run(vgpu_ctx* ctx) {
last_beat = nowt;
}
/* --- cursor position: sensor data, reported every tick independent of
* draw_cursor / compositing (host may overlay it itself) --- */
cursor_sample_pos(ctx);
vgpu_publish_cursor(rv, (int32_t)ctx->cursor.x, (int32_t)ctx->cursor.y,
(uint32_t)(ctx->cursor.visible != 0));
/* --- reconcile control (gen-seqlock -> apply -> ack) --- */
vgpu_control_view cv;
uint32_t desired = prev_state;
@@ -152,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;
@@ -172,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);
@@ -182,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) {