diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-11-10 14:40:39 -0500 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-11-10 14:40:39 -0500 |
| commit | 72c7e2e6d4443457807d947f38ec650b3b8fb75a (patch) | |
| tree | 5cbfd48ec4dac31d107d27be1f446df1bc102794 /tools/guest-diag/scrub_pin_qmp.py | |
| parent | 6979a6edea641bf6adc5c7dff1196fbc0dd38e7c (diff) | |
| download | open-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.py | 176 |
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()) |
