/* test_perception.c — table-driven invariant predicates + per-cr3 user-AS scan. * * Two layers (no proc_list / win32 — that path needs a real Windows kernel * bring-up and is covered by an out-of-tree integration run, not this unit): * 1) Invariant predicates as a TABLE of cases over a synthesized producer * block (pure, no vmie): valid / latest==NONE / torn odd seq / non-BGRA / * stride!=width*4 / dims out of range — each asserts accept-vs-reject. * 2) Per-cr3 user-AS scan + sampling under a SYNTHETIC cr3: lay out a real * region per vgpu_stream.h in a memfd, build a minimal x86-64 identity page * table (2 MiB large pages) that maps the region at a USER VA (the region * really lives in a producer's user-AS), open it RO via vmie_mem_from_ro_fd, * and run vgpup_scan_user_as_for_region + a two-phase heartbeat liveness * check, then construct a handle (proc_cr3 = synth cr3) and run the real * frame/cursor/geometry/status reads and the control-write seam under it. * (cr3 0 over a flat image cannot translate — gva_* needs real page tables — * so we synthesize them; this exercises the actual translation path the * caller will use.) The win32 proc_list wrapper is deliberately NOT exercised * here: vgpup_scan_user_as_for_region is the pure per-cr3 core it calls. * * Exit 0 on all-pass; nonzero on the first failure. */ #define _GNU_SOURCE #include #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) /* ---- layer 1: invariant predicate table ---------------------------------- */ /* Build a baseline VALID producer block (one published BGRA frame in slot 0). */ static void make_valid_producer(vgpu_producer_t* p) { memset(p, 0, sizeof *p); p->latest = 0; p->frame_id = 1; p->seq[0] = 2; /* even = stable */ p->desc[0].width = 1920; p->desc[0].height = 1080; p->desc[0].stride = 1920 * 4; p->desc[0].format = VGPU_FMT_BGRA8888; p->desc[0].frame_id = 1; p->status = VGPU_ST_CAPTURING; p->backend = VGPU_BK_DDA; p->supported_formats = (1u << VGPU_FMT_BGRA8888); p->heartbeat = 42; } typedef struct { const char* name; void (*mutate)(vgpu_producer_t*); int expect; /* expected vgpup_invariants_hold result */ } inv_case; static void mut_none(vgpu_producer_t* p) { (void)p; } static void mut_latest_none(vgpu_producer_t* p) { p->latest = VGPU_LATEST_NONE; } static void mut_latest_oob(vgpu_producer_t* p) { p->latest = VGPU_SLOT_COUNT; } static void mut_seq_odd(vgpu_producer_t* p) { p->seq[0] = 3; } static void mut_not_bgra(vgpu_producer_t* p) { p->desc[0].format = 7; } static void mut_bad_stride(vgpu_producer_t* p) { p->desc[0].stride = 1920 * 4 + 1; } static void mut_width_zero(vgpu_producer_t* p) { p->desc[0].width = 0; } static void mut_width_huge(vgpu_producer_t* p) { p->desc[0].width = VGPU_MAX_WIDTH + 1; } static void mut_height_huge(vgpu_producer_t* p) { p->desc[0].height = VGPU_MAX_HEIGHT + 1; } static void mut_status_oob(vgpu_producer_t* p) { p->status = VGPU_ST_ERROR + 1; } static void mut_backend_oob(vgpu_producer_t* p) { p->backend = VGPU_BK_GDI + 1; } static void mut_no_bgra_support(vgpu_producer_t* p) { p->supported_formats = 0; } static const inv_case INV_CASES[] = { { "valid", mut_none, 1 }, { "latest==NONE", mut_latest_none, 1 }, /* no frame yet, still valid */ { "latest out of range", mut_latest_oob, 0 }, { "torn odd seq", mut_seq_odd, 0 }, { "non-BGRA format", mut_not_bgra, 0 }, { "stride != width*4", mut_bad_stride, 0 }, { "width == 0", mut_width_zero, 0 }, { "width too large", mut_width_huge, 0 }, { "height too large", mut_height_huge, 0 }, { "status out of range", mut_status_oob, 0 }, { "backend out of range", mut_backend_oob, 0 }, { "BGRA not supported", mut_no_bgra_support, 0 }, }; static void run_invariant_table(void) { size_t i; for (i = 0; i < sizeof(INV_CASES) / sizeof(INV_CASES[0]); ++i) { vgpu_producer_t p; int got; make_valid_producer(&p); INV_CASES[i].mutate(&p); got = vgpup_invariants_hold(&p); CHECK(got == INV_CASES[i].expect, INV_CASES[i].name); } } /* ---- layer 2: per-cr3 user-AS scan + sampling over a real RO vmie_mem ------ */ /* x86-64 paging entry flags for the synthetic identity table. */ #define PTE_P 0x1u /* present */ #define PTE_RW 0x2u /* writable */ #define PTE_US 0x4u /* user-accessible (the region is in a user-AS) */ #define PTE_PS 0x80u /* page size (2 MiB leaf at PD level) */ #define LARGE_PAGE (2ull * 1024 * 1024) /* Build a minimal identity page table mapping [0, span) of the image at user VA * `base` using 2 MiB large pages, with the PML4/PDPT/PD pages laid out right * after the region in the same image. Every level carries US so the run reports * VR_W|VR_U (a real user-AS mapping). Returns the cr3 (PML4 GPA). The mapped VA * range fits one PD (covers up to 1 GiB), which is plenty for the region. */ static uint64_t build_identity_table(uint8_t* img, uint64_t region_bytes, uint64_t base, uint64_t span) { const uint64_t pml4_gpa = region_bytes; /* one page each, after region */ const uint64_t pdpt_gpa = region_bytes + 0x1000; const uint64_t pd_gpa = region_bytes + 0x2000; uint64_t* pml4 = (uint64_t*)(img + pml4_gpa); uint64_t* pdpt = (uint64_t*)(img + pdpt_gpa); uint64_t* pd = (uint64_t*)(img + pd_gpa); const unsigned pml4i = (unsigned)((base >> 39) & 0x1ffu); const unsigned pdpti = (unsigned)((base >> 30) & 0x1ffu); const unsigned pdi0 = (unsigned)((base >> 21) & 0x1ffu); uint64_t mapped = 0; unsigned k = 0; pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW | PTE_US; pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW | PTE_US; while (mapped < span) { pd[pdi0 + k] = mapped | PTE_P | PTE_RW | PTE_US | PTE_PS; /* VA base+k*2M → GPA mapped */ mapped += LARGE_PAGE; ++k; } return pml4_gpa; } static void run_flat_smoke(void) { const uint64_t region_bytes = VGPU_REGION_BYTES; /* region rounded up to a 2 MiB boundary for the large-page identity map */ const uint64_t mapped_span = (region_bytes + LARGE_PAGE - 1) & ~(LARGE_PAGE - 1); const size_t total_bytes = (size_t)region_bytes + 0x3000; /* + PML4/PDPT/PD */ /* a USER VA, 2 MiB-aligned, within [USER_MIN, USER_MAX] — the region lives in * a producer's user address space, so we map it there (not at a kernel VA). */ const uint64_t base_va = 0x0000000010000000ull; const uint32_t w = 64, h = 32; const size_t frame_bytes = (size_t)w * h * 4u; int fd; uint8_t* img; uint64_t cr3; vmie_mem* m; vgpu_producer_t p; uint8_t marker; fd = memfd_create("vgpu-region", 0); CHECK(fd >= 0, "memfd_create"); if (fd < 0) { return; } if (ftruncate(fd, (off_t)total_bytes) != 0) { CHECK(0, "ftruncate"); close(fd); return; } img = mmap(NULL, total_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); CHECK(img != MAP_FAILED, "mmap"); if (img == MAP_FAILED) { close(fd); return; } /* lay out a valid producer block with one BGRA frame in slot 0 (at GPA 0) */ make_valid_producer(&p); p.desc[0].width = w; p.desc[0].height = h; p.desc[0].stride = w * 4u; memcpy(img + VGPU_PRODUCER_OFFSET, &p, sizeof p); /* fill the slot-0 frame bytes in the RING with a recognizable marker */ marker = 0xA5; memset(img + VGPU_RING_OFFSET + 0 * VGPU_SLOT_STRIDE, marker, frame_bytes); /* synthesize an identity table mapping the region at base_va, then open RO */ cr3 = build_identity_table(img, region_bytes, base_va, mapped_span); m = vmie_mem_from_ro_fd(fd, total_bytes); CHECK(m != NULL, "vmie_mem_from_ro_fd"); if (!m) { munmap(img, total_bytes); close(fd); return; } /* per-cr3 user-AS scan: candidate found at the user VA with hb0 == 42 */ { uint64_t rgva = 0xdead, hb0 = 0; int rc = vgpup_scan_user_as_for_region(m, cr3, &rgva, &hb0); CHECK(rc == 0, "scan_user_as rc"); CHECK(rgva == base_va, "scan_user_as region gva"); CHECK(hb0 == 42, "scan_user_as hb0"); /* two-phase liveness: not alive until heartbeat advances */ CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 0, "confirm not-yet-alive"); { uint64_t hb = 43; memcpy(img + offsetof(vgpu_producer_t, heartbeat), &hb, sizeof hb); } CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 1, "confirm alive after tick"); } /* construct a handle directly (the proc_list/win32 path is not unit-testable; * proc_cr3 is the synthetic cr3 here) and exercise the read API + control seam */ { vgpup_region rr; vgpup_region* r = &rr; uint8_t* dst = malloc(frame_bytes); vgpup_frame_info fi; vgpup_cursor cur; vgpup_geometry geo; vgpup_status st; int rc; memset(&rr, 0, sizeof rr); rr.proc_cr3 = cr3; rr.region_gva = base_va; rr.ctrl_gva = base_va + VGPU_CONTROL_OFFSET; rr.ring_gva = base_va + VGPU_RING_OFFSET; CHECK(dst != NULL, "malloc dst"); rc = vgpup_sample_frame(r, m, dst, frame_bytes, &fi); CHECK(rc == 1, "sample_frame fresh"); if (rc == 1) { CHECK(fi.desc.width == w && fi.desc.height == h, "sample dims"); CHECK(fi.bytes == frame_bytes, "sample bytes"); CHECK(dst[0] == marker && dst[frame_bytes - 1] == marker, "sample content"); } /* same frame_id → no fresh frame (dedup) */ CHECK(vgpup_sample_frame(r, m, dst, frame_bytes, &fi) == 0, "sample dedup"); /* too-small buffer → lossy drop (0), not error */ CHECK(vgpup_sample_frame(r, m, dst, 1, &fi) == 0, "sample tiny-cap"); CHECK(vgpup_read_cursor(r, m, &cur) == 1, "read_cursor"); CHECK(vgpup_read_geometry(r, m, &geo) == 1, "read_geometry"); CHECK(vgpup_read_status(r, m, &st) == 0, "read_status"); CHECK(st.status == VGPU_ST_CAPTURING, "status value"); CHECK(st.heartbeat == 43, "status heartbeat"); CHECK(vgpup_run_epoch(r) == st.run_epoch, "run_epoch accessor"); /* control-write seam: builds frame + offsets, writes nothing */ { vgpup_control_intent in = { VGPU_CMD_RUN, 60, 1, 7 }; vgpu_control_t frame; uint64_t ctrl_gva = 0; uint32_t off = 0, len = 0; int crc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len); CHECK(crc == 0, "build_control_write rc"); CHECK(frame.desired_state == VGPU_CMD_RUN, "control desired_state"); CHECK(frame.target_fps == 60, "control target_fps"); CHECK(frame.full_frame_req == 7, "control full_frame_req"); CHECK(frame.ctrl_gen == 0, "control ctrl_gen untouched"); CHECK(ctrl_gva == base_va + VGPU_CONTROL_OFFSET, "control gva"); CHECK(off == offsetof(vgpu_control_t, desired_state), "control off"); CHECK(len == offsetof(vgpu_control_t, full_frame_req) + sizeof(uint32_t) - offsetof(vgpu_control_t, desired_state), "control len"); } free(dst); } vmie_mem_close(m); /* the TEST owns vmie_mem here (it is the caller) */ munmap(img, total_bytes); close(fd); } int main(void) { run_invariant_table(); run_flat_smoke(); if (g_fail) { fprintf(stderr, "%d check(s) failed\n", g_fail); return 1; } printf("all checks passed\n"); return 0; }