From 725898e44666b4def396f7f3c5e50712fd37aa33 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Thu, 12 Nov 2026 20:01:04 -0500 Subject: vgsp: stop fabricating 0xFFFF into the nvPush error notifier; add r1780 GL ops tooling The uncommitted keep-alive change wrote 0xFFFF into slot 0's status field (u16 at notifier+0x0e) on the X DDX / GLX kick-idle channel families. Ground truth (570.144 reference sources + stock nvidia_drv.so disassembly): slot 0 of an nvPush notifier block is the ERROR notifier; nvPushCheckChannelError reads that status and 0xFFFF aborts every nvPushWaitForNotifier/idle wait as a channel error. There is no pb_kickIfIdle in 570.144 - the DDX DMA-idle test reads the progress-tracker semaphore word, not this slot. Leave slot 0 untouched; keep the 0x8000->0 completion ack for the remaining slots and the r864 0xFFFF/0x8000 completion on the RC-watchdog/beef families. Correct the CONT.408/409 comments accordingly. New host/guest tooling: gpa.py (QMP gva2gpa), tick_gen.py (guest-RM timer BO tick), guest_mode.sh (shared nvClip modeNum reader), gl_verify.sh (X/GLX bring-up with ScreenInit-lottery retries), push_gl_tools.sh, the gzdoom_pbuf_run.sh guest recipe and the test_tri SDL2 harness validation program; ignore tools/guest-diag/build/. --- tools/guest-diag/gl_verify.sh | 30 ++++ tools/guest-diag/gpa.py | 82 +++++++++++ tools/guest-diag/guest_mode.sh | 285 ++++++++++++++++++++++++++++++++++++ tools/guest-diag/gzdoom_pbuf_run.sh | 94 ++++++++++++ tools/guest-diag/push_gl_tools.sh | 74 ++++++++++ tools/guest-diag/test_tri.c | 112 ++++++++++++++ tools/guest-diag/tick_gen.py | 97 ++++++++++++ 7 files changed, 774 insertions(+) create mode 100644 tools/guest-diag/gl_verify.sh create mode 100755 tools/guest-diag/gpa.py create mode 100755 tools/guest-diag/guest_mode.sh create mode 100755 tools/guest-diag/gzdoom_pbuf_run.sh create mode 100755 tools/guest-diag/push_gl_tools.sh create mode 100644 tools/guest-diag/test_tri.c create mode 100755 tools/guest-diag/tick_gen.py (limited to 'tools') diff --git a/tools/guest-diag/gl_verify.sh b/tools/guest-diag/gl_verify.sh new file mode 100644 index 0000000..edfaab2 --- /dev/null +++ b/tools/guest-diag/gl_verify.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Open-IOV contributors +# gl_verify.sh - host-side: (re)start X in the running guest, verify xdpyinfo + +# glxinfo -B + shared nvClip modeNum. Prints a compact summary. +# +# Usage: bash tools/guest-diag/gl_verify.sh [--no-restart] +# --no-restart : check the current X without killing/restarting it. +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +VMEXEC=(sudo python3 "$HERE/../../scripts/vgpu_vm.py") +RESTART=1 +[ "${1:-}" = "--no-restart" ] && RESTART=0 + +gx() { "${VMEXEC[@]}" exec --timeout "${1:-45}" "$2" 2>&1 | tail -n "${3:-40}"; } + +if [ "$RESTART" = "1" ]; then + # The DDX ScreenInit is a per-start lottery ("GPU exception"): retry up to 3x. + for try in 1 2 3; do + gx 30 "pkill -f '[X]org :0' 2>/dev/null; sleep 1; systemctl reset-failed xorg0 2>/dev/null; systemd-run --unit=xorg0 --collect --setenv=HOME=/root /usr/bin/Xorg :0 -config /etc/X11/xorg.conf >/dev/null 2>&1; echo XSTART-ISSUED" >/dev/null + sleep 8 + ok=$(gx 20 "timeout 12 env DISPLAY=:0 xdpyinfo >/dev/null 2>&1 && echo XDPYINFO-OK || echo X-DEAD" 3 | tail -1) + echo "try $try: $ok" + [ "$ok" = "XDPYINFO-OK" ] && break + sleep 4 + done +fi + +gx 40 "pgrep -c Xorg || echo NO_XORG; timeout 15 env DISPLAY=:0 xdpyinfo >/dev/null 2>&1 && echo XDPYINFO-OK || echo X-HUNG; timeout 20 env DISPLAY=:0 glxinfo -B 2>&1 | grep -E 'direct rendering|OpenGL (renderer|version|core profile version)'; timeout 20 env DISPLAY=:0 glxinfo -B >/dev/null 2>&1 && echo GLXINFO-OK || echo GLX-HUNG; bash /tmp/guest_mode.sh 2>/dev/null || echo NO_MODENUM_SCRIPT" 60 diff --git a/tools/guest-diag/gpa.py b/tools/guest-diag/gpa.py new file mode 100755 index 0000000..61ba27b --- /dev/null +++ b/tools/guest-diag/gpa.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Open-IOV contributors +# gpa.py - translate a guest virtual address to a guest physical address via +# the QEMU monitor's HMP `gva2gpa`, reusing the QMP client in semfix_watch.py. +# +# Usage: sudo python3 tools/guest-diag/gpa.py [--offset] +# guest VA, e.g. 0xffffc90000000000 or 0x7f... +# --offset also print the /dev/shm/vgpu-guest-ram file offset +# (gpa - 0x80000000) +# +# The monitor's `gva2gpa` translates with the selected CPU's CR3; while a CPU +# runs user code under PTI the kernel half is hidden, so retry across CPUs +# exactly like semfix_watch.Qmp.gva2gpa does. On failure the raw monitor text +# is printed so the operator can see the reason (unmapped VA, no guest, ...). +import argparse +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +from semfix_watch import Qmp # noqa: E402 + +QMP_SOCK = os.environ.get("QMP_SOCK", "/tmp/qemu-qmp.sock") +RAM_BASE = 0x80000000 + + +def parse_va(text): + text = text.strip() + try: + return int(text, 0) + except ValueError: + return int(text, 16) + + +def translate(q, va): + """Return (gpa, raw_hmp_text) or (None, last_raw_text).""" + last = "" + for cpu in q._cpu_order(): + q._set_cpu(cpu) + out = q.hmp("gva2gpa 0x%x" % va) + last = out.strip() + m = re.search(r"gpa:\s*0x([0-9a-fA-F]+)", out) + if m: + return int(m.group(1), 16), last + return None, last + + +def main(): + ap = argparse.ArgumentParser(add_help=True) + ap.add_argument("gva", help="guest virtual address (hex, 0x prefix optional)") + ap.add_argument("--offset", action="store_true", + help="also print gpa - 0x80000000 (guest RAM file offset)") + ap.add_argument("--qmp", default=QMP_SOCK, help="QMP socket path") + args = ap.parse_args() + + va = parse_va(args.gva) + if not os.path.exists(args.qmp): + print("gpa: QMP socket not found: %s" % args.qmp, file=sys.stderr) + return 2 + try: + q = Qmp(args.qmp) + except Exception as exc: # noqa: BLE001 - diagnostic surface + print("gpa: cannot connect to %s: %s" % (args.qmp, exc), file=sys.stderr) + return 2 + + gpa, text = translate(q, va) + if gpa is None: + print("gpa: gva2gpa 0x%x failed" % va, file=sys.stderr) + if text: + print(text, file=sys.stderr) + return 1 + + print("GPA 0x%x" % gpa) + if args.offset: + print("OFFSET 0x%x" % (gpa - RAM_BASE)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/guest-diag/guest_mode.sh b/tools/guest-diag/guest_mode.sh new file mode 100755 index 0000000..2460131 --- /dev/null +++ b/tools/guest-diag/guest_mode.sh @@ -0,0 +1,285 @@ +#!/bin/bash +# guest_mode.sh - read the NVIDIA X driver's nvClip screens[0].modeNum. +# +# The X driver (nvidia_drv.so) creates the nvClip shared area through its +# anonymous-file helper: memfd_create first (name derived from +# "/.nvidia_drv.XXXXXX"), then O_TMPFILE, then mkstemp+unlink. 570.144 size +# is 0x245000 bytes. The fd is handed to GLX clients over X SEND_FDS. +# Layout (570.144, verified against nvidia_drv.so 570.144): +# +0x00 u32 grabbed +# +0x04 u32 token (unique X-server token, sanity check) +# +0x08 u32 screens[0].modeNum <-- this script +# screens[] stride 0x84 (132) bytes +# +# modeNum parity is the GL client's __GL_DEV_MODESWITCH_TEST_COMPLETE bit: +# even = device enabled / mode switch complete +# odd = mode switch in progress / device disabled +# GZDoom's GL init must observe an EVEN value; odd makes __glNVStateInit +# clear initBits, buffer objects get no GPU memory, and GZDoom crashes at +# 0x20 in FFlatVertexBuffer::Copy. +# +# STDOUT contract (parse this, nothing else): +# MODENUM +# NO_XORG +# +# Exit codes: +# 0 MODENUM printed +# 3 NO_XORG (no Xorg process with an nvClip fd) +# 4 permission / unexpected error (NO_XORG still printed) +# +# Usage: +# sudo ./guest_mode.sh # one shot, display from $DISPLAY (default :0) +# sudo ./guest_mode.sh --wait 15 # poll up to 15 s for Xorg's nvClip fd +# sudo ./guest_mode.sh --pid 1515 # force one Xorg pid (testing/forensics) +# sudo ./guest_mode.sh --interval 50 # poll interval ms (with --wait) +# sudo ./guest_mode.sh --debug # candidate details on stderr +# +# Requires: bash, python3 (stdlib only), root (or a user that can read +# /proc//fd; Xorg normally runs as root in the guest). + +set -u + +if ! command -v python3 >/dev/null 2>&1; then + echo "guest_mode.sh: python3 not found" >&2 + echo "NO_XORG" + exit 4 +fi + +exec python3 - "$@" <<'PY' +import argparse +import os +import re +import struct +import sys +import time + +NVCLIP_TARGET_RE = re.compile(r'(?:^|/)\.nvidia_drv\.[^/]*$|memfd:.*nvidia_drv') +# 570.144 initial size is 0x245000. ExtendSharedArea() ftruncates upward +# when a second channel block is needed, so accept a range, prefer exact. +NVCLIP_SIZE_EXACT = 0x245000 +NVCLIP_SIZE_MIN = 0x80000 +NVCLIP_SIZE_MAX = 0x800000 +MODE_OFF = 0x8 +TOKEN_OFF = 0x4 + + +def eprint(msg): + print(msg, file=sys.stderr) + + +def parse_args(): + p = argparse.ArgumentParser(add_help=True) + p.add_argument('--pid', type=int, default=0, + help='only inspect this pid (bypasses the Xorg comm check)') + p.add_argument('--display', default=os.environ.get('DISPLAY', ':0'), + help='X display to prefer (default $DISPLAY or :0)') + p.add_argument('--wait', type=float, default=0.0, + help='poll up to SECONDS for the fd (default 0 = one shot)') + p.add_argument('--interval', type=float, default=200.0, + help='poll interval in milliseconds (default 200)') + p.add_argument('--debug', action='store_true', + help='print candidate details to stderr') + return p.parse_args() + + +def read_text(path): + with open(path, 'rb') as f: + return f.read() + + +def proc_args(pid): + try: + raw = read_text('/proc/%d/cmdline' % pid).split(b'\0') + except OSError: + return [] + return [x.decode('utf-8', 'replace') for x in raw if x] + + +def proc_comm(pid): + try: + return read_text('/proc/%d/comm' % pid).decode('utf-8', 'replace').strip() + except OSError: + return '' + + +def proc_starttime(pid): + # /proc/pid/stat field 22 (1-based). Fields after the comm field: + # rest[0] is field 3 (state), so starttime is rest[19]. + try: + data = read_text('/proc/%d/stat' % pid).decode('utf-8', 'replace') + rest = data.rsplit(')', 1)[1].split() + return int(rest[19]) + except (OSError, IndexError, ValueError): + return 0 + + +def display_number(display): + # ":0", ":0.0", "host:0.0" -> 0 + try: + tail = display.rsplit(':', 1)[1] + return int(tail.split('.', 1)[0]) + except (IndexError, ValueError): + return None + + +def is_xorg(pid): + comm = proc_comm(pid) + if comm in ('Xorg', 'X'): + return True + args = proc_args(pid) + if not args: + return False + base = os.path.basename(args[0]) + return base in ('Xorg', 'X') + + +def display_matches(pid, disp): + if disp is None: + return False + needle = ':%d' % disp + for arg in proc_args(pid): + if arg == needle or arg.startswith(needle + '.'): + return True + return False + + +def candidate_fds(pid): + """Yield (fd_name, target, size) for nvClip-like fds of pid.""" + fd_dir = '/proc/%d/fd' % pid + try: + names = os.listdir(fd_dir) + except PermissionError: + raise + except OSError: + return + for name in names: + if not name.isdigit(): + continue + path = os.path.join(fd_dir, name) + try: + target = os.readlink(path) + st = os.stat(path) + except PermissionError: + raise + except OSError: + continue + if not NVCLIP_TARGET_RE.search(target): + continue + if not (NVCLIP_SIZE_MIN <= st.st_size <= NVCLIP_SIZE_MAX): + continue + yield name, target, st.st_size + + +def read_candidate(pid, fd_name, target, size): + path = '/proc/%d/fd/%s' % (pid, fd_name) + try: + fd = os.open(path, os.O_RDONLY) + except OSError as exc: + return None, str(exc) + try: + blob = os.pread(fd, 12, 0) + except OSError as exc: + return None, str(exc) + finally: + os.close(fd) + if len(blob) < 12: + return None, 'short read (%d bytes)' % len(blob) + grabbed, token, mode = struct.unpack_from(' 0xFFFF: + return None, 'implausible modeNum 0x%x' % mode + return { + 'pid': pid, + 'fd': int(fd_name), + 'target': target, + 'size': size, + 'grabbed': grabbed, + 'token': token, + 'mode': mode, + 'start': proc_starttime(pid), + 'disp': display_matches(pid, display_number(args.display)), + }, None + + +def scan(debug=False): + """Return (best_candidate_or_None, saw_permission_error).""" + best = None + perm_error = False + + try: + pids = [int(x) for x in os.listdir('/proc') if x.isdigit()] + except OSError as exc: + eprint('guest_mode.sh: cannot list /proc: %s' % exc) + return None, False + + for pid in pids: + if args.pid and pid != args.pid: + continue + if not args.pid and not is_xorg(pid): + continue + try: + fds = list(candidate_fds(pid)) + except PermissionError: + perm_error = True + if debug: + eprint('pid %d: permission denied listing fd/' % pid) + continue + for fd_name, target, size in fds: + cand, err = read_candidate(pid, fd_name, target, size) + if cand is None: + if debug: + eprint('pid %d fd %s: %s' % (pid, fd_name, err)) + continue + if debug: + eprint('candidate pid=%d fd=%s size=0x%x token=0x%x mode=%d ' + 'start=%d display_match=%s target=%s' % + (cand['pid'], cand['fd'], cand['size'], cand['token'], + cand['mode'], cand['start'], cand['disp'], cand['target'])) + # Score: display match, exact size, nonzero token, newest Xorg. + key = (cand['disp'], + 1 if cand['size'] == NVCLIP_SIZE_EXACT else 0, + 1 if cand['token'] != 0 else 0, + cand['start'], + -cand['pid']) + if best is None or key > best[0]: + best = (key, cand) + + return (best[1] if best else None), perm_error + + +def main(): + global args + args = parse_args() + deadline = time.monotonic() + max(0.0, args.wait) + interval = max(0.01, args.interval / 1000.0) + perm_error = False + + while True: + cand, pe = scan(args.debug) + perm_error = perm_error or pe + if cand is not None: + print('MODENUM %d' % cand['mode']) + return 0 + if time.monotonic() >= deadline: + break + time.sleep(interval) + + if perm_error: + eprint('guest_mode.sh: Xorg exists but /proc//fd is not readable; ' + 'run as root (sudo).') + print('NO_XORG') + return 4 + print('NO_XORG') + return 3 + + +if __name__ == '__main__': + try: + sys.exit(main()) + except KeyboardInterrupt: + print('NO_XORG') + sys.exit(4) + except Exception as exc: # keep the stdout contract even on bugs + eprint('guest_mode.sh: unexpected error: %s' % exc) + print('NO_XORG') + sys.exit(4) +PY \ No newline at end of file diff --git a/tools/guest-diag/gzdoom_pbuf_run.sh b/tools/guest-diag/gzdoom_pbuf_run.sh new file mode 100755 index 0000000..b152bdf --- /dev/null +++ b/tools/guest-diag/gzdoom_pbuf_run.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Open-IOV contributors +# gzdoom_pbuf_run.sh - in-guest GZDoom run recipe for the offscreen SDL2 +# GLX-pbuffer harness. Run this inside the guest after push_gl_tools.sh has +# placed the shims in /tmp. It prints the exact command line, runs GZDoom +# under timeout, and leaves the output in $GZLOG. +# +# Environment (all overridable): +# GZDOOM_BIN default /opt/gzdoom/gzdoom +# WAD default /usr/share/games/doom/freedoom2.wad +# GZCAP default /tmp/gzframes.bin (sdl_cap.so capture file) +# GZPBUF_W/H default 640/480 (gz_pbuf.so pbuffer size) +# GZLOG default /tmp/gzdoom-pbuf.log +# TIMEOUT default 300 seconds +# GZ_ARGS extra GZDoom args appended after the base set +# GZ_DETACH default 0; set 1 to setsid/nohup the run and return +# DISPLAY default :0 +# +# The base args are windowed GL: +set vid_fullscreen 0, 640x480, OpenGL +# backend 0, +vid_rendermode 4 (GL mode), MAP01. The preload order matters: +# sdl_cap.so first (captures SDL_GL_SwapWindow), gz_pbuf.so second (turns the +# SDL GL context into a GLX pbuffer and the swap into glFinish). +set -u + +GZDOOM_BIN="${GZDOOM_BIN:-/opt/gzdoom/gzdoom}" +WAD="${WAD:-/usr/share/games/doom/freedoom2.wad}" +GZCAP="${GZCAP:-/tmp/gzframes.bin}" +GZPBUF_W="${GZPBUF_W:-640}" +GZPBUF_H="${GZPBUF_H:-480}" +GZLOG="${GZLOG:-/tmp/gzdoom-pbuf.log}" +TIMEOUT="${TIMEOUT:-300}" +GZ_ARGS="${GZ_ARGS:-}" +GZ_DETACH="${GZ_DETACH:-0}" +DISPLAY="${DISPLAY:-:0}" +SDL_VIDEODRIVER="${SDL_VIDEODRIVER:-offscreen}" +LD_PRELOAD="${LD_PRELOAD:-/tmp/sdl_cap.so /tmp/gz_pbuf.so}" + +export DISPLAY SDL_VIDEODRIVER LD_PRELOAD GZCAP GZPBUF_W GZPBUF_H + +if [ ! -x "$GZDOOM_BIN" ]; then + echo "gzdoom_pbuf_run: GZDOOM_BIN not executable: $GZDOOM_BIN" >&2 + exit 2 +fi +if [ ! -f "$WAD" ]; then + echo "gzdoom_pbuf_run: WAD not found: $WAD" >&2 + exit 2 +fi +for so in /tmp/sdl_cap.so /tmp/gz_pbuf.so; do + if [ ! -f "$so" ]; then + echo "gzdoom_pbuf_run: shim missing: $so" >&2 + exit 2 + fi +done + +CMD=( + "$GZDOOM_BIN" + -iwad "$WAD" + +set vid_fullscreen 0 + +set vid_defwidth "$GZPBUF_W" + +set vid_defheight "$GZPBUF_H" + +set vid_preferbackend 0 + +vid_rendermode 4 + -nosound + -nomusic + +vid_activeinbackground 1 + +vid_maxfps 60 + +map MAP01 +) +if [ -n "$GZ_ARGS" ]; then + read -r -a GZ_EXTRA <<< "$GZ_ARGS" + CMD+=("${GZ_EXTRA[@]}") +fi + +echo "gzdoom_pbuf_run: DISPLAY=$DISPLAY SDL_VIDEODRIVER=$SDL_VIDEODRIVER" +echo "gzdoom_pbuf_run: LD_PRELOAD=$LD_PRELOAD" +echo "gzdoom_pbuf_run: GZCAP=$GZCAP GZPBUF_W=$GZPBUF_W GZPBUF_H=$GZPBUF_H" +echo "gzdoom_pbuf_run: LOG=$GZLOG TIMEOUT=$TIMEOUT DETACH=$GZ_DETACH" +printf 'CMD:' +printf ' %q' "${CMD[@]}" +echo + +if [ "$GZ_DETACH" = "1" ]; then + setsid nohup timeout "$TIMEOUT" "${CMD[@]}" > "$GZLOG" 2>&1 < /dev/null & + echo "PID=$!" + echo "LOG=$GZLOG" + exit 0 +fi + +timeout "$TIMEOUT" "${CMD[@]}" > "$GZLOG" 2>&1 +rc=$? +echo "RC=$rc" +echo "LOG=$GZLOG" +exit "$rc" diff --git a/tools/guest-diag/push_gl_tools.sh b/tools/guest-diag/push_gl_tools.sh new file mode 100755 index 0000000..4064d58 --- /dev/null +++ b/tools/guest-diag/push_gl_tools.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Open-IOV contributors +# push_gl_tools.sh - push the guest-side GL/GZDoom harness binaries and scripts +# into the running guest's /tmp and chmod +x them. Uses the established QGA +# guest-exec + base64 mechanism from push_guest_tools.sh (scripts/vgpu_vm.py). +# +# Usage: +# bash tools/guest-diag/push_gl_tools.sh # fixed list below +# bash tools/guest-diag/push_gl_tools.sh FILE... # arbitrary host files +# bash tools/guest-diag/push_gl_tools.sh --dry-run # list only, no push +# +# Destination is /tmp/. The guest's /tmp is wiped on boot; run this +# after every VM start. It does NOT run the pushed programs. +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +VMEXEC="$HERE/../../scripts/vgpu_vm.py" +BUILD="$HERE/build" +DEST_DIR="${DEST_DIR:-/tmp}" + +DRY_RUN=0 +if [ "${1:-}" = "--dry-run" ]; then + DRY_RUN=1 + shift +fi + +FIXED=( + "$BUILD/gl_frames" + "$BUILD/gz_pbuf.so" + "$BUILD/sdl_cap.so" + "$BUILD/test_tri" + "$HERE/guest_mode.sh" + "$HERE/gzf2_video.py" + "$HERE/gzdoom_pbuf_run.sh" +) + +if [ "$#" -gt 0 ]; then + FILES=("$@") +else + FILES=("${FIXED[@]}") +fi + +if [ ! -x "$VMEXEC" ] && [ ! -f "$VMEXEC" ]; then + echo "push_gl_tools: vgpu_vm.py not found: $VMEXEC" >&2 + exit 2 +fi + +push_b64() { # push_b64 + local src="$1" dst="$2" b64 + b64=$(base64 -w0 "$src") + if [ "$DRY_RUN" = "1" ]; then + echo "PUSH-DRY $src -> $dst ($(stat -c%s "$src") bytes)" + return 0 + fi + if ! sudo "$VMEXEC" exec --timeout 120 \ + "echo '$b64' | base64 -d > '$dst' && chmod +x '$dst'" \ + >/dev/null 2>&1; then + echo "PUSH-FAIL $src -> $dst" >&2 + return 1 + fi + echo "PUSHED $src -> $dst" +} + +rc=0 +for f in "${FILES[@]}"; do + if [ ! -f "$f" ]; then + echo "PUSH-SKIP missing $f" >&2 + rc=1 + continue + fi + push_b64 "$f" "$DEST_DIR/$(basename "$f")" || rc=1 +done +exit "$rc" diff --git a/tools/guest-diag/test_tri.c b/tools/guest-diag/test_tri.c new file mode 100644 index 0000000..dc42876 --- /dev/null +++ b/tools/guest-diag/test_tri.c @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Open-IOV contributors +// test_tri.c - minimal SDL2 + OpenGL triangle for the offscreen pbuffer +// harness. Intended to be run with SDL_VIDEODRIVER=offscreen and the +// gz_pbuf.so shim preloaded (plus sdl_cap.so to capture the swaps): +// +// SDL_VIDEODRIVER=offscreen DISPLAY=:0 \ +// LD_PRELOAD="/tmp/sdl_cap.so /tmp/gz_pbuf.so" GZCAP=/tmp/testframes.bin \ +// ./test_tri 20 640 480 +// +// It creates an SDL2 GL window/context (the shim turns the context into a +// GLX pbuffer), clears to a colour, draws a rotating triangle, reads the +// centre pixel back (non-black proof), and calls SDL_GL_SwapWindow each +// frame. The sdl_cap.so hook captures the swap; the gz_pbuf.so hook turns +// the swap into glFinish. Build: +// gcc -O2 -o test_tri test_tri.c $(pkg-config --cflags --libs sdl2) -lGL +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + int frames = argc > 1 ? atoi(argv[1]) : 20; + int w = argc > 2 ? atoi(argv[2]) : 640; + int h = argc > 3 ? atoi(argv[3]) : 480; + SDL_Window *win; + SDL_GLContext ctx; + int i; + + setvbuf(stdout, NULL, _IONBF, 0); + + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + fprintf(stderr, "test_tri: SDL_Init: %s\n", SDL_GetError()); + return 1; + } + printf("test_tri: videodriver=%s\n", + SDL_GetCurrentVideoDriver() ? SDL_GetCurrentVideoDriver() : "?"); + + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + + win = SDL_CreateWindow("test_tri", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, w, h, + SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + if (!win) { + fprintf(stderr, "test_tri: SDL_CreateWindow: %s\n", SDL_GetError()); + SDL_Quit(); + return 2; + } + + ctx = SDL_GL_CreateContext(win); + if (!ctx) { + fprintf(stderr, "test_tri: SDL_GL_CreateContext: %s\n", + SDL_GetError()); + SDL_DestroyWindow(win); + SDL_Quit(); + return 3; + } + if (SDL_GL_MakeCurrent(win, ctx) != 0) + fprintf(stderr, "test_tri: SDL_GL_MakeCurrent: %s\n", + SDL_GetError()); + SDL_GL_SetSwapInterval(0); + + printf("test_tri: GL %s | %s | %s\n", + (const char *)glGetString(GL_VENDOR), + (const char *)glGetString(GL_RENDERER), + (const char *)glGetString(GL_VERSION)); + + for (i = 0; i < frames; i++) { + float a = (float)i * 0.10f; + unsigned char px[4] = { 0, 0, 0, 0 }; + + glViewport(0, 0, w, h); + glClearColor(0.05f, 0.05f, 0.10f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glRotatef(a * 57.29578f, 0.0f, 0.0f, 1.0f); + + glBegin(GL_TRIANGLES); + glColor3f(1.0f, 0.2f, 0.1f); + glVertex2f(0.0f, 0.8f); + glColor3f(0.1f, 1.0f, 0.3f); + glVertex2f(-0.8f, -0.6f); + glColor3f(0.2f, 0.3f, 1.0f); + glVertex2f(0.8f, -0.6f); + glEnd(); + glFinish(); + + glReadPixels(w / 2, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + printf("test_tri: frame=%d centre=%02x%02x%02x%02x err=0x%x\n", + i, px[0], px[1], px[2], px[3], glGetError()); + + SDL_GL_SwapWindow(win); + SDL_Delay(20); + } + + printf("test_tri: done frames=%d %dx%d\n", frames, w, h); + SDL_GL_DeleteContext(ctx); + SDL_DestroyWindow(win); + SDL_Quit(); + return 0; +} diff --git a/tools/guest-diag/tick_gen.py b/tools/guest-diag/tick_gen.py new file mode 100755 index 0000000..d2c176b --- /dev/null +++ b/tools/guest-diag/tick_gen.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Open-IOV contributors +# tick_gen.py - keep the guest-RM timer tick page live in the host-side +# /dev/shm/vgpu-guest-ram image. The guest's timer vmap page is read as a +# u32 millisecond counter; writing a strictly increasing u32 there at 1 ms +# (or --ms) keeps the guest RM's timer path moving. +# +# Usage: sudo python3 tools/guest-diag/tick_gen.py [--ms 1] [--file PATH] +# guest physical address of the timer tick u32 (the guest's +# timer vmap base + 0x6c004) +# --ms N write interval in milliseconds (default 1) +# --file P guest RAM file (default /dev/shm/vgpu-guest-ram) +# +# The file offset is gpa - 0x80000000. The mapping is created once; the loop +# only writes the u32. SIGINT stops cleanly. +import argparse +import mmap +import os +import signal +import struct +import sys +import time + +RAM_BASE = 0x80000000 +DEFAULT_FILE = "/dev/shm/vgpu-guest-ram" +stop = False + + +def on_sigint(signum, frame): + global stop + stop = True + + +def main(): + ap = argparse.ArgumentParser(add_help=True) + ap.add_argument("gpa", help="guest physical address (hex, 0x prefix optional)") + ap.add_argument("--ms", type=float, default=1.0, + help="write interval in milliseconds (default 1)") + ap.add_argument("--file", default=DEFAULT_FILE, help="guest RAM file") + args = ap.parse_args() + + try: + gpa = int(args.gpa, 0) + except ValueError: + gpa = int(args.gpa, 16) + if gpa < RAM_BASE: + print("tick_gen: gpa 0x%x is below 0x%x" % (gpa, RAM_BASE), + file=sys.stderr) + return 2 + off = gpa - RAM_BASE + + fd = os.open(args.file, os.O_RDWR) + try: + size = os.fstat(fd).st_size + if off + 4 > size: + print("tick_gen: offset 0x%x beyond %s (0x%x bytes)" + % (off, args.file, size), file=sys.stderr) + return 2 + mm = mmap.mmap(fd, size, access=mmap.ACCESS_WRITE) + try: + signal.signal(signal.SIGINT, on_sigint) + interval = max(0.0, args.ms / 1000.0) + counter = time.monotonic_ns() // 1_000_000 + iterations = 0 + last_print = time.monotonic() + print("tick_gen: file=%s gpa=0x%x off=0x%x interval=%.3fms" + % (args.file, gpa, off, args.ms), flush=True) + while not stop: + now_ms = time.monotonic_ns() // 1_000_000 + if now_ms <= counter: + now_ms = counter + 1 + counter = now_ms + val = counter & 0xFFFFFFFF + struct.pack_into("= 2.0: + print("tick_gen: iterations=%d value=%d (0x%08x)" + % (iterations, val, val), flush=True) + last_print = t + if interval: + time.sleep(interval) + print("tick_gen: stopped iterations=%d value=0x%08x" + % (iterations, counter & 0xFFFFFFFF), flush=True) + finally: + mm.close() + finally: + os.close(fd) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(0) -- cgit v1.2.3