summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/scrub_pin_qmp.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-11-10 14:40:39 -0500
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-11-10 14:40:39 -0500
commit72c7e2e6d4443457807d947f38ec650b3b8fb75a (patch)
tree5cbfd48ec4dac31d107d27be1f446df1bc102794 /tools/guest-diag/scrub_pin_qmp.py
parent6979a6edea641bf6adc5c7dff1196fbc0dd38e7c (diff)
downloadopen-iov-72c7e2e6d4443457807d947f38ec650b3b8fb75a.tar.gz
open-iov-72c7e2e6d4443457807d947f38ec650b3b8fb75a.zip
vgsp: USERD index/alias resolution, DDX kick-idle fix, completion-slot authorship (CONT.403-412)
Ten debugging rounds against the direct-GL channel, each fixing the wall that blocked the previous one: - USERD index: honour USERD_INDEX_VALUE/PAGE_VALUE from NVOS04 flags (X idx=3, GL idx=4) instead of always reading the block at base+0. This fixed the X "Failed to initialize DMA" wall. - USERD alias: learn the BAR1-alias bias from the first ring-validated FB latch (wire 0x80600 -> live 0x910000) and prefer alias/wire-page candidates before falling back to the RAM scan, which previously adopted another channel's control page. - USERD latch: keep a ring-validated kick-family block across a transient put=0 instead of dropping it (bounded, per-channel log). - VAS alias: DUP_OBJECT copies pdb/entries/ap/verified so hVASpace 0xbfef0001 resolves to 0x10005 and its gpfifo VA stops re-resolving to a bogus FB PA. - Short-kick NOTIFY: complete a kick that has no armed class-2D target (release + event, no write) so the client proceeds past setup. - DDX kick-idle: slot 0 of the 0x01xxxxxx family is the pb_kickIfIdle gate and 0xFFFF is its IDLE value; the keepalive no longer zeroes it. Removes the GR-STALL-stale-wedge on the X channel. - Completion slots: register a per-channel completion on every successful kick and reuse it when a NOTIFY has no per-push SET_NOTIFY; prefer the channel's notifier page over the VA walk. - Empty/no-op submit: author the completion even when there is no push to replay, so a synchronous GL submit does not wait forever. - Scrub records: multi-record per guest, stash-first lookup, registered PA is authoritative against the GPU-VAS walk, scanner-only records author the proven 0xffffffff/0xffffffff pair. - GL attachment: accept a draw run whose own subchannel is the engine subchannel the channel uses (bind is subch 0, draws on subch 3), not only the bind subchannel. Measured: glxgears 427 -> 526 FPS sustained with no GR stall; X binds and renders its 2D path; 3D draw capture on the GL channel still does not occur (see CONT.412: the GL thread blocks on the NVRM RM write lock before it reaches any ioctl). Adds tools/guest-diag/scrub_pin_qmp.py to register guest channel-buffer vmap pages as scrub records from the host.
Diffstat (limited to 'tools/guest-diag/scrub_pin_qmp.py')
-rw-r--r--tools/guest-diag/scrub_pin_qmp.py176
1 files changed, 176 insertions, 0 deletions
diff --git a/tools/guest-diag/scrub_pin_qmp.py b/tools/guest-diag/scrub_pin_qmp.py
new file mode 100644
index 0000000..a69f8eb
--- /dev/null
+++ b/tools/guest-diag/scrub_pin_qmp.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Open-IOV contributors
+# scrub_pin_qmp.py - host-side channel-buffer (PMA scrubber / X DDX CE)
+# semaphore+finish page scanner.
+#
+# The guest RM polls the channel-buffer host semaphore (channelWaitForFreeEntry)
+# and finish payload (channelWaitForFinishPayload) through the *vmap* page of
+# the buffer memdesc. After the guest RM re-creates its kernel VAS the host's
+# GPU-VAS walk can keep resolving a stale page (r1712: pass authored
+# 0x126b60000 while the guest polled 0x124fec000), so the waits never
+# completed. This tool resolves the guest's own vmap pages and registers them
+# through sema_scrubbuf_add; the service then authors the proven all-ones pair
+# into the pages the guest actually reads (CONT.409).
+#
+# Usage (host, root for the QMP socket):
+# sudo python3 tools/guest-diag/scrub_pin_qmp.py [--duration 0] [--interval 2]
+#
+# Every registered page must look like a channel buffer: the ring page
+# (base+0x64000) is all-zero or 906F entries whose GET names a push inside
+# the channel's own GPU window. A page that fails the shape test is skipped.
+import argparse
+import importlib.util
+import os
+import re
+import subprocess
+import sys
+import time
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+OIV = os.path.normpath(os.path.join(HERE, "..", ".."))
+VMEXEC = os.path.join(OIV, "scripts", "vgpu_vm.py")
+PARAM = "/sys/module/vgsp/parameters/sema_scrubbuf_add"
+
+# nv_vm_map_pages areas seen for channel buffers (pb 0x64000 + gpfifo + sema
+# + finish, with per-size variants). The fwimage boots observed 0x6e000 and
+# neighbours; keep a small band and rely on the ring shape test.
+SCRUB_SIZES = (0x6b000, 0x6c000, 0x6d000, 0x6e000, 0x6f000, 0x70000,
+ 0x71000, 0x72000)
+PAGE = 0x1000
+
+
+def log(msg):
+ print("[%s] %s" % (time.strftime("%H:%M:%S"), msg), flush=True)
+
+
+def load_qmp():
+ spec = importlib.util.spec_from_file_location(
+ "semfix_watch", os.path.join(HERE, "semfix_watch.py"))
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod.Qmp("/tmp/qemu-qmp.sock")
+
+
+def vmexec(cmd, timeout=25):
+ try:
+ out = subprocess.run(
+ ["python3", VMEXEC, "exec", "--timeout", str(timeout), cmd],
+ capture_output=True, text=True, timeout=timeout + 15).stdout
+ except Exception:
+ return ""
+ return out
+
+
+def guest_vmaps():
+ out = vmexec(
+ "sudo -n grep -E 'nv_vm_map_pages' /proc/vmallocinfo 2>/dev/null | "
+ "awk '{split($1,a,\"-\"); "
+ "sz=strtonum(\"0x\" substr(a[2],3))-strtonum(\"0x\" substr(a[1],3)); "
+ "print a[1], sz}'", timeout=20)
+ found = []
+ for line in out.splitlines():
+ m = re.match(r"^(0x[0-9a-fA-F]+)\s+(\d+)$", line.strip())
+ if m:
+ base, size = int(m.group(1), 16), int(m.group(2))
+ if size in SCRUB_SIZES:
+ found.append((base, size))
+ return found
+
+
+def phys_read64(q, pa, n=2):
+ """Read n qwords of guest physical memory through the QEMU monitor.
+
+ HMP prints each output line as "<addr>: <values>"; only the part after
+ the colon is data (the address tokens are not).
+ """
+ out = q.hmp("xp/%dgx 0x%x" % (n, pa))
+ vals = []
+ for line in out.splitlines():
+ if ":" not in line:
+ continue
+ vals += [int(x, 16) for x in re.findall(r"0x([0-9a-fA-F]+)",
+ line.split(":", 1)[1])]
+ return vals[:n]
+
+
+def ring_ok(q, ring_pa):
+ """906F GPFIFO ring page: all-zero or entries whose GET names a push
+ inside the channel's own GPU window (the guest scanner's shape test,
+ guest_pin_stream.py _scrub_ring_ok)."""
+ vals = phys_read64(q, ring_pa, n=16)
+ if not vals:
+ return False
+ for i in range(0, len(vals), 2):
+ e0 = vals[i] & 0xffffffff
+ if e0 == 0:
+ continue
+ # Low 32 bits of the push GPU VA: the CeUtils scrubber window is
+ # 0x120000000..0x120080000, the DDX channel buffer pushes reach
+ # 0x1200a0000+, so accept through 0x120200000.
+ if not (0x20000000 <= e0 < 0x20200000) or (e0 & 3):
+ return False
+ return True
+
+
+def register(fin_pa, hsem_pa):
+ req = "0x%x,0x%x" % (fin_pa, hsem_pa)
+ r = subprocess.run(["sudo", "tee", PARAM], input=req + "\n",
+ capture_output=True, text=True)
+ return r.returncode == 0
+
+
+def clear():
+ register(0, 0)
+
+
+def scan(q):
+ pages = []
+ for base, size in guest_vmaps():
+ try:
+ hsem = q.gva2gpa(base + 0x6c000)
+ ring = q.gva2gpa(base + 0x64000)
+ except Exception:
+ continue
+ if not hsem or not ring:
+ continue
+ if not ring_ok(q, ring):
+ log("reject vmap %#x (ring shape)" % base)
+ continue
+ pages.append((hsem + 4, hsem))
+ return pages
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--duration", type=float, default=0, help="seconds (0 = forever)")
+ ap.add_argument("--interval", type=float, default=2.0)
+ args = ap.parse_args()
+
+ q = load_qmp()
+ cur = None
+ start = time.time()
+ while True:
+ if args.duration and time.time() - start >= args.duration:
+ break
+ try:
+ pages = scan(q)
+ except Exception as e:
+ log("scan failed: %s" % e)
+ pages = []
+ key = tuple(sorted(pages))
+ if key != cur:
+ if cur is not None:
+ clear()
+ for fin, hsem in pages:
+ if register(fin, hsem):
+ log("registered fin=%#x hsem=%#x" % (fin, hsem))
+ else:
+ log("register failed fin=%#x hsem=%#x" % (fin, hsem))
+ cur = key
+ time.sleep(args.interval)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())