summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/guest-diag/guest_pin_stream.py3
-rw-r--r--tools/guest-diag/pin_gr_stream.py3
-rwxr-xr-xtools/guest-diag/pin_supervisor.sh9
-rw-r--r--tools/guest-diag/qga_snip.py41
-rw-r--r--tools/guest-diag/sdl_cap.c229
-rw-r--r--tools/guest-diag/xfocus.c69
-rw-r--r--tools/guest-diag/xgrab.c115
-rw-r--r--tools/guest-diag/xresize.c66
-rw-r--r--tools/guest-diag/xtest_move.c126
9 files changed, 658 insertions, 3 deletions
diff --git a/tools/guest-diag/guest_pin_stream.py b/tools/guest-diag/guest_pin_stream.py
index 8768f2f..3db61ba 100644
--- a/tools/guest-diag/guest_pin_stream.py
+++ b/tools/guest-diag/guest_pin_stream.py
@@ -1428,7 +1428,8 @@ def pid_line(tag, pid, start):
def cdt_pid():
prefixes = tuple(p for p in os.environ.get("CLIENT_PREFIXES",
- "cdt,va,victory").split(",") if p)
+ "cdt,va,victory,Xorg,X,glxgears,"
+ "gzdoom").split(",") if p)
# Processes that hold a /dev/nvidia* fd but are not CUDA clients; the
# scanner must never commit to them (a transient nvidia-smi at boot was
# picked once and its exited pagemap read back EOF forever).
diff --git a/tools/guest-diag/pin_gr_stream.py b/tools/guest-diag/pin_gr_stream.py
index ff7e299..4289d37 100644
--- a/tools/guest-diag/pin_gr_stream.py
+++ b/tools/guest-diag/pin_gr_stream.py
@@ -136,7 +136,8 @@ def _start_cmd():
b64 = subprocess.run(["base64", "-w0"], stdin=f,
capture_output=True, text=True,
timeout=10).stdout
- prefixes = os.environ.get("CLIENT_PREFIXES", "cdt,va,victory")
+ prefixes = os.environ.get("CLIENT_PREFIXES",
+ "cdt,va,victory,Xorg,X,glxgears,gzdoom")
vma_every = os.environ.get("STREAM_VMA_EVERY", "1")
# Kill EVERY existing scanner, not just the pid-file one: a retry after
# a partially-failed first attempt (pid file not written yet) used to
diff --git a/tools/guest-diag/pin_supervisor.sh b/tools/guest-diag/pin_supervisor.sh
index b87d5c1..72f588f 100755
--- a/tools/guest-diag/pin_supervisor.sh
+++ b/tools/guest-diag/pin_supervisor.sh
@@ -70,7 +70,14 @@ kill_tool() {
start_pin() {
if tool_alive "$PIN"; then return 0; fi
touch "$PIN_HB" 2>/dev/null || true
- ( setsid nohup env SEMFIX_COUNTER_RETRY=10 \
+ # CLIENT_PREFIXES threads through pin_gr_stream.py into the guest
+ # scanner launch (its _start_cmd). GL clients (glxgears, gzdoom) and
+ # the X server must be covered or the GL client's ring/mappings are
+ # never pinned (SESSION 149: the GR ring scan then finds no candidate
+ # for h=0xbfef0002, va=0x1203cfff0).
+ ( setsid nohup env \
+ CLIENT_PREFIXES="${CLIENT_PREFIXES:-cdt,va,victory,Xorg,X,glxgears,gzdoom}" \
+ SEMFIX_COUNTER_RETRY=10 \
python3 "$PIN" >> "$PIN_LOG" 2>&1 & )
pin_started=$(date +%s)
log "START pin_gr_stream"
diff --git a/tools/guest-diag/qga_snip.py b/tools/guest-diag/qga_snip.py
new file mode 100644
index 0000000..cd3b992
--- /dev/null
+++ b/tools/guest-diag/qga_snip.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# Fetch a byte range from a guest file via the QEMU guest agent.
+# Usage: qga_snip.py <guest_path> <offset> <length> <host_out>
+import base64
+import json
+import socket
+import sys
+
+GUEST, OFF, LEN, HOST = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
+import os
+SOCK = os.environ.get("QGA_SOCK", "/tmp/qemu-guest-agent.sock")
+
+
+def recv_line(s):
+ buf = b""
+ while b"\n" not in buf:
+ c = s.recv(65536)
+ if not c:
+ break
+ buf += c
+ return buf.split(b"\n", 1)[0]
+
+
+def cmd(s, obj):
+ s.sendall((json.dumps(obj) + "\r\n").encode())
+ return json.loads(recv_line(s))
+
+
+s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+s.settimeout(30.0)
+s.connect(SOCK)
+h = cmd(s, {"execute": "guest-file-open",
+ "arguments": {"path": GUEST, "mode": "r"}})["return"]
+cmd(s, {"execute": "guest-file-seek",
+ "arguments": {"handle": h, "offset": OFF, "whence": 0}})
+r = cmd(s, {"execute": "guest-file-read",
+ "arguments": {"handle": h, "count": LEN}})
+data = base64.b64decode(r["return"].get("buf-b64", ""))
+open(HOST, "wb").write(data)
+cmd(s, {"execute": "guest-file-close", "arguments": {"handle": h}})
+print(f"GOT {len(data)} bytes @ {OFF} -> {HOST}")
diff --git a/tools/guest-diag/sdl_cap.c b/tools/guest-diag/sdl_cap.c
new file mode 100644
index 0000000..23c83ab
--- /dev/null
+++ b/tools/guest-diag/sdl_cap.c
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// sdl_cap.c - LD_PRELOAD capture shim: records every frame GZDoom presents
+// through SDL into an append-only file, so the host can harvest the guest's
+// real rendered frames without any X/driver readback path.
+//
+// Hooks:
+// SDL_RenderPresent(SDL_Renderer*) -> SDL_RenderReadPixels(ARGB8888)
+// SDL_UpdateWindowSurface(SDL_Window*) -> SDL_GetWindowSurface pixels
+// Both forward to the real SDL after capturing.
+//
+// File layout (little endian):
+// [0] 8B magic "GZCAPV01" (+ 4088 B reserved zero)
+// [4096] frame records:
+// 4B "GZF2" | u32 w | u32 h | u32 stride | u32 bpp
+// u64 ts_ns (CLOCK_MONOTONIC) | u64 seq | stride*h bytes (top-down)
+// A trailing record with seq = 0xFFFFFFFFFFFFFFFF marks clean stop.
+//
+// Env: GZCAP (path, default /dev/shm/gzframes.bin), GZCAP_MAXF (0=unlimited)
+#define _GNU_SOURCE
+#include <dlfcn.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define ARGB8888 0x16362004u
+
+typedef struct SDL_Renderer SDL_Renderer;
+typedef struct SDL_Window SDL_Window;
+
+typedef struct SDL_Surface {
+ uint32_t flags;
+ void *format;
+ int w, h;
+ int pitch;
+ void *pixels;
+ void *userdata;
+ int locked;
+ void *list_blitmap;
+ struct { int x, y, w, h; } clip_rect;
+ void *map;
+ int refcount;
+} SDL_Surface;
+
+static int (*real_RenderReadPixels)(SDL_Renderer *, const void *, uint32_t,
+ void *, int);
+static int (*real_RenderPresent)(SDL_Renderer *);
+static SDL_Surface *(*real_GetWindowSurface)(SDL_Window *);
+static int (*real_UpdateWindowSurface)(SDL_Window *);
+static int (*real_GetRendererOutputSize)(SDL_Renderer *, int *, int *);
+static void (*real_GL_SwapWindow)(SDL_Window *);
+static int (*real_GL_GetDrawableSize)(SDL_Window *, int *, int *);
+static void (*real_glReadPixels)(int, int, int, int, unsigned, unsigned, void *);
+
+static FILE *cap_fp;
+static unsigned long long cap_count, cap_max = 0;
+static int cap_stop;
+
+__attribute__((constructor)) static void cap_init(void)
+{
+ const char *p = getenv("GZCAP");
+ const char *m = getenv("GZCAP_MAXF");
+ char hdr[4096];
+
+ if (m)
+ cap_max = strtoull(m, NULL, 0);
+ if (!p)
+ p = "/dev/shm/gzframes.bin";
+ real_RenderReadPixels = dlsym(RTLD_NEXT, "SDL_RenderReadPixels");
+ real_RenderPresent = dlsym(RTLD_NEXT, "SDL_RenderPresent");
+ real_GetWindowSurface = dlsym(RTLD_NEXT, "SDL_GetWindowSurface");
+ real_UpdateWindowSurface = dlsym(RTLD_NEXT, "SDL_UpdateWindowSurface");
+ real_GetRendererOutputSize =
+ dlsym(RTLD_NEXT, "SDL_GetRendererOutputSize");
+ real_GL_SwapWindow = dlsym(RTLD_NEXT, "SDL_GL_SwapWindow");
+ real_GL_GetDrawableSize = dlsym(RTLD_NEXT, "SDL_GL_GetDrawableSize");
+ real_glReadPixels = dlsym(RTLD_NEXT, "glReadPixels");
+ if (!real_glReadPixels)
+ real_glReadPixels = dlsym(RTLD_DEFAULT, "glReadPixels");
+ if (!real_glReadPixels) {
+ void *h = dlopen("libGL.so.1", RTLD_NOW | RTLD_GLOBAL);
+
+ if (!h)
+ h = dlopen("libGLESv2.so.2", RTLD_NOW | RTLD_GLOBAL);
+ if (h)
+ real_glReadPixels = dlsym(h, "glReadPixels");
+ }
+ fprintf(stderr, "[gzcap] init path=%s present=%p updwin=%p swap=%p read=%p gros=%p\n",
+ p, (void *)real_RenderPresent, (void *)real_UpdateWindowSurface,
+ (void *)real_GL_SwapWindow, (void *)real_RenderReadPixels,
+ (void *)real_GetRendererOutputSize, (void *)real_glReadPixels);
+ cap_fp = fopen(p, "wb");
+ if (!cap_fp)
+ return;
+ memset(hdr, 0, sizeof(hdr));
+ memcpy(hdr, "GZCAPV01", 8);
+ fwrite(hdr, 1, sizeof(hdr), cap_fp);
+ fflush(cap_fp);
+}
+
+static void cap_record(const void *px, int w, int h, int stride, int bpp)
+{
+ struct timespec ts;
+ uint64_t tns, seq;
+ uint32_t rh[4];
+ char m[4] = { 'G', 'Z', 'F', '2' };
+
+ if (!cap_fp || cap_stop)
+ return;
+ if (cap_max && cap_count >= cap_max) {
+ uint64_t endseq = 0xFFFFFFFFFFFFFFFFull;
+
+ fwrite(m, 1, 4, cap_fp);
+ fwrite(&endseq, 8, 1, cap_fp);
+ fflush(cap_fp);
+ cap_stop = 1;
+ return;
+ }
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ tns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
+ seq = cap_count;
+ rh[0] = (uint32_t)w;
+ rh[1] = (uint32_t)h;
+ rh[2] = (uint32_t)stride;
+ rh[3] = (uint32_t)bpp;
+ fwrite(m, 1, 4, cap_fp);
+ fwrite(rh, 4, 4, cap_fp);
+ fwrite(&tns, 8, 1, cap_fp);
+ fwrite(&seq, 8, 1, cap_fp);
+ fwrite(px, 1, (size_t)stride * h, cap_fp);
+ fflush(cap_fp);
+ cap_count++;
+ if ((cap_count & 31) == 0)
+ fprintf(stderr, "[gzcap] frames=%llu\n", cap_count);
+}
+
+int SDL_RenderPresent(SDL_Renderer *r)
+{
+ static int hit;
+
+ if (!hit++)
+ fprintf(stderr, "[gzcap] hook RenderPresent\n");
+ if (cap_fp && !cap_stop && real_RenderReadPixels &&
+ real_GetRendererOutputSize) {
+ int w = 0, h = 0;
+
+ if (real_GetRendererOutputSize(r, &w, &h) == 0 && w > 0 && h > 0 &&
+ w <= 4096 && h <= 4096) {
+ static unsigned char *buf;
+ static size_t bufsz;
+ size_t need = (size_t)w * 4 * h;
+
+ if (bufsz < need) {
+ free(buf);
+ buf = malloc(need);
+ bufsz = buf ? need : 0;
+ }
+ if (buf && real_RenderReadPixels(r, NULL, ARGB8888, buf,
+ w * 4) == 0)
+ cap_record(buf, w, h, w * 4, 32);
+ }
+ }
+ if (real_RenderPresent)
+ return real_RenderPresent(r);
+ return 0;
+}
+
+int SDL_UpdateWindowSurface(SDL_Window *win)
+{
+ static int hit;
+
+ if (!hit++)
+ fprintf(stderr, "[gzcap] hook UpdateWindowSurface\n");
+ if (cap_fp && !cap_stop && real_GetWindowSurface) {
+ SDL_Surface *s = real_GetWindowSurface(win);
+
+ if (s && s->pixels && s->w > 0 && s->h > 0 && s->pitch > 0 &&
+ s->w <= 4096 && s->h <= 4096)
+ cap_record(s->pixels, s->w, s->h, s->pitch, 32);
+ }
+ if (real_UpdateWindowSurface)
+ return real_UpdateWindowSurface(win);
+ return 0;
+}
+
+void SDL_GL_SwapWindow(SDL_Window *win)
+{
+ static int hit;
+ static unsigned char *gbuf;
+ static size_t gbufsz;
+
+ if (!hit++)
+ fprintf(stderr, "[gzcap] hook GLSwapWindow\n");
+ if (cap_fp && !cap_stop && real_glReadPixels && real_GL_GetDrawableSize) {
+ int w = 0, h = 0;
+
+ if (real_GL_GetDrawableSize(win, &w, &h) == 0 && w > 0 && h > 0 &&
+ w <= 4096 && h <= 4096) {
+ size_t need = (size_t)w * 4 * h;
+
+ if (gbufsz < need) {
+ free(gbuf);
+ gbuf = malloc(need);
+ gbufsz = gbuf ? need : 0;
+ }
+ if (gbuf) {
+ real_glReadPixels(0, 0, w, h, 0x1908, 0x1401, gbuf);
+ /* GL rows are bottom-up: flip into a scratch record */
+ {
+ unsigned char *tmp = malloc(need);
+ int y;
+
+ if (tmp) {
+ for (y = 0; y < h; y++)
+ memcpy(tmp + (size_t)y * w * 4,
+ gbuf + (size_t)(h - 1 - y) * w * 4,
+ (size_t)w * 4);
+ cap_record(tmp, w, h, w * 4, 32);
+ free(tmp);
+ }
+ }
+ }
+ }
+ }
+ if (real_GL_SwapWindow)
+ real_GL_SwapWindow(win);
+}
diff --git a/tools/guest-diag/xfocus.c b/tools/guest-diag/xfocus.c
new file mode 100644
index 0000000..e0ec05d
--- /dev/null
+++ b/tools/guest-diag/xfocus.c
@@ -0,0 +1,69 @@
+// xfocus.c - print the current X input focus and the gzdoom window, and try
+// to focus the gzdoom window; then report again. Diagnostic for WM-less X.
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <stdio.h>
+#include <string.h>
+
+static Window find_named(Display *d, Window root, const char *sub,
+ char *title, size_t tl)
+{
+ Window r, parent, *kids = NULL;
+ unsigned int nk = 0;
+
+ title[0] = 0;
+ if (!XQueryTree(d, root, &r, &parent, &kids, &nk))
+ return 0;
+ for (unsigned int i = 0; i < nk; i++) {
+ char *name = NULL;
+
+ if (XFetchName(d, kids[i], &name) && name) {
+ if (strstr(name, sub)) {
+ snprintf(title, tl, "%s", name);
+ Window w = kids[i];
+
+ XFree(name);
+ if (kids)
+ XFree(kids);
+ return w;
+ }
+ XFree(name);
+ }
+ }
+ if (kids)
+ XFree(kids);
+ return 0;
+}
+
+int main(void)
+{
+ Display *d = XOpenDisplay(NULL);
+ Window root, focus, w, croot;
+ int rx, ry, wx, wy;
+ unsigned int mask;
+ char title[256];
+
+ if (!d) {
+ printf("no display\n");
+ return 1;
+ }
+ root = DefaultRootWindow(d);
+ w = find_named(d, root, "Freedoom", title, sizeof(title));
+ XGetInputFocus(d, &focus, &rx);
+ printf("gzdoom window: 0x%lx '%s'\n", w, title);
+ printf("input focus: 0x%lx\n", focus);
+ printf("pointer window: ");
+ if (XQueryPointer(d, root, &croot, &rx /*reuse*/, &wx, &wy, &rx, &ry,
+ &mask))
+ printf("0x%lx at %d,%d mask=0x%x\n", rx, wx, wy, mask);
+ else
+ printf("query failed\n");
+ if (w) {
+ XSetInputFocus(d, w, RevertToParent, CurrentTime);
+ XFlush(d);
+ XGetInputFocus(d, &focus, &rx);
+ printf("after set: 0x%lx\n", focus);
+ }
+ XCloseDisplay(d);
+ return 0;
+}
diff --git a/tools/guest-diag/xgrab.c b/tools/guest-diag/xgrab.c
new file mode 100644
index 0000000..eb605d6
--- /dev/null
+++ b/tools/guest-diag/xgrab.c
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// xgrab.c - capture the X screen (root window) into an append-only frame file.
+// Pure client-side XGetImage on a software X server (Xvfb): no GL, no driver,
+// no present-path hooks. Same record format as sdl_cap.c:
+// [0] 8B "GZCAPV01" + zero pad to 4096
+// [4096..] records: 4B "GZF2" | u32 w | u32 h | u32 stride | u32 bpp |
+// u64 ts_ns | u64 seq | stride*h bytes (top-down BGRA)
+// Trailing record seq=0xFFFFFFFFFFFFFFFF on clean stop.
+// Usage: xgrab <display> <out> <maxframes|0> <pace_us|0>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+int main(int argc, char **argv)
+{
+ const char *dpy = argc > 1 ? argv[1] : ":1";
+ const char *out = argc > 2 ? argv[2] : "/dev/shm/gzframes.bin";
+ long maxf = argc > 3 ? atol(argv[3]) : 0;
+ long pace = argc > 4 ? atol(argv[4]) : 0;
+ int r_x = argc > 5 ? atoi(argv[5]) : -1;
+ int r_y = argc > 6 ? atoi(argv[6]) : 0;
+ int r_w = argc > 7 ? atoi(argv[7]) : 0;
+ int r_h = argc > 8 ? atoi(argv[8]) : 0;
+ Display *d;
+ Window root;
+ XWindowAttributes wa;
+ FILE *f;
+ char hdr[4096];
+ uint64_t seq = 0;
+
+ d = XOpenDisplay(dpy);
+ if (!d) {
+ fprintf(stderr, "xgrab: cannot open display %s\n", dpy);
+ return 1;
+ }
+ root = DefaultRootWindow(d);
+ if (!XGetWindowAttributes(d, root, &wa)) {
+ fprintf(stderr, "xgrab: no root attrs\n");
+ return 1;
+ }
+ if (r_x < 0 || r_w <= 0 || r_h <= 0 || r_x + r_w > wa.width ||
+ r_y + r_h > wa.height) {
+ r_x = 0; r_y = 0; r_w = wa.width; r_h = wa.height;
+ }
+ fprintf(stderr, "xgrab: root %dx%d depth=%d region %d,%d %dx%d\n",
+ wa.width, wa.height, wa.depth, r_x, r_y, r_w, r_h);
+ f = fopen(out, "wb");
+ if (!f) {
+ perror("xgrab: open out");
+ return 1;
+ }
+ memset(hdr, 0, sizeof(hdr));
+ memcpy(hdr, "GZCAPV01", 8);
+ fwrite(hdr, 1, sizeof(hdr), f);
+ fflush(f);
+
+ while (!maxf || (long)seq < maxf) {
+ XImage *im = XGetImage(d, root, r_x, r_y, r_w, r_h, AllPlanes,
+ ZPixmap);
+ struct timespec ts;
+ uint64_t tns;
+ uint32_t rh[4];
+ char m[4] = { 'G', 'Z', 'F', '2' };
+ int y;
+
+ if (!im) {
+ usleep(10000);
+ continue;
+ }
+ if (im->bits_per_pixel != 32) {
+ fprintf(stderr, "xgrab: unexpected bpp %d\n",
+ im->bits_per_pixel);
+ XDestroyImage(im);
+ break;
+ }
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ tns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
+ rh[0] = (uint32_t)r_w;
+ rh[1] = (uint32_t)r_h;
+ rh[2] = (uint32_t)r_w * 4;
+ rh[3] = 32;
+ fwrite(m, 1, 4, f);
+ fwrite(rh, 4, 4, f);
+ fwrite(&tns, 8, 1, f);
+ fwrite(&seq, 8, 1, f);
+ for (y = 0; y < r_h; y++)
+ fwrite(im->data + (size_t)y * im->bytes_per_line, 1,
+ (size_t)r_w * 4, f);
+ fflush(f);
+ seq++;
+ XDestroyImage(im);
+ if (pace)
+ usleep(pace);
+ if ((seq & 63) == 0)
+ fprintf(stderr, "xgrab: frames=%llu\n",
+ (unsigned long long)seq);
+ }
+ {
+ char m[4] = { 'G', 'Z', 'F', '2' };
+ uint64_t end = 0xFFFFFFFFFFFFFFFFull;
+
+ fwrite(m, 1, 4, f);
+ fwrite(&end, 8, 1, f);
+ fflush(f);
+ }
+ fclose(f);
+ XCloseDisplay(d);
+ fprintf(stderr, "xgrab: done frames=%llu\n", (unsigned long long)seq);
+ return 0;
+}
diff --git a/tools/guest-diag/xresize.c b/tools/guest-diag/xresize.c
new file mode 100644
index 0000000..877f0e9
--- /dev/null
+++ b/tools/guest-diag/xresize.c
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// xresize.c - find a window by name substring and resize/move it (Xvfb has
+// no WM, so a client-side XMoveResizeWindow is the only way to force the
+// game window onto the screen).
+// Usage: xresize <display> <name-substr> <w> <h> <x> <y>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+static Window find_named(Display *d, Window root, const char *sub)
+{
+ Window r, parent, *kids = NULL;
+ unsigned int nkids = 0;
+
+ if (!XQueryTree(d, root, &r, &parent, &kids, &nkids))
+ return 0;
+ for (unsigned int i = 0; i < nkids; i++) {
+ char *name = NULL;
+
+ if (XFetchName(d, kids[i], &name) && name) {
+ int hit = strstr(name, sub) != NULL;
+
+ XFree(name);
+ if (hit) {
+ Window w = kids[i];
+
+ if (kids)
+ XFree(kids);
+ return w;
+ }
+ }
+ }
+ if (kids)
+ XFree(kids);
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ Display *d;
+ Window w;
+
+ if (argc < 6) {
+ fprintf(stderr, "usage: xresize <display> <name> <w> <h> <x> <y>\n");
+ return 2;
+ }
+ d = XOpenDisplay(argv[1]);
+ if (!d) {
+ fprintf(stderr, "no display\n");
+ return 1;
+ }
+ w = find_named(d, DefaultRootWindow(d), argv[2]);
+ if (!w) {
+ fprintf(stderr, "window '%s' not found\n", argv[2]);
+ return 1;
+ }
+ XMoveResizeWindow(d, w, atoi(argv[5]), atoi(argv[6]), atoi(argv[3]),
+ atoi(argv[4]));
+ XFlush(d);
+ printf("resized 0x%lx to %sx%s at %s,%s\n", w, argv[3], argv[4],
+ argv[5], argv[6]);
+ XCloseDisplay(d);
+ return 0;
+}
diff --git a/tools/guest-diag/xtest_move.c b/tools/guest-diag/xtest_move.c
new file mode 100644
index 0000000..8331487
--- /dev/null
+++ b/tools/guest-diag/xtest_move.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// xtest_move.c - drive the game with XSendEvent-synthetic keys (SDL2 on this
+// Xorg ignores XTEST-faked keys; it accepts send_event KeyPress/KeyRelease).
+// Finds the game window by name, then holds forward+fire and alternates
+// turning left/right so the captured footage has constant motion.
+// Usage: xtest_move <display> <seconds>
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+#include <X11/Xutil.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static Display *d;
+static Window game;
+
+static Window find_named(const char *sub)
+{
+ Window root = DefaultRootWindow(d), r, parent, *kids = NULL;
+ unsigned int nk = 0;
+
+ if (!XQueryTree(d, root, &r, &parent, &kids, &nk))
+ return 0;
+ for (unsigned int i = 0; i < nk; i++) {
+ char *name = NULL;
+
+ if (XFetchName(d, kids[i], &name) && name) {
+ if (strstr(name, sub)) {
+ Window w = kids[i];
+
+ XFree(name);
+ if (kids)
+ XFree(kids);
+ return w;
+ }
+ XFree(name);
+ }
+ }
+ if (kids)
+ XFree(kids);
+ return 0;
+}
+
+static void send_key(KeySym ks, int down)
+{
+ XEvent e;
+ Window root = DefaultRootWindow(d);
+ int rx, ry, wx, wy;
+ unsigned int mask;
+ Window cr, pr;
+
+ if (!game)
+ return;
+ memset(&e, 0, sizeof(e));
+ e.xkey.type = down ? KeyPress : KeyRelease;
+ e.xkey.display = d;
+ e.xkey.window = game;
+ e.xkey.root = root;
+ e.xkey.subwindow = None;
+ e.xkey.time = CurrentTime;
+ XQueryPointer(d, root, &cr, &pr, &rx, &ry, &wx, &wy, &mask);
+ e.xkey.x = wx;
+ e.xkey.y = wy;
+ e.xkey.x_root = rx;
+ e.xkey.y_root = ry;
+ e.xkey.same_screen = True;
+ e.xkey.keycode = XKeysymToKeycode(d, ks);
+ e.xkey.state = 0;
+ XSendEvent(d, game, True, down ? KeyPressMask : KeyReleaseMask, &e);
+ XFlush(d);
+}
+
+int main(int argc, char **argv)
+{
+ int secs = argc > 2 ? atoi(argv[2]) : 12;
+ int t;
+
+ d = XOpenDisplay(argc > 1 ? argv[1] : ":1");
+ if (!d) {
+ fprintf(stderr, "xtest_move: no display\n");
+ return 1;
+ }
+ game = find_named("Freedoom");
+ if (!game)
+ game = find_named("Outer Prison");
+ if (!game)
+ game = find_named("Doom");
+ fprintf(stderr, "xtest_move: game window 0x%lx\n", game);
+ if (!game)
+ return 1;
+ XSetInputFocus(d, game, RevertToParent, CurrentTime);
+ XFlush(d);
+
+ /* Hold forward only (no fire: fire pulls enemies and the player can
+ * die mid-capture; with +noclip from the command line this is a smooth
+ * fly-through). */
+ send_key(XK_Up, 1);
+
+ for (t = 0; t < secs * 10; t++) {
+ static int dir; /* 0 = left held, 1 = right held */
+
+ XSetInputFocus(d, game, RevertToParent, CurrentTime); /* keep focus */
+ /* Re-assert the held forward key: SDL's held state drops between
+ * synthetic events, so forward motion stalls unless re-sent. */
+ if ((t % 4) == 0)
+ send_key(XK_Up, 1);
+ if ((t % 9) == 8) {
+ KeySym off = dir ? XK_Right : XK_Left;
+ KeySym on = dir ? XK_Left : XK_Right;
+
+ send_key(off, 0);
+ usleep(30000);
+ send_key(on, 1);
+ dir = !dir;
+ }
+ XFlush(d);
+ usleep(100000);
+ }
+ send_key(XK_Left, 0);
+ send_key(XK_Right, 0);
+ send_key(XK_Up, 0);
+ XCloseDisplay(d);
+ fprintf(stderr, "xtest_move: done\n");
+ return 0;
+}