summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/scrub_pin_qmp.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-11-11 00:23:21 -0500
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-11-11 00:23:21 -0500
commit131d44e083925f35f842114b6f7b6db546f28368 (patch)
treeb3cf3da4a55a920c8d04d289adf682d853598420 /tools/guest-diag/scrub_pin_qmp.py
parent25e3e800df8e5caf18d7e846a0cd36df4f5834f9 (diff)
downloadopen-iov-131d44e083925f35f842114b6f7b6db546f28368.tar.gz
open-iov-131d44e083925f35f842114b6f7b6db546f28368.zip
vgsp: all-ones scrub release for registered waits; walk-page registration; GL frame publisher (CONT.416-417)
- gsp_sema_service: merge a scanner registration arriving after the stash record instead of dropping it (a dropped registration let the next authoring pass write the blocking pair into the page the guest was polling, so the wait never ended); author the finish word all-ones for every registered record while the sema word stays put-based once real submissions were observed (CONT.416/417). - scrub_pin_qmp (host scanner): accept an unstable-but-live wait page instead of skipping it, and register the walk-resolved scrubber channel page - first the walk summary, then the per-VAS kernel row (h=0xc) when the summary path fails - so the release is armed before the guest polls (r1762/r1764 evidence). - guest-diag: add gl_frames, the direct-GL (GLX pbuffer) frame publisher; with it the stack produced the first non-black GPU-rendered frames (40/40, 640x480, animated) through pbuffer -> sysmem -> ivshmem -> host frame_reader. - gsp_rm_emulation: log the display-shim request words for triage.
Diffstat (limited to 'tools/guest-diag/scrub_pin_qmp.py')
-rw-r--r--tools/guest-diag/scrub_pin_qmp.py219
1 files changed, 207 insertions, 12 deletions
diff --git a/tools/guest-diag/scrub_pin_qmp.py b/tools/guest-diag/scrub_pin_qmp.py
index a69f8eb..653bb5d 100644
--- a/tools/guest-diag/scrub_pin_qmp.py
+++ b/tools/guest-diag/scrub_pin_qmp.py
@@ -44,12 +44,32 @@ def log(msg):
print("[%s] %s" % (time.strftime("%H:%M:%S"), msg), flush=True)
-def load_qmp():
+QMP_PATH = "/tmp/qemu-qmp.sock"
+
+
+def load_qmp(wait=0.0):
+ """Connect to the QEMU monitor, retrying until it exists.
+
+ The systemd unit starts at host boot, before the VM exists, and cycle.sh
+ removes and re-creates the socket on every VM cycle. Wait for the socket
+ instead of exiting, so one long-lived scanner covers the whole session
+ (it must already be scanning when Xorg creates its DDX channel buffer -
+ that is the point of running it from boot)."""
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")
+ deadline = time.time() + wait if wait else None
+ last = "socket missing"
+ while True:
+ if os.path.exists(QMP_PATH):
+ try:
+ return mod.Qmp(QMP_PATH)
+ except Exception as e:
+ last = e
+ if deadline is not None and time.time() >= deadline:
+ raise SystemExit("no QMP socket: %s" % last)
+ time.sleep(2)
def vmexec(cmd, timeout=25):
@@ -113,6 +133,22 @@ def ring_ok(q, ring_pa):
return True
+def wait_shape(q, hsem):
+ """A channel-buffer wait signature: the host semaphore reads a
+ plausible consumed count and the finish word is still zero.
+
+ r1749: the DDX's own channel buffer does not expose the CeUtils ring
+ layout at +0x64000 (its ring reads the semaphore word 0xfff), so the
+ ring test rejected exactly the page X was blocked on
+ (_nv015102rm timed wait on vmap+0x6c004). The consumed value with a
+ zero finish word is the wait the release pair exists to end."""
+ hv = phys_read64(q, hsem, n=4)
+ if len(hv) < 2:
+ return False
+ return 0 < hv[0] <= 0x20000 and hv[1] == 0 and hv[2] == 0 \
+ and hv[3] == 0
+
+
def register(fin_pa, hsem_pa):
req = "0x%x,0x%x" % (fin_pa, hsem_pa)
r = subprocess.run(["sudo", "tee", PARAM], input=req + "\n",
@@ -134,13 +170,148 @@ def scan(q):
continue
if not hsem or not ring:
continue
- if not ring_ok(q, ring):
- log("reject vmap %#x (ring shape)" % base)
- continue
+ # r1749/r1752: the ring-shape test rejected exactly the buffers
+ # the guest was blocked on, and the QMP physical reads are not
+ # stable enough to shape-test a moving page. The candidate set is
+ # already narrow (nv_vm_map_pages area of a channel-buffer size),
+ # so register every stable candidate; the service only authors a
+ # record the guest itself polls (and only while !submitted).
+ #
+ # r1760: in the single green cycle the strict "two identical reads"
+ # gate skipped the live DDX wait page (hsem_pa=0x103789000) for the
+ # WHOLE cycle -> no registration -> the Xorg DDX wedge the release
+ # exists to end. Retry the read a few times (the flakiness is often
+ # a single bad sample), and accept a page that reads wait-shaped
+ # even when the samples disagree: consumed count in (0, 0x20000]
+ # with a zero finish word is the exact signature of the wait this
+ # registration releases (wait_shape below).
+ stable = None
+ prev = None
+ last = []
+ for _ in range(6):
+ hv = phys_read64(q, hsem, n=2)
+ if len(hv) < 2:
+ continue
+ last = hv
+ if prev is not None and hv == prev:
+ stable = hv
+ break
+ prev = hv
+ if stable is None:
+ # r1760: the live page read hsem=0x20000064/fin=0x6001 while the
+ # DDX spun on it and every sample pair disagreed. The page the
+ # guest waits on IS a live channel buffer; a nonzero sema word
+ # at the channel-buffer sema offset is that signature. Register
+ # with a loud log; the service only authors records the guest
+ # polls and (for all-ones) only while no USERD submissions are
+ # observed, so a false registration is bounded.
+ if wait_shape(q, hsem) or (last and last[0]):
+ log("register vmap %#x (unstable but live: last=%s) "
+ "hsem_pa=%#x" % (base, [hex(v) for v in last], hsem))
+ else:
+ log("skip vmap %#x (unstable read) hsem_pa=%#x" %
+ (base, hsem))
+ continue
pages.append((hsem + 4, hsem))
+ wp = walk_page_cached(SCRUB_SEMA_VA)
+ if wp:
+ pages.append((wp + 4, wp))
return pages
+# r1762: the DDX scrub wait polls the CeUtils scrubber channel's sema/fin
+# page at the GPU VA 0x12006c000 (+4 = fin, the mem_scrub finish word), which
+# the service's kernel-VAS walk resolves (walk_probe) - NOT the guest-vmap
+# candidate pages this scanner used to register (0x128013000 was already
+# all-ones and the guest still spun; registering the walk-resolved
+# 0x12801b000 released the wait in ~23 ms). Register both: the vmap path
+# stays the fallback for boots where the walk is stale (r1712).
+SCRUB_SEMA_VA = 0x12006c000
+WALK_PARAM = "/sys/module/vgsp/parameters/walk_probe"
+_walk_cache = {"va": 0, "pa": 0, "at": 0.0}
+
+
+def host_walk_page(va):
+ """Resolve a GPU VA to a guest PA through the service's kernel-VAS walk.
+
+ walk_probe is a module parameter; the result lands in dmesg as the last
+ 'sema-walk-probe: ... discovered rc=0 pa=0x...' block."""
+ try:
+ r = subprocess.run(["sudo", "tee", WALK_PARAM],
+ input="0x%x\n" % va,
+ capture_output=True, text=True)
+ if r.returncode != 0:
+ return 0
+ out = subprocess.run(["sudo", "dmesg"], capture_output=True,
+ text=True).stdout
+ except Exception:
+ return 0
+ pa = 0
+ kern_pa = 0
+ vas_h = None
+ for line in out.splitlines():
+ if "sema-walk-probe: va=0x" in line:
+ pa = 0 # a newer block begins; only its result counts
+ kern_pa = 0
+ vas_h = None
+ elif "sema-walk-probe: vas[" in line:
+ try:
+ vas_h = int(line.split("h=0x", 1)[1].split()[0], 16)
+ except Exception:
+ vas_h = None
+ elif "sema-walk-probe:" in line and "rc=0 pa=0x" in line:
+ try:
+ p = int(line.rsplit("pa=0x", 1)[1].split()[0], 16)
+ except Exception:
+ p = 0
+ if "discovered" in line:
+ pa = p
+ elif vas_h == 0xc and p:
+ # r1764: the summary 'discovered' path can fail (rc=-2)
+ # while the per-VAS kernel row (h=0xc) still resolves the
+ # polled scrubber sema page (r1667-authoritative).
+ kern_pa = p
+ return pa or kern_pa
+
+
+def walk_page_cached(va):
+ now = time.time()
+ if _walk_cache["va"] == va and now - _walk_cache["at"] < 15:
+ return _walk_cache["pa"]
+ pa = host_walk_page(va)
+ if pa != _walk_cache["pa"] or not _walk_cache["at"]:
+ log("scrubber walk page va=%#x -> pa=%#x (fin=%#x)" %
+ (va, pa, pa + 4))
+ _walk_cache.update({"va": va, "pa": pa, "at": now})
+ return pa
+
+
+def module_generation():
+ """Identity of the current vgsp module + QEMU instance.
+
+ scrub_pin_qmp is long-lived: cycle.sh rmmod/insmods vgsp and relaunches
+ QEMU between rounds. Both reset the registration state the scanner
+ tracks implicitly, so a changed generation must force a full re-register
+ even when the guest's vmap addresses happen to repeat."""
+ mod = "?"
+ try:
+ with open("/proc/modules") as f:
+ for line in f:
+ if line.startswith("vgsp "):
+ f = line.split()
+ mod = f[0] + "@" + (f[5] if len(f) > 5 else "?")
+ break
+ except Exception:
+ pass
+ pid = "?"
+ try:
+ with open("/tmp/qemu-harness.pid") as f:
+ pid = f.read().strip()
+ except Exception:
+ pass
+ return (mod, pid)
+
+
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--duration", type=float, default=0, help="seconds (0 = forever)")
@@ -149,25 +320,49 @@ def main():
q = load_qmp()
cur = None
+ gen = None
+ last_refresh = 0.0
start = time.time()
while True:
if args.duration and time.time() - start >= args.duration:
break
+ g = module_generation()
+ if g != gen:
+ log("generation %s -> %s, re-registering" % (gen, g))
+ gen = g
+ cur = None
try:
pages = scan(q)
except Exception as e:
log("scan failed: %s" % e)
pages = []
key = tuple(sorted(pages))
+ now = time.time()
if key != cur:
- if cur is not None:
- clear()
+ if cur is not None and not key:
+ # r1743: a transiently-empty scan (the live buffer was only
+ # visible through a rejected alias for a pass) used to
+ # clear() and the release stopped, wedging the DDX wait the
+ # registration exists to release. Keep the live records;
+ # only a *different non-empty* set replaces them.
+ log("scan empty; keeping %d registration(s)" % len(cur))
+ else:
+ 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
+ last_refresh = now
+ elif key and now - last_refresh > 30:
+ # Additive re-assert: the service reaps records on its own
+ # (bounded registry); re-registering is idempotent because
+ # records are keyed by hsem_pa.
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
+ register(fin, hsem)
+ last_refresh = now
time.sleep(args.interval)
return 0