diff --git a/CMakeLists.txt b/CMakeLists.txt index 5813339..ad6919b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) # 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. -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) set(CMAKE_C_STANDARD 17) @@ -130,6 +130,7 @@ if(VMSIG_WITH_VMIE) add_library(vgpu-perception SHARED src/si/vgpu-perception/discover.c src/si/vgpu-perception/sample.c + src/si/vgpu-perception/capture.c src/si/vgpu-perception/control.c) set_target_properties(vgpu-perception PROPERTIES 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) set_tests_properties(vgpu_perception PROPERTIES 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() # ---- vmsigd: the management daemon ----------------------------------------- diff --git a/include/vgpu_perception.h b/include/vgpu_perception.h index 7cc648d..b654e8a 100644 --- a/include/vgpu_perception.h +++ b/include/vgpu_perception.h @@ -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). */ 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) ------------------------ */ /* Desired control-block value (host-RW fields). The caller builds it and later diff --git a/src/si/vgpu-perception/capture.c b/src/si/vgpu-perception/capture.c new file mode 100644 index 0000000..6e2a537 --- /dev/null +++ b/src/si/vgpu-perception/capture.c @@ -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 + +/* 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); +} diff --git a/src/si/vgpu-perception/include/perception-internal.h b/src/si/vgpu-perception/include/perception-internal.h index ceea25f..66caee2 100644 --- a/src/si/vgpu-perception/include/perception-internal.h +++ b/src/si/vgpu-perception/include/perception-internal.h @@ -40,6 +40,30 @@ struct vgpup_region { 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 3–5 for ONE address space): scan * gva_regions over [USER_MIN, USER_MAX] under `cr3` for a contiguous RW run of * >= VGPU_REGION_BYTES, read the producer block at its base, and accept it iff @@ -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; } +/* 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) -------------------------------- */ static inline int32_t vgpup_cursor_x(uint64_t pos) { return (int32_t)(uint32_t)(pos & 0xFFFFFFFFu); } diff --git a/src/si/vgpu-perception/sample.c b/src/si/vgpu-perception/sample.c index 812d6ca..d34269e 100644 --- a/src/si/vgpu-perception/sample.c +++ b/src/si/vgpu-perception/sample.c @@ -18,14 +18,6 @@ #include "perception-internal.h" #include /* 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, 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; /* 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) { if (_dbg) fprintf(stderr, "VGPUP_DBG ret=-1 latest-read-fail\n"); return -1; @@ -132,11 +124,11 @@ int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out) &seq_before) < 0) { return -1; } if (vgpup_seq_is_writing(seq_before)) { continue; } - if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) { + if (vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 || + vgpup_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_hotspot), &hotspot, sizeof hotspot) < 0 || + vgpup_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_id), &id, sizeof id) < 0) { return -1; } @@ -173,14 +165,14 @@ int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out) &seq_before) < 0) { return -1; } if (vgpup_seq_is_writing(seq_before)) { continue; } - if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 || - read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) { + if (vgpup_read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 || + vgpup_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_w), &virt_w, sizeof virt_w) < 0 || + vgpup_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, cap_x), &cap_x, sizeof cap_x) < 0 || + vgpup_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, dpi), &dpi, sizeof dpi) < 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; } diff --git a/src/test/test_capture.c b/src/test/test_capture.c new file mode 100644 index 0000000..617a34c --- /dev/null +++ b/src/test/test_capture.c @@ -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 +#include +#include +#include +#include + +#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; +}