mirror of
https://dev.lirent.ru/Vatrog/vm-vgpu-streamer.git
synced 2026-07-09 00:46:36 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9e32bbd956
|
|||
|
bcf708d3cc
|
@@ -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
|
||||
|
||||
@@ -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, ®ion_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 */
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, ®ion_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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user