// SPDX-License-Identifier: BSD-3-Clause /* * fbcopy_pub.c - capture the vGPU framebuffer WITHOUT X and publish frames * into the kvmfr ivshmem BAR. * * The X server's screen has 1280x720 pixels in the emulated VRAM, which the * guest maps as the vGPU's BAR1 (resource1). The X SHM readback path can * stall behind the X server's own push wait; this tool reads the framebuffer * pixels straight out of the BAR1 mapping instead (host RAM backing, no GPU * or X round-trip), swizzles BGRA -> RGBA and writes the kvmfr frame * protocol the host reader already decodes. * * Usage (guest, as root): * ./fbcopy_pub test one solid red frame (pipeline check) * ./fbcopy_pub find [passes] [ms] motion-scan first 64 MB of BAR1 * ./fbcopy_pub findp [off] [sz] [p] [ms] * pread motion-scan of any BAR1 range * (mmap only reaches the first 64 MB) * ./fbcopy_pub pub [n] [fps] [w] [h] [pitch] * copy w*h from BAR1@off (pitch), * mmap path (off < 64 MB) * ./fbcopy_pub pubp [n] [fps] [w] [h] [pitch] * same but pread path (any offset) * * Find/build on the host (same glibc as the guest): gcc -O2 -o fbcopy_pub * fbcopy_pub.c */ #include #include #include #include #include #include #include #include #include #include #include #define FRAME_OFFSET 0x100000UL #define HDR_MAGIC 0x4b564d46u /* "FMVK" */ #define DEF_W 1280 #define DEF_H 720 #define DEF_PITCH 5120 /* xorg-vgpu.conf: 1280*4 rounded */ struct frame_hdr { uint32_t magic; uint32_t version; uint32_t width; uint32_t height; uint32_t stride; uint32_t format; /* 0 = RGBA8 */ uint32_t seq; uint32_t reserved; uint64_t timestamp_ns; }; static uint64_t now_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ull + ts.tv_nsec; } static const char *find_vgpu_resource1(void) { static char path[320]; DIR *d = opendir("/sys/bus/pci/devices"); struct dirent *de; if (!d) return NULL; while ((de = readdir(d))) { char p[340], buf[32]; FILE *f; int n; if (de->d_name[0] == '.') continue; snprintf(p, sizeof(p), "/sys/bus/pci/devices/%s/vendor", de->d_name); f = fopen(p, "r"); if (!f) continue; n = fread(buf, 1, sizeof(buf) - 1, f); fclose(f); if (n <= 0) continue; buf[n] = 0; if (strtoul(buf, NULL, 16) != 0x10de) continue; snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource1", de->d_name); closedir(d); return path; } closedir(d); return NULL; } /* * The vfio mdev region mmap is limited: only file offset 0 with len <= 64 MB * works (nonzero offsets and 256 MB both EINVAL). Map the first 64 MB once at * offset 0 and index into the mapping for every access. */ #define CHUNK_MAX (64UL << 20) static unsigned char *map_vram(int fd, size_t want, size_t *got) { size_t len = want > CHUNK_MAX ? CHUNK_MAX : want; unsigned char *p; p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); if (p == MAP_FAILED) return NULL; *got = len; return p; } static double t_ms(const struct timespec *a, const struct timespec *b) { return (double)(b->tv_sec - a->tv_sec) * 1e3 + (double)(b->tv_nsec - a->tv_nsec) / 1e6; } /* * Find the largest contiguous run of "dense" pages (>= 64 of 256 sampled * bytes nonzero) - an image in VRAM looks like one long dense run. Small * gaps (<= 16 pages) are tolerated. */ static void report_cluster(const uint32_t *nz, size_t pages, unsigned long long base_off, const char *tag) { size_t p, run_start = 0, last_dense = 0, best_start = 0, best_len = 0; int in_run = 0; for (p = 0; p <= pages; p++) { int dense = p < pages && nz[p] >= 64; if (dense) { if (!in_run) { run_start = p; in_run = 1; } last_dense = p; } else if (in_run && p - last_dense > 16) { size_t len = last_dense - run_start + 1; if (len > best_len) { best_len = len; best_start = run_start; } in_run = 0; } } if (best_len >= 8) printf("%s_CLUSTER off=0x%llx pages=%zu bytes=%zu\n", tag, base_off + ((unsigned long long)best_start << 12), best_len, best_len << 12); } int main(int argc, char **argv) { const char *mode = argc > 1 ? argv[1] : "find"; const char *bar_path = getenv("FBCOPY_BAR") ? getenv("FBCOPY_BAR") : "/sys/bus/pci/devices/0000:00:03.0/resource2"; const char *res1 = getenv("FBCOPY_RES1") ? getenv("FBCOPY_RES1") : find_vgpu_resource1(); struct stat st; size_t map_size, i; unsigned char *bar; struct frame_hdr *hdr; volatile uint32_t *seqp; int fd, vram_fd; setvbuf(stdout, NULL, _IONBF, 0); if (!res1) { printf("NO_VGPU\n"); return 1; } printf("VGPU_RES1 %s\n", res1); if (stat(res1, &st) || !st.st_size) { printf("STAT_FAIL\n"); return 1; } map_size = (size_t)st.st_size; printf("VRAM_SIZE 0x%zx (%zu MB)\n", map_size, map_size >> 20); vram_fd = open(res1, O_RDONLY | O_SYNC); if (vram_fd < 0) { perror(res1); return 1; } /* kvmfr BAR next: every mode needs it except find. */ fd = open(bar_path, O_RDWR | O_SYNC); if (fd < 0) { perror(bar_path); return 1; } bar = (unsigned char *)mmap(NULL, 128UL << 20, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); if (bar == MAP_FAILED) { perror("mmap kvmfr"); return 1; } hdr = (struct frame_hdr *)(bar + FRAME_OFFSET); seqp = &hdr->seq; if (!strcmp(mode, "zero")) { /* Reset the kvmfr header seq so a reader seeding from it will * accept the next pub run's frame 1. */ hdr->magic = HDR_MAGIC; hdr->version = 1; hdr->width = DEF_W; hdr->height = DEF_H; hdr->stride = DEF_W * 4; hdr->format = 0; hdr->timestamp_ns = now_ns(); __sync_synchronize(); *seqp = 0; printf("SEQ_ZEROED\n"); return 0; } if (!strcmp(mode, "test")) { unsigned char *dst = bar + FRAME_OFFSET + sizeof(struct frame_hdr); size_t n = (size_t)DEF_W * DEF_H * 4; hdr->magic = HDR_MAGIC; hdr->version = 1; hdr->width = DEF_W; hdr->height = DEF_H; hdr->stride = DEF_W * 4; hdr->format = 0; for (i = 0; i < n; i += 4) { dst[i + 0] = (unsigned char)(argc > 2 ? atoi(argv[2]) : 255); dst[i + 1] = (unsigned char)(argc > 3 ? atoi(argv[3]) : 0); dst[i + 2] = (unsigned char)(argc > 4 ? atoi(argv[4]) : 0); dst[i + 3] = 0xff; } hdr->timestamp_ns = now_ns(); __sync_synchronize(); *seqp = 1; printf("TEST_FRAME_PUBLISHED\n"); return 0; } if (!strcmp(mode, "find")) { /* * Motion + content scan of the first 64 MB of BAR1 (the only * window the vfio mdev mmap accepts). Sample 256 B of every * 4 KB page over `passes` passes `interval_ms` apart; score * pages by total differing bytes (motion) and by nonzero * density (content). Rendering can be slower than one frame * per second, so the interval defaults to 3 s and is * configurable: find [passes] [interval_ms] */ int passes = argc > 2 ? atoi(argv[2]) : 3; int interval_ms = argc > 3 ? atoi(argv[3]) : 3000; size_t scan_size = map_size > CHUNK_MAX ? CHUNK_MAX : map_size; size_t pages = scan_size >> 12; unsigned char *s0, *s1, *tmp, *w; uint32_t *motion, *nz; size_t p, wlen; int top, pass; if (passes < 2) passes = 2; w = map_vram(vram_fd, scan_size, &wlen); if (!w) { printf("CHUNK_MAP_FAIL\n"); return 1; } printf("SCAN_WINDOW 0x%zx pages=%zu passes=%d interval=%dms\n", wlen, pages, passes, interval_ms); s0 = malloc(pages * 256); s1 = malloc(pages * 256); motion = calloc(pages, sizeof(uint32_t)); nz = calloc(pages, sizeof(uint32_t)); if (!s0 || !s1 || !motion || !nz) return 1; for (p = 0; p < pages; p++) memcpy(s0 + p * 256, w + (p << 12), 256); for (pass = 1; pass < passes; pass++) { struct timespec a, b; uint64_t moved = 0; usleep((useconds_t)interval_ms * 1000); clock_gettime(CLOCK_MONOTONIC, &a); for (p = 0; p < pages; p++) memcpy(s1 + p * 256, w + (p << 12), 256); clock_gettime(CLOCK_MONOTONIC, &b); for (p = 0; p < pages; p++) { unsigned char *x = s0 + p * 256; unsigned char *y = s1 + p * 256; uint32_t d = 0; int j; for (j = 0; j < 256; j++) if (x[j] != y[j]) d++; motion[p] += d; moved += d; } printf("PASS %d moved_bytes=%llu dt=%.0fms\n", pass, (unsigned long long)moved, t_ms(&a, &b)); tmp = s0; s0 = s1; s1 = tmp; } /* nonzero density of the last sample */ for (p = 0; p < pages; p++) { unsigned char *x = s0 + p * 256; uint32_t c = 0; int j; for (j = 0; j < 256; j++) if (x[j]) c++; nz[p] = c; } report_cluster(nz, pages, 0, "FIND"); /* per-16MB summary */ for (i = 0; i + 4096 <= pages; i += 4096) { uint64_t s = 0, c = 0; size_t j; for (j = i; j < i + 4096; j++) { s += motion[j]; c += nz[j]; } if (s || c) printf("BLOCK off=0x%zx moving_bytes=%llu nonzero_bytes=%llu\n", i << 12, (unsigned long long)s, (unsigned long long)c); } for (top = 0; top < 16; top++) { size_t best = 0; uint32_t bs = 0; for (p = 0; p < pages; p++) if (motion[p] > bs) { bs = motion[p]; best = p; } if (!bs) break; printf("HOT off=0x%zx score=%u\n", best << 12, bs); motion[best] = 0; } for (top = 0; top < 16; top++) { size_t best = 0; uint32_t bs = 0; for (p = 0; p < pages; p++) if (nz[p] > bs) { bs = nz[p]; best = p; } if (!bs) break; printf("CONTENT off=0x%zx nonzero=%u\n", best << 12, bs); nz[best] = 0; } printf("FIND_DONE\n"); return 0; } if (!strcmp(mode, "probe")) { /* * Ground truth on what BAR1 access actually works: try mmap * windows at several offsets/sizes and pread at several * offsets, report each result. Read-only. */ struct { unsigned long long off, len; } mm[] = { { 0, 64UL << 20 }, { 0, 256UL << 20 }, { 0, 4UL << 20 }, { 4UL << 20, 4UL << 20 }, { 64UL << 20, 64UL << 20 }, { 128UL << 20, 4UL << 20 }, { 192UL << 20, 4UL << 20 }, { 60UL << 20, 8UL << 20 }, }; unsigned long long po[] = { 0, 0x100000, 4UL << 20, 60UL << 20, 64UL << 20, 128UL << 20, 252UL << 20, }; unsigned char buf[64]; size_t k; for (k = 0; k < sizeof(mm) / sizeof(mm[0]); k++) { unsigned char *p; if (mm[k].off + mm[k].len > map_size) { printf("MMAP off=0x%llx len=0x%llx SKIP(>bar)\n", mm[k].off, mm[k].len); continue; } errno = 0; p = mmap(NULL, (size_t)mm[k].len, PROT_READ, MAP_SHARED, vram_fd, (off_t)mm[k].off); if (p == MAP_FAILED) printf("MMAP off=0x%llx len=0x%llx FAIL errno=%d\n", mm[k].off, mm[k].len, errno); else { printf("MMAP off=0x%llx len=0x%llx OK first=%02x%02x%02x%02x\n", mm[k].off, mm[k].len, p[0], p[1], p[2], p[3]); munmap(p, (size_t)mm[k].len); } } for (k = 0; k < sizeof(po) / sizeof(po[0]); k++) { ssize_t r; errno = 0; r = pread(vram_fd, buf, 16, (off_t)po[k]); if (r != 16) printf("PREAD off=0x%llx FAIL r=%zd errno=%d\n", po[k], r, errno); else printf("PREAD off=0x%llx OK %02x%02x%02x%02x\n", po[k], buf[0], buf[1], buf[2], buf[3]); } printf("PROBE_DONE\n"); return 0; } if (!strcmp(mode, "findp")) { /* * Same motion + content scan as `find`, but via pread so the * whole 256 MB BAR is reachable (mmap stops at 64 MB). * findp [start_off] [size] [passes] [interval_ms] */ unsigned long long start = argc > 2 ? strtoull(argv[2], NULL, 0) : 0; unsigned long long size = argc > 3 ? strtoull(argv[3], NULL, 0) : map_size - start; int passes = argc > 4 ? atoi(argv[4]) : 3; int interval_ms = argc > 5 ? atoi(argv[5]) : 3000; size_t pages = size >> 12; unsigned char *s0, *s1, *tmp; uint32_t *motion, *nz; size_t p; int top, pass; if (passes < 2) passes = 2; if (start + size > map_size) { printf("RANGE_FAIL start=0x%llx size=0x%llx bar=0x%zx\n", start, size, map_size); return 1; } printf("SCANP start=0x%llx size=0x%llx pages=%zu passes=%d interval=%dms\n", start, size, pages, passes, interval_ms); s0 = malloc(pages * 256); s1 = malloc(pages * 256); motion = calloc(pages, sizeof(uint32_t)); nz = calloc(pages, sizeof(uint32_t)); if (!s0 || !s1 || !motion || !nz) return 1; for (pass = 0; pass < passes; pass++) { struct timespec a, b; uint64_t moved = 0; if (pass) usleep((useconds_t)interval_ms * 1000); clock_gettime(CLOCK_MONOTONIC, &a); for (p = 0; p < pages; p++) { unsigned char *dst = pass ? s1 : s0; ssize_t r = pread(vram_fd, dst + p * 256, 256, (off_t)(start + (p << 12))); if (r != 256) { printf("PREAD_FAIL off=0x%llx r=%zd errno=%d\n", start + (p << 12), r, errno); return 1; } } clock_gettime(CLOCK_MONOTONIC, &b); if (pass) { for (p = 0; p < pages; p++) { unsigned char *x = s0 + p * 256; unsigned char *y = s1 + p * 256; uint32_t d = 0; int j; for (j = 0; j < 256; j++) if (x[j] != y[j]) d++; motion[p] += d; moved += d; } printf("PASS %d moved_bytes=%llu dt=%.0fms\n", pass, (unsigned long long)moved, t_ms(&a, &b)); tmp = s0; s0 = s1; s1 = tmp; } else { printf("PASS 0 sampled dt=%.0fms\n", t_ms(&a, &b)); } } for (p = 0; p < pages; p++) { unsigned char *x = s0 + p * 256; uint32_t c = 0; int j; for (j = 0; j < 256; j++) if (x[j]) c++; nz[p] = c; } report_cluster(nz, pages, start, "FINDP"); for (i = 0; i + 4096 <= pages; i += 4096) { uint64_t s = 0, c = 0; size_t j; for (j = i; j < i + 4096; j++) { s += motion[j]; c += nz[j]; } if (s || c) printf("BLOCK off=0x%llx moving_bytes=%llu nonzero_bytes=%llu\n", start + ((unsigned long long)i << 12), (unsigned long long)s, (unsigned long long)c); } for (top = 0; top < 16; top++) { size_t best = 0; uint32_t bs = 0; for (p = 0; p < pages; p++) if (motion[p] > bs) { bs = motion[p]; best = p; } if (!bs) break; printf("HOT off=0x%llx score=%u\n", start + ((unsigned long long)best << 12), bs); motion[best] = 0; } for (top = 0; top < 16; top++) { size_t best = 0; uint32_t bs = 0; for (p = 0; p < pages; p++) if (nz[p] > bs) { bs = nz[p]; best = p; } if (!bs) break; printf("CONTENT off=0x%llx nonzero=%u\n", start + ((unsigned long long)best << 12), bs); nz[best] = 0; } printf("FINDP_DONE\n"); return 0; } if (!strcmp(mode, "pub")) { unsigned long long off = argc > 2 ? strtoull(argv[2], NULL, 0) : 0; int frames = argc > 3 ? atoi(argv[3]) : 30; int fps = argc > 4 ? atoi(argv[4]) : 5; int w = argc > 5 ? atoi(argv[5]) : DEF_W; int h = argc > 6 ? atoi(argv[6]) : DEF_H; int pitch = argc > 7 ? atoi(argv[7]) : DEF_PITCH; unsigned long long need = off + (unsigned long long)(h - 1) * pitch + (unsigned long long)w * 4; size_t wlen = 0; unsigned char *wmap; int f, y, x; wmap = map_vram(vram_fd, map_size, &wlen); if (!wmap) { printf("WIN_MAP_FAIL len=0x%zx\n", wlen); return 1; } if (need > wlen) { printf("OFF_RANGE_FAIL need=0x%llx window=0x%zx\n", need, wlen); return 1; } printf("PUB off=0x%llx frames=%d fps=%d %dx%d pitch=%d\n", off, frames, fps, w, h, pitch); hdr->magic = HDR_MAGIC; hdr->version = 1; hdr->width = (uint32_t)w; hdr->height = (uint32_t)h; hdr->stride = (uint32_t)w * 4; hdr->format = 0; for (f = 0; f < frames; f++) { uint64_t t0 = now_ns(); unsigned char *dst = bar + FRAME_OFFSET + sizeof(struct frame_hdr); unsigned char *src = wmap + (size_t)off; for (y = 0; y < h; y++) { unsigned char *s = src + (size_t)y * pitch; unsigned char *d = dst + (size_t)y * w * 4; for (x = 0; x < w; x++) { d[4 * x + 0] = s[4 * x + 2]; d[4 * x + 1] = s[4 * x + 1]; d[4 * x + 2] = s[4 * x + 0]; d[4 * x + 3] = 0xff; } } hdr->timestamp_ns = now_ns(); __sync_synchronize(); *seqp = (uint32_t)(f + 1); __sync_synchronize(); printf("FRAME seq=%d ms=%.1f\n", f + 1, (double)(now_ns() - t0) / 1e6); if (fps > 0) usleep((useconds_t)(1000000 / fps)); } printf("FRAME_DONE frames=%d\n", f); return 0; } if (!strcmp(mode, "pubp")) { unsigned long long off = argc > 2 ? strtoull(argv[2], NULL, 0) : 0; int frames = argc > 3 ? atoi(argv[3]) : 30; int fps = argc > 4 ? atoi(argv[4]) : 5; int w = argc > 5 ? atoi(argv[5]) : DEF_W; int h = argc > 6 ? atoi(argv[6]) : DEF_H; int pitch = argc > 7 ? atoi(argv[7]) : DEF_PITCH; unsigned long long span = (unsigned long long)(h - 1) * pitch + (unsigned long long)w * 4; unsigned long long base = off & ~0xfffULL; size_t len = (size_t)((off - base) + span); unsigned char *buf; int f, y, x; len = (len + 0xfff) & ~0xfffUL; if (off + span > map_size) { printf("OFF_RANGE_FAIL need=0x%llx bar=0x%zx\n", off + span, map_size); return 1; } buf = malloc(len); if (!buf) return 1; printf("PUBP off=0x%llx frames=%d fps=%d %dx%d pitch=%d len=0x%zx\n", off, frames, fps, w, h, pitch, len); hdr->magic = HDR_MAGIC; hdr->version = 1; hdr->width = (uint32_t)w; hdr->height = (uint32_t)h; hdr->stride = (uint32_t)w * 4; hdr->format = 0; for (f = 0; f < frames; f++) { uint64_t t0 = now_ns(); unsigned char *dst = bar + FRAME_OFFSET + sizeof(struct frame_hdr); unsigned char *src; ssize_t r = pread(vram_fd, buf, len, (off_t)base); if (r != (ssize_t)len) { printf("PREAD_FAIL off=0x%llx r=%zd errno=%d\n", base, r, errno); return 1; } src = buf + (size_t)(off - base); for (y = 0; y < h; y++) { unsigned char *s = src + (size_t)y * pitch; unsigned char *d = dst + (size_t)y * w * 4; for (x = 0; x < w; x++) { d[4 * x + 0] = s[4 * x + 2]; d[4 * x + 1] = s[4 * x + 1]; d[4 * x + 2] = s[4 * x + 0]; d[4 * x + 3] = 0xff; } } hdr->timestamp_ns = now_ns(); __sync_synchronize(); *seqp = (uint32_t)(f + 1); __sync_synchronize(); printf("FRAME seq=%d ms=%.1f\n", f + 1, (double)(now_ns() - t0) / 1e6); if (fps > 0) usleep((useconds_t)(1000000 / fps)); } printf("FRAME_DONE frames=%d\n", f); return 0; } printf("USAGE: fbcopy_pub test|find|findp|pub|pubp " " [n] [fps] [w] [h] [pitch]\n"); return 1; }