feat(vgpu): coherent sub-rect capture in perception

Persistent capture window over a fixed sub-rectangle of the captured surface. open() allocates a stable buffer once; sync() re-fills it all-or-nothing from the current latest frame under one seqlock window (byte-exact, never torn). Pull only - no poll/thread inside; the caller drives. FLAT/ROWS layouts and BGRA/RGB24 formats chosen at open, orthogonal. Strict skip when the rect does not fit the current frame; no frame_id dedup.

Extract read_field into perception-internal.h as vgpup_read_field to share the producer-field read between sample.c and capture.c. Bump version to 0.3.14.
This commit is contained in:
2026-06-29 22:43:38 +03:00
parent 17ad439b9b
commit d66f19cb24
6 changed files with 746 additions and 23 deletions
+11 -1
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# Single source of truth for the version: CI passes -DVMSIG_VERSION=${TAG#v}, so the project # Single source of truth for the version: CI passes -DVMSIG_VERSION=${TAG#v}, so the project
# version (-> libvgpu-perception SONAME/.so version) and the .deb version come from one tag. # version (-> libvgpu-perception SONAME/.so version) and the .deb version come from one tag.
set(VMSIG_VERSION "0.3.13" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag") set(VMSIG_VERSION "0.3.14" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag")
project(vmsig VERSION ${VMSIG_VERSION} LANGUAGES C) project(vmsig VERSION ${VMSIG_VERSION} LANGUAGES C)
set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD 17)
@@ -130,6 +130,7 @@ if(VMSIG_WITH_VMIE)
add_library(vgpu-perception SHARED add_library(vgpu-perception SHARED
src/si/vgpu-perception/discover.c src/si/vgpu-perception/discover.c
src/si/vgpu-perception/sample.c src/si/vgpu-perception/sample.c
src/si/vgpu-perception/capture.c
src/si/vgpu-perception/control.c) src/si/vgpu-perception/control.c)
set_target_properties(vgpu-perception PROPERTIES set_target_properties(vgpu-perception PROPERTIES
VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) # libvgpu-perception.so.0 VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) # libvgpu-perception.so.0
@@ -147,6 +148,15 @@ if(VMSIG_WITH_VMIE)
add_test(NAME vgpu_perception COMMAND vgpu_perceptiontest) add_test(NAME vgpu_perception COMMAND vgpu_perceptiontest)
set_tests_properties(vgpu_perception PROPERTIES set_tests_properties(vgpu_perception PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${LIBVMIE_PATH}/.build:${CMAKE_BINARY_DIR}") ENVIRONMENT "LD_LIBRARY_PATH=${LIBVMIE_PATH}/.build:${CMAKE_BINARY_DIR}")
add_executable(vgpu_capturetest src/test/test_capture.c)
target_include_directories(vgpu_capturetest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/si/vgpu-perception/include)
target_link_libraries(vgpu_capturetest PRIVATE vgpu-perception)
target_compile_options(vgpu_capturetest PRIVATE -O2 -Wall -Wextra)
add_test(NAME vgpu_capture COMMAND vgpu_capturetest)
set_tests_properties(vgpu_capture PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${LIBVMIE_PATH}/.build:${CMAKE_BINARY_DIR}")
endif() endif()
# ---- vmsigd: the management daemon ----------------------------------------- # ---- vmsigd: the management daemon -----------------------------------------
+78
View File
@@ -234,6 +234,84 @@ int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out);
* value; it holds no reset policy (what to reset is the caller's decision). */ * value; it holds no reset policy (what to reset is the caller's decision). */
uint32_t vgpup_run_epoch(const vgpup_region* r); uint32_t vgpup_run_epoch(const vgpup_region* r);
/* ---- region capture: coherent pull of a sub-rectangle of the CURRENT frame -- *
* A persistent window over a fixed sub-rectangle of the captured surface. Open
* allocates a STABLE buffer once (address fixed across syncs); each sync re-fills
* it, all-or-nothing, from the CURRENT latest frame under one seqlock window
* (byte-exact, never torn — pixels may carry encrypted structures). Pull only: no
* poll / sleep / thread inside; the caller drives. NOT zero-copy: the rect crosses
* pages and must survive a seqlock re-check, so it is copied via gva_read (zero-copy
* would be live, hence non-coherent). Unlike vgpup_sample_frame there is NO frame_id
* dedup: each sync yields the coherent rect of the current frame even if frame_id is
* unchanged (info reports frame_id so the caller may dedup itself). */
/* Rectangle in captured-surface pixels; (0,0) = top-left; bounds are validated at
* sync time against the CURRENT frame's desc.width/height. No geometry translation. */
typedef struct { uint32_t x, y, w, h; } vgpup_rect;
/* Destination layout (chosen at open, orthogonal to format). */
enum { VGPUP_LAYOUT_FLAT = 0, /* one contiguous block, rows back-to-back */
VGPUP_LAYOUT_ROWS = 1 }; /* H separate row blocks; rows[] points at each */
/* Destination pixel format (chosen at open, orthogonal to layout). Source is ABI
* VGPU_FMT_BGRA8888 (4 bpp, memory order B,G,R,A, stride == width*4). */
enum { VGPUP_PX_BGRA = 0, /* 4 bpp, verbatim B,G,R,A from source */
VGPUP_PX_RGB24 = 1 }; /* 3 bpp, BGRA->RGB: drop A, out byte order R,G,B */
/* Opaque capture window; owns the stable buffer(s). Heap-owned by the core. */
typedef struct vgpup_capture vgpup_capture;
/* Per-sync metadata. desc mirrors the current frame; bytes is the TOTAL coherent
* payload size: for FLAT == rect.w*rect.h*out_bpp; for ROWS == rect.h*row_bytes
* (the sum of the row payloads, padding excluded). out_bpp / row_bytes are also
* available via the accessors below. */
typedef struct {
vgpup_frame_desc desc; /* descriptor of the frame this rect was taken from */
uint64_t frame_id; /* == desc.frame_id; surfaced for caller-side dedup */
size_t bytes; /* total coherent payload bytes (see above) */
} vgpup_capture_info;
/* Open a capture window over region handle r for sub-rectangle `rect`, with the
* given layout (VGPUP_LAYOUT_*) and format (VGPUP_PX_*). r MUST outlive the window
* (open caches proc_cr3 + ring_gva from r; sync does not take r). rect.w/h must be
* > 0 and rect.x+rect.w / rect.y+rect.h must not exceed the ABI ceilings
* (VGPU_MAX_WIDTH / VGPU_MAX_HEIGHT) — per-frame fit is checked at sync. Returns a
* heap-owned window, or NULL on bad args / bad enum / allocation failure.
*
* If a later sync returns <0 (producer restarted), the old window is invalid: the
* caller re-discovers (vgpup_close + vgpup_open) and must vgpup_capture_close this
* window and re-open a fresh one on the new region handle. */
vgpup_capture* vgpup_capture_open(vgpup_region* r, vgpup_rect rect,
int layout, int format);
/* Coherently re-fill the stable buffer from the CURRENT latest frame, under one
* seqlock window (fill, then re-check seq unchanged & even; changed -> retry up to
* VGPUP_SEQLOCK_RETRIES). m is BORROWED (== vmie_win32_mem(v)). Returns:
* 1 = stable buffer now holds the COHERENT rect (info filled);
* 0 = no frame yet / writer busy past the retry limit / rect does not fit the
* current frame (lossy SKIP — buffer is INVALID, do not consume, as with
* vgpup_sample_frame partial writes);
* <0 = hard read error (producer restarted -> caller re-discovers).
* NO frame_id dedup: a repeat sync of the same frame returns 1 again. */
int vgpup_capture_sync(vgpup_capture* c, vmie_mem* m, vgpup_capture_info* info);
/* Buffer geometry, fixed at open (do not change across syncs). */
uint32_t vgpup_capture_out_bpp(const vgpup_capture* c); /* 4 (BGRA) | 3 (RGB24) */
uint32_t vgpup_capture_row_bytes(const vgpup_capture* c); /* rect.w * out_bpp */
uint32_t vgpup_capture_row_count(const vgpup_capture* c); /* rect.h */
/* FLAT accessors: one contiguous block, rows back-to-back, sub-stride == row_bytes,
* pixel (x,y) at mem[(y*rect.w + x) * out_bpp]. NULL/0 if the window is ROWS. */
const uint8_t* vgpup_capture_mem(const vgpup_capture* c);
size_t vgpup_capture_bytes(const vgpup_capture* c); /* row_bytes*row_count */
/* ROWS accessors: an array of row_count pointers, each at a row_bytes block. The
* pointer array and the blocks are stable across syncs. NULL if the window is FLAT. */
const uint8_t* const* vgpup_capture_rows(const vgpup_capture* c);
/* Release the window and its buffer(s). Does NOT touch r / m. Safe on NULL. */
void vgpup_capture_close(vgpup_capture* c);
/* ---- control-write — SEAM ONLY (this never writes) ------------------------ */ /* ---- control-write — SEAM ONLY (this never writes) ------------------------ */
/* Desired control-block value (host-RW fields). The caller builds it and later /* Desired control-block value (host-RW fields). The caller builds it and later
+249
View File
@@ -0,0 +1,249 @@
/* capture.c — coherent pull of a sub-rectangle of the CURRENT frame.
*
* A persistent capture window over a fixed rect of the captured surface. open()
* allocates a stable buffer once (its address is fixed across syncs); sync()
* re-fills it all-or-nothing from the current latest frame under one seqlock
* window — the exact mirror of vgpup_sample_frame's discipline, but for a
* sub-rectangle, and WITHOUT frame_id dedup (a repeat sync of the same frame
* still yields the coherent rect — see vgpu_perception.h).
*
* Every read goes through gva_read under c->proc_cr3 into the stable buffer (or
* a per-window scratch for the RGB24 conversion); no gva_ptr is ever held across
* the seqlock window. NOT zero-copy by design (the rect crosses pages and must
* survive the seq re-check). All GVA arithmetic is done in uint64_t.
*
* The window caches proc_cr3 + ring_gva (copied from the region handle at open);
* the producer block lives at region_gva == ring_gva - VGPU_RING_OFFSET, where
* latest / seq[] / desc[] are read (the same fields sample.c reads via region_gva).
*/
#include "perception-internal.h"
#include <stdlib.h>
/* Round `v` up to the next multiple of `a` (a is a power of two). */
static inline size_t vgpup_align_up(size_t v, size_t a)
{
return (v + (a - 1u)) & ~(a - 1u);
}
vgpup_capture* vgpup_capture_open(vgpup_region* r, vgpup_rect rect,
int layout, int format)
{
vgpup_capture* c;
uint32_t out_bpp, row_bytes;
/* validate args and enums; fit against the ABI ceilings (NOT the current
* desc — open may precede the first frame; per-frame fit is a sync check).
* x+w / y+h computed in uint64_t to avoid overflow. */
if (!r) { return NULL; }
if (rect.w == 0u || rect.h == 0u) { return NULL; }
if (layout != VGPUP_LAYOUT_FLAT && layout != VGPUP_LAYOUT_ROWS) { return NULL; }
if (format != VGPUP_PX_BGRA && format != VGPUP_PX_RGB24) { return NULL; }
if ((uint64_t)rect.x + rect.w > VGPU_MAX_WIDTH) { return NULL; }
if ((uint64_t)rect.y + rect.h > VGPU_MAX_HEIGHT) { return NULL; }
out_bpp = (format == VGPUP_PX_RGB24) ? 3u : 4u;
row_bytes = rect.w * out_bpp;
c = calloc(1, sizeof *c);
if (!c) { return NULL; }
c->proc_cr3 = r->proc_cr3;
c->ring_gva = r->ring_gva;
c->x = rect.x;
c->y = rect.y;
c->w = rect.w;
c->h = rect.h;
c->out_bpp = out_bpp;
c->row_bytes = row_bytes;
c->layout = layout;
c->format = format;
/* RGB24 stages each source BGRA row in a per-window scratch (the source row
* is too large for the stack: up to VGPU_MAX_WIDTH*4). BGRA reads straight
* into the destination row, no scratch needed. */
if (format == VGPUP_PX_RGB24) {
c->scratch = malloc((size_t)VGPU_MAX_WIDTH * 4u);
if (!c->scratch) { vgpup_capture_close(c); return NULL; }
}
if (layout == VGPUP_LAYOUT_FLAT) {
/* one contiguous block, rows back-to-back (sub-stride == row_bytes) */
c->buf_bytes = (size_t)row_bytes * rect.h;
c->buf = calloc(1, c->buf_bytes);
if (!c->buf) { vgpup_capture_close(c); return NULL; }
} else {
/* ROWS: ONE backing block sliced into h cache-line-aligned sub-blocks +
* an array of h pointers. One allocation (less fragmentation, addresses
* stable since the backing is never reallocated); the padding between
* row_bytes..row_stride is never filled and is excluded from info->bytes. */
size_t row_stride = vgpup_align_up(row_bytes, VGPUP_ROW_ALIGN);
uint32_t i;
c->buf_bytes = row_stride * rect.h;
c->buf = calloc(1, c->buf_bytes);
if (!c->buf) { vgpup_capture_close(c); return NULL; }
c->rows = calloc(rect.h, sizeof *c->rows);
if (!c->rows) { vgpup_capture_close(c); return NULL; }
for (i = 0; i < rect.h; ++i) {
c->rows[i] = c->buf + (size_t)i * row_stride;
}
}
return c;
}
/* Convert w source BGRA pixels in `src` (B,G,R,A) to RGB24 (R,G,B) in `dst`. */
static inline void vgpup_bgra_to_rgb24(const uint8_t* src, uint8_t* dst, uint32_t w)
{
uint32_t i;
for (i = 0; i < w; ++i) {
dst[3u * i + 0u] = src[4u * i + 2u]; /* R */
dst[3u * i + 1u] = src[4u * i + 1u]; /* G */
dst[3u * i + 2u] = src[4u * i + 0u]; /* B */
}
}
int vgpup_capture_sync(vgpup_capture* c, vmie_mem* m, vgpup_capture_info* info)
{
unsigned attempt;
uint64_t region_gva;
if (!c || !m || !info) { return -1; }
/* the producer block sits one ring-offset below the cached ring base */
region_gva = c->ring_gva - VGPU_RING_OFFSET;
for (attempt = 0; attempt < VGPUP_SEQLOCK_RETRIES; ++attempt) {
uint32_t latest = 0, seq_before = 0, seq_after = 0;
vgpu_desc_t d;
uint64_t seq_gva, desc_gva, slot_base, src_bytes;
uint32_t row;
/* latest (acquire-equivalent: its own read) */
if (vgpup_read_field(m, c->proc_cr3, region_gva,
offsetof(vgpu_producer_t, latest),
&latest, sizeof latest) < 0) {
return -1;
}
if (latest == VGPU_LATEST_NONE || latest >= VGPU_SLOT_COUNT) {
return 0; /* no frame yet */
}
seq_gva = region_gva + offsetof(vgpu_producer_t, seq)
+ (uint64_t)latest * sizeof(uint32_t);
desc_gva = region_gva + offsetof(vgpu_producer_t, desc)
+ (uint64_t)latest * sizeof(vgpu_desc_t);
if (vgpup_read_seq(m, c->proc_cr3, seq_gva, &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } /* writer in slot */
if (gva_read(m, (uintptr_t)c->proc_cr3, (uintptr_t)desc_gva,
&d, sizeof d) < 0) {
return -1;
}
/* 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 */
}
/* NO frame_id dedup (intentional): a repeat sync of the same frame must
* still return the coherent rect (see vgpu_perception.h). */
/* fit against THIS frame: rect must lie within the current dimensions.
* A frame that became smaller is a strict lossy skip (no partial read). */
if ((uint64_t)c->x + c->w > d.width || (uint64_t)c->y + c->h > d.height) {
return 0;
}
/* fill the whole rect, row by row, from the latest slot under proc_cr3 */
slot_base = c->ring_gva + (uint64_t)latest * VGPU_SLOT_STRIDE;
src_bytes = (uint64_t)c->w * 4u; /* source is BGRA, 4 bpp */
for (row = 0; row < c->h; ++row) {
uint64_t src_gva = slot_base
+ (uint64_t)(c->y + row) * d.stride
+ (uint64_t)c->x * 4u;
uint8_t* dst = (c->layout == VGPUP_LAYOUT_ROWS)
? (uint8_t*)c->rows[row]
: c->buf + (size_t)row * c->row_bytes;
if (c->format == VGPUP_PX_RGB24) {
/* stage the source BGRA row, then convert into the dst row */
if (gva_read(m, (uintptr_t)c->proc_cr3, (uintptr_t)src_gva,
c->scratch, src_bytes) < 0) {
return -1;
}
vgpup_bgra_to_rgb24(c->scratch, dst, c->w);
} else {
/* BGRA verbatim: read straight into the dst row */
if (gva_read(m, (uintptr_t)c->proc_cr3, (uintptr_t)src_gva,
dst, src_bytes) < 0) {
return -1;
}
}
}
/* re-check the slot seq ONCE after the whole rect is filled: unchanged
* and still even → the rect is one coherent snapshot (all-or-nothing). */
if (vgpup_read_seq(m, c->proc_cr3, seq_gva, &seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) {
continue; /* the slot was rewritten under us — re-fill the rect */
}
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->frame_id = d.frame_id;
info->bytes = (size_t)c->row_bytes * c->h; /* payload only */
return 1;
}
return 0; /* writer kept the slot busy past the retry limit — lossy skip */
}
uint32_t vgpup_capture_out_bpp(const vgpup_capture* c)
{
return c ? c->out_bpp : 0u;
}
uint32_t vgpup_capture_row_bytes(const vgpup_capture* c)
{
return c ? c->row_bytes : 0u;
}
uint32_t vgpup_capture_row_count(const vgpup_capture* c)
{
return c ? c->h : 0u;
}
const uint8_t* vgpup_capture_mem(const vgpup_capture* c)
{
if (!c || c->layout != VGPUP_LAYOUT_FLAT) { return NULL; }
return c->buf;
}
size_t vgpup_capture_bytes(const vgpup_capture* c)
{
if (!c || c->layout != VGPUP_LAYOUT_FLAT) { return 0u; }
return (size_t)c->row_bytes * c->h;
}
const uint8_t* const* vgpup_capture_rows(const vgpup_capture* c)
{
if (!c || c->layout != VGPUP_LAYOUT_ROWS) { return NULL; }
return c->rows;
}
void vgpup_capture_close(vgpup_capture* c)
{
if (!c) { return; }
free(c->rows);
free(c->scratch);
free(c->buf);
free(c);
}
@@ -40,6 +40,30 @@ struct vgpup_region {
uint32_t run_epoch; /* last run_epoch seen via vgpup_read_status */ uint32_t run_epoch; /* last run_epoch seen via vgpup_read_status */
}; };
/* Row-start alignment for the ROWS layout backing block: one cache line, so the
* upper layer gets SIMD-friendly per-row pointers. row_stride = align_up(row_bytes,
* VGPUP_ROW_ALIGN); the padding between row_bytes..row_stride is never filled. */
#define VGPUP_ROW_ALIGN 64u
/* Capture window over a fixed sub-rectangle of the captured surface. Owns its
* stable buffer(s); the address is fixed across syncs (the backing is never
* reallocated). proc_cr3 / ring_gva are COPIED from the region handle at open so
* sync never dereferences `r` (it may be closed early by a caller bug — that must
* surface as re-discover, not a use-after-free). Heap-owned by the core. */
struct vgpup_capture {
uint64_t proc_cr3; /* copied from r at open (producer user-AS cr3) */
uint64_t ring_gva; /* copied from r at open (ring base GVA) */
uint32_t x, y, w, h; /* sub-rectangle in captured-surface pixels */
uint32_t out_bpp; /* 4 (BGRA) | 3 (RGB24) */
uint32_t row_bytes; /* w * out_bpp */
int layout; /* VGPUP_LAYOUT_* */
int format; /* VGPUP_PX_* */
uint8_t* buf; /* FLAT: the whole block; ROWS: backing for all rows */
size_t buf_bytes; /* total allocated payload bytes */
const uint8_t** rows; /* ROWS only: row_count pointers into buf; NULL if FLAT */
uint8_t* scratch; /* RGB24 only: VGPU_MAX_WIDTH*4 BGRA row staging; NULL */
};
/* Per-cr3 user-AS region scan (discovery steps 35 for ONE address space): scan /* Per-cr3 user-AS region scan (discovery steps 35 for ONE address space): scan
* gva_regions over [USER_MIN, USER_MAX] under `cr3` for a contiguous RW run of * gva_regions over [USER_MIN, USER_MAX] under `cr3` for a contiguous RW run of
* >= VGPU_REGION_BYTES, read the producer block at its base, and accept it iff * >= VGPU_REGION_BYTES, read the producer block at its base, and accept it iff
@@ -62,6 +86,15 @@ static inline int vgpup_read_seq(vmie_mem* m, uintptr_t cr3, uint64_t gva,
return gva_read(m, cr3, (uintptr_t)gva, out, sizeof *out) < 0 ? -1 : 0; return gva_read(m, cr3, (uintptr_t)gva, out, sizeof *out) < 0 ? -1 : 0;
} }
/* Read one cold-line / packed field at producer offset `off` into dst under the
* producer's user-AS cr3. Shared by all perception TUs (sample / capture) so the
* field-read discipline is not duplicated across the module. 0 / <0. */
static inline int vgpup_read_field(vmie_mem* m, uintptr_t cr3, uint64_t region_gva,
size_t off, void* dst, size_t n)
{
return gva_read(m, cr3, (uintptr_t)region_gva + off, dst, n) < 0 ? -1 : 0;
}
/* ---- packed-field unpackers (cursor line) -------------------------------- */ /* ---- 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_x(uint64_t pos) { return (int32_t)(uint32_t)(pos & 0xFFFFFFFFu); }
+14 -22
View File
@@ -18,14 +18,6 @@
#include "perception-internal.h" #include "perception-internal.h"
#include <stdio.h> /* TEMP debug (revert): stderr skip-reason trace */ #include <stdio.h> /* TEMP debug (revert): stderr skip-reason trace */
/* Read one cold-line / packed field at producer offset `off` into dst under the
* producer's user-AS cr3. */
static int read_field(vmie_mem* m, uintptr_t cr3, uint64_t region_gva,
size_t off, void* dst, size_t n)
{
return gva_read(m, cr3, (uintptr_t)region_gva + off, dst, n) < 0 ? -1 : 0;
}
int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, int vgpup_sample_frame(vgpup_region* r, vmie_mem* m,
uint8_t* dst, size_t cap, vgpup_frame_info* info) uint8_t* dst, size_t cap, vgpup_frame_info* info)
{ {
@@ -42,7 +34,7 @@ int vgpup_sample_frame(vgpup_region* r, vmie_mem* m,
size_t frame_bytes; size_t frame_bytes;
/* latest (acquire-equivalent: its own read) */ /* latest (acquire-equivalent: its own read) */
if (read_field(m, r->proc_cr3, r->region_gva, if (vgpup_read_field(m, r->proc_cr3, r->region_gva,
offsetof(vgpu_producer_t, latest), &latest, sizeof latest) < 0) { offsetof(vgpu_producer_t, latest), &latest, sizeof latest) < 0) {
if (_dbg) fprintf(stderr, "VGPUP_DBG ret=-1 latest-read-fail\n"); if (_dbg) fprintf(stderr, "VGPUP_DBG ret=-1 latest-read-fail\n");
return -1; return -1;
@@ -132,11 +124,11 @@ int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out)
&seq_before) < 0) { return -1; } &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } if (vgpup_seq_is_writing(seq_before)) { continue; }
if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 || if (vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) { vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) {
return -1; return -1;
} }
@@ -173,14 +165,14 @@ int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out)
&seq_before) < 0) { return -1; } &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } if (vgpup_seq_is_writing(seq_before)) { continue; }
if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 || if (vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 || vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 ||
read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) { vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) {
return -1; return -1;
} }
+361
View File
@@ -0,0 +1,361 @@
/* test_capture.c — region capture (sub-rectangle coherent pull) smoke test.
*
* Mirrors test_perception.c layer 2: lay out a real vgpu region per
* vgpu_stream.h in a memfd, build a minimal x86-64 identity page table (2 MiB
* large pages) mapping the region at a USER VA, open it RO via vmie_mem_from_ro_fd,
* and craft one BGRA frame in slot 0 with a coordinate-derived pixel pattern.
* Then exercise vgpup_capture_open/sync/accessors/close across layout × format,
* offset rects, the OOB strict skip, the no-dedup contract, and bad args.
*
* The TEST owns the vmie_mem (it is the caller). Exit 0 on all-pass.
*/
#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)
/* ---- synthetic identity page table (same mechanics as test_perception.c) --- */
#define PTE_P 0x1u
#define PTE_RW 0x2u
#define PTE_US 0x4u
#define PTE_PS 0x80u
#define LARGE_PAGE (2ull * 1024 * 1024)
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;
const uint64_t pdpt_gpa = region_bytes + 0x1000;
const uint64_t pd_gpa = region_bytes + 0x2000;
uint64_t* pml4 = (uint64_t*)(img + pml4_gpa);
uint64_t* pdpt = (uint64_t*)(img + pdpt_gpa);
uint64_t* pd = (uint64_t*)(img + pd_gpa);
const unsigned pml4i = (unsigned)((base >> 39) & 0x1ffu);
const unsigned pdpti = (unsigned)((base >> 30) & 0x1ffu);
const unsigned pdi0 = (unsigned)((base >> 21) & 0x1ffu);
uint64_t mapped = 0;
unsigned k = 0;
pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW | PTE_US;
pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW | PTE_US;
while (mapped < span) {
pd[pdi0 + k] = mapped | PTE_P | PTE_RW | PTE_US | PTE_PS;
mapped += LARGE_PAGE;
++k;
}
return pml4_gpa;
}
/* The full-surface frame dimensions crafted in slot 0. */
#define FRAME_W 256u
#define FRAME_H 128u
/* Coordinate-derived BGRA bytes for surface pixel (px, py) — distinct per
* channel so offset AND conversion are both checkable on concrete bytes. */
static uint8_t pat_B(uint32_t px, uint32_t py) { (void)py; return (uint8_t)(px & 0xFFu); }
static uint8_t pat_G(uint32_t px, uint32_t py) { (void)px; return (uint8_t)(py & 0xFFu); }
static uint8_t pat_R(uint32_t px, uint32_t py) { return (uint8_t)((px ^ py) & 0xFFu); }
static uint8_t pat_A(uint32_t px, uint32_t py) { (void)px; (void)py; return 0xFFu; }
/* Fill the slot-0 frame in the RING with the coordinate pattern (BGRA order). */
static void fill_pattern_frame(uint8_t* img, uint32_t w, uint32_t h)
{
uint8_t* slot = img + VGPU_RING_OFFSET + 0u * VGPU_SLOT_STRIDE;
uint32_t py, px;
for (py = 0; py < h; ++py) {
for (px = 0; px < w; ++px) {
uint8_t* p = slot + ((size_t)py * w + px) * 4u;
p[0] = pat_B(px, py);
p[1] = pat_G(px, py);
p[2] = pat_R(px, py);
p[3] = pat_A(px, py);
}
}
}
static void make_producer(vgpu_producer_t* p, uint32_t w, uint32_t h)
{
memset(p, 0, sizeof *p);
p->latest = 0;
p->frame_id = 7;
p->seq[0] = 2; /* even = stable */
p->desc[0].width = w;
p->desc[0].height = h;
p->desc[0].stride = w * 4u;
p->desc[0].format = VGPU_FMT_BGRA8888;
p->desc[0].frame_id = 7;
p->status = VGPU_ST_CAPTURING;
p->backend = VGPU_BK_DDA;
p->supported_formats = (1u << VGPU_FMT_BGRA8888);
p->heartbeat = 1;
}
/* ---- the test driver, given an open RO region handle ---------------------- */
static void run_capture(vmie_mem* m, uint64_t base_va, uint64_t cr3, uint8_t* img)
{
vgpup_region rr;
vgpup_region* r = &rr;
memset(&rr, 0, sizeof rr);
rr.proc_cr3 = cr3;
rr.region_gva = base_va;
rr.ctrl_gva = base_va + VGPU_CONTROL_OFFSET;
rr.ring_gva = base_va + VGPU_RING_OFFSET;
/* an offset sub-rectangle, strictly inside the frame (case 6: x,y > 0,
* w < width, h < height). */
const uint32_t rx = 10, ry = 20, rw = 32, rh = 16;
vgpup_rect rect = { rx, ry, rw, rh };
/* ---- case 2: FLAT × BGRA ---------------------------------------------- */
{
vgpup_capture* c = vgpup_capture_open(r, rect, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA);
vgpup_capture_info info;
int rc;
CHECK(c != NULL, "flat/bgra open");
rc = vgpup_capture_sync(c, m, &info);
CHECK(rc == 1, "flat/bgra sync==1");
CHECK(vgpup_capture_out_bpp(c) == 4u, "flat/bgra out_bpp");
CHECK(vgpup_capture_row_bytes(c) == rw * 4u, "flat/bgra row_bytes");
CHECK(vgpup_capture_bytes(c) == (size_t)rw * rh * 4u, "flat/bgra bytes");
CHECK(vgpup_capture_rows(c) == NULL, "flat/bgra rows NULL");
if (rc == 1) {
const uint8_t* mem = vgpup_capture_mem(c);
uint32_t yy, xx;
int ok = 1;
CHECK(mem != NULL, "flat/bgra mem non-NULL");
for (yy = 0; yy < rh && ok; ++yy) {
for (xx = 0; xx < rw; ++xx) {
const uint8_t* px = mem + ((size_t)yy * rw + xx) * 4u;
uint32_t sx = rx + xx, sy = ry + yy;
if (px[0] != pat_B(sx, sy) || px[1] != pat_G(sx, sy) ||
px[2] != pat_R(sx, sy) || px[3] != pat_A(sx, sy)) {
ok = 0; break;
}
}
}
CHECK(ok, "flat/bgra verbatim content");
CHECK(info.frame_id == 7, "flat/bgra info frame_id"); /* case 11 */
CHECK(info.desc.frame_id == 7, "flat/bgra desc frame_id"); /* case 11 */
CHECK(info.bytes == (size_t)rw * rh * 4u, "flat/bgra info bytes");
}
/* case 9: NO dedup — a repeat sync of the same frame returns 1 again. */
CHECK(vgpup_capture_sync(c, m, &info) == 1, "flat/bgra no-dedup repeat==1");
vgpup_capture_close(c);
}
/* ---- case 3: FLAT × RGB24 -------------------------------------------- */
{
vgpup_capture* c = vgpup_capture_open(r, rect, VGPUP_LAYOUT_FLAT, VGPUP_PX_RGB24);
vgpup_capture_info info;
int rc;
CHECK(c != NULL, "flat/rgb24 open");
CHECK(vgpup_capture_out_bpp(c) == 3u, "flat/rgb24 out_bpp");
rc = vgpup_capture_sync(c, m, &info);
CHECK(rc == 1, "flat/rgb24 sync==1");
CHECK(vgpup_capture_bytes(c) == (size_t)rw * rh * 3u, "flat/rgb24 bytes");
if (rc == 1) {
const uint8_t* mem = vgpup_capture_mem(c);
uint32_t yy, xx;
int ok = 1;
for (yy = 0; yy < rh && ok; ++yy) {
for (xx = 0; xx < rw; ++xx) {
const uint8_t* px = mem + ((size_t)yy * rw + xx) * 3u;
uint32_t sx = rx + xx, sy = ry + yy;
/* R,G,B (drop A, swap B<->R) */
if (px[0] != pat_R(sx, sy) || px[1] != pat_G(sx, sy) ||
px[2] != pat_B(sx, sy)) {
ok = 0; break;
}
}
}
CHECK(ok, "flat/rgb24 converted content");
CHECK(info.bytes == (size_t)rw * rh * 3u, "flat/rgb24 info bytes");
}
vgpup_capture_close(c);
}
/* ---- case 4: ROWS × BGRA -------------------------------------------- */
{
vgpup_capture* c = vgpup_capture_open(r, rect, VGPUP_LAYOUT_ROWS, VGPUP_PX_BGRA);
vgpup_capture_info info, info2;
const uint8_t* const* rows;
const uint8_t* saved[64];
int rc;
CHECK(c != NULL, "rows/bgra open");
CHECK(vgpup_capture_mem(c) == NULL, "rows/bgra mem NULL");
CHECK(vgpup_capture_row_count(c) == rh, "rows/bgra row_count");
rc = vgpup_capture_sync(c, m, &info);
CHECK(rc == 1, "rows/bgra sync==1");
rows = vgpup_capture_rows(c);
CHECK(rows != NULL, "rows/bgra rows non-NULL");
if (rc == 1 && rows) {
uint32_t yy, xx;
int ok = 1;
for (yy = 0; yy < rh && ok; ++yy) {
const uint8_t* line = rows[yy];
saved[yy] = line;
for (xx = 0; xx < rw; ++xx) {
const uint8_t* px = line + (size_t)xx * 4u;
uint32_t sx = rx + xx, sy = ry + yy;
if (px[0] != pat_B(sx, sy) || px[1] != pat_G(sx, sy) ||
px[2] != pat_R(sx, sy) || px[3] != pat_A(sx, sy)) {
ok = 0; break;
}
}
}
CHECK(ok, "rows/bgra per-row content");
}
/* row pointers stable across a second sync */
CHECK(vgpup_capture_sync(c, m, &info2) == 1, "rows/bgra second sync==1");
{
const uint8_t* const* rows2 = vgpup_capture_rows(c);
uint32_t yy;
int stable = 1;
for (yy = 0; yy < rh; ++yy) {
if (rows2[yy] != saved[yy]) { stable = 0; break; }
}
CHECK(stable, "rows/bgra row pointers stable across syncs");
}
vgpup_capture_close(c);
}
/* ---- case 5: ROWS × RGB24 ------------------------------------------- */
{
vgpup_capture* c = vgpup_capture_open(r, rect, VGPUP_LAYOUT_ROWS, VGPUP_PX_RGB24);
vgpup_capture_info info;
const uint8_t* const* rows;
int rc;
CHECK(c != NULL, "rows/rgb24 open");
CHECK(vgpup_capture_row_count(c) == rh, "rows/rgb24 row_count");
CHECK(vgpup_capture_row_bytes(c) == rw * 3u, "rows/rgb24 row_bytes");
rc = vgpup_capture_sync(c, m, &info);
CHECK(rc == 1, "rows/rgb24 sync==1");
rows = vgpup_capture_rows(c);
if (rc == 1 && rows) {
uint32_t yy, xx;
int ok = 1;
for (yy = 0; yy < rh && ok; ++yy) {
const uint8_t* line = rows[yy];
for (xx = 0; xx < rw; ++xx) {
const uint8_t* px = line + (size_t)xx * 3u;
uint32_t sx = rx + xx, sy = ry + yy;
if (px[0] != pat_R(sx, sy) || px[1] != pat_G(sx, sy) ||
px[2] != pat_B(sx, sy)) {
ok = 0; break;
}
}
}
CHECK(ok, "rows/rgb24 per-row converted content");
}
vgpup_capture_close(c);
}
/* ---- case 7: coherence / retry — odd seq -> 0, even -> 1 ------------- */
{
vgpup_capture* c = vgpup_capture_open(r, rect, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA);
vgpup_capture_info info;
uint32_t seq_odd = 3, seq_even = 2;
size_t seq0_off = offsetof(vgpu_producer_t, seq); /* slot 0 */
CHECK(c != NULL, "retry open");
/* writer busy: slot seq odd before sync -> retry-exhaust -> 0 */
memcpy(img + seq0_off, &seq_odd, sizeof seq_odd);
CHECK(vgpup_capture_sync(c, m, &info) == 0, "retry odd-seq -> 0");
/* back to even -> coherent -> 1 */
memcpy(img + seq0_off, &seq_even, sizeof seq_even);
CHECK(vgpup_capture_sync(c, m, &info) == 1, "retry even-seq -> 1");
vgpup_capture_close(c);
}
/* ---- case 8: strict OOB skip (frame smaller than rect) --------------- */
{
/* rect valid vs ABI ceilings but past THIS frame's width -> sync 0 */
vgpup_rect big = { FRAME_W - 8u, 0u, 16u, 8u }; /* x+w = FRAME_W+8 > width */
vgpup_capture* c = vgpup_capture_open(r, big, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA);
vgpup_capture_info info;
CHECK(c != NULL, "oob open (valid vs ABI)");
CHECK(vgpup_capture_sync(c, m, &info) == 0, "oob sync lossy skip == 0");
vgpup_capture_close(c);
}
/* ---- case 10: bad args ---------------------------------------------- */
{
vgpup_rect ok_rect = { 0, 0, 8, 8 };
vgpup_rect w0 = { 0, 0, 0, 8 };
vgpup_rect h0 = { 0, 0, 8, 0 };
vgpup_rect xw = { VGPU_MAX_WIDTH - 4u, 0, 8, 8 }; /* x+w > MAX_WIDTH */
vgpup_rect yh = { 0, VGPU_MAX_HEIGHT - 4u, 8, 8 }; /* y+h > MAX_HEIGHT */
CHECK(vgpup_capture_open(NULL, ok_rect, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA) == NULL, "bad: r NULL");
CHECK(vgpup_capture_open(r, w0, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA) == NULL, "bad: w==0");
CHECK(vgpup_capture_open(r, h0, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA) == NULL, "bad: h==0");
CHECK(vgpup_capture_open(r, xw, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA) == NULL, "bad: x+w>MAX");
CHECK(vgpup_capture_open(r, yh, VGPUP_LAYOUT_FLAT, VGPUP_PX_BGRA) == NULL, "bad: y+h>MAX");
CHECK(vgpup_capture_open(r, ok_rect, 99, VGPUP_PX_BGRA) == NULL, "bad: layout");
CHECK(vgpup_capture_open(r, ok_rect, VGPUP_LAYOUT_FLAT, 99) == NULL, "bad: format");
}
/* close on NULL is safe */
vgpup_capture_close(NULL);
}
static void run_smoke(void)
{
const uint64_t region_bytes = VGPU_REGION_BYTES;
const uint64_t mapped_span = (region_bytes + LARGE_PAGE - 1) & ~(LARGE_PAGE - 1);
const size_t total_bytes = (size_t)region_bytes + 0x3000;
const uint64_t base_va = 0x0000000010000000ull;
int fd;
uint8_t* img;
uint64_t cr3;
vmie_mem* m;
vgpu_producer_t p;
fd = memfd_create("vgpu-cap-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; }
make_producer(&p, FRAME_W, FRAME_H);
memcpy(img + VGPU_PRODUCER_OFFSET, &p, sizeof p);
fill_pattern_frame(img, FRAME_W, FRAME_H);
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; }
run_capture(m, base_va, cr3, img);
vmie_mem_close(m);
munmap(img, total_bytes);
close(fd);
}
int main(void)
{
run_smoke();
if (g_fail) {
fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
printf("all checks passed\n");
return 0;
}