summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--kernel/gsp_emulation.c35
-rw-r--r--kernel/gsp_emulation.h8
-rw-r--r--kernel/gsp_sema_service.c530
-rw-r--r--kernel/gsp_sema_service.h7
-rw-r--r--kernel/main.c81
-rwxr-xr-xtools/guest-diag/cycle-r1780.sh142
-rw-r--r--tools/guest-diag/gzf2_video.py232
-rw-r--r--tools/guest-diag/params-r1780.txt2
9 files changed, 1010 insertions, 31 deletions
diff --git a/.gitignore b/.gitignore
index 4d20bc9..f844d85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -166,3 +166,7 @@ userspace/sdks/
/*.mp4
/open-iov-*.mp4
kernel/golden-mme-tu104.h
+
+# generated per-cycle launcher + run scratch (r1780 cycle tooling)
+tools/guest-diag/launch_qemu_r1780.run.sh
+tools/guest-diag/run/
diff --git a/kernel/gsp_emulation.c b/kernel/gsp_emulation.c
index 38f6284..b82bab1 100644
--- a/kernel/gsp_emulation.c
+++ b/kernel/gsp_emulation.c
@@ -705,6 +705,38 @@ void gsp_emu_raise_gsp_event_irq(struct gsp_emu_context *ctx)
* Emulation Thread
* ============================================================ */
+/* Advance the emulated GPU PTIMER (NV_PTIMER_TIME_0/1 at BAR0 0x9400/0x9410).
+ *
+ * The guest's nvidia.ko RM uses this register as its time source for timed
+ * waits (e.g. _nv015102rm -> _nv018147rm -> _nv018351rm). BAR0 is a plain
+ * host shadow mapped to the guest, so nothing advances these registers by
+ * default: every RM deadline becomes unreachable and the DDX/GL client
+ * spins forever inside the driver (observed as an Xorg hang at GDI/window
+ * creation). Seed from the existing value to keep time continuous, then
+ * advance by the host monotonic clock.
+ */
+static void gsp_emu_tick_ptimer(struct gsp_emu_context *ctx)
+{
+ u64 now, val;
+
+ if (!ctx->bar0_vaddr)
+ return;
+
+ now = ktime_get_ns();
+ if (!ctx->ptimer_seeded) {
+ u32 lo = gsp_emu_shadow_read32(ctx, 0x9400);
+ u32 hi = gsp_emu_shadow_read32(ctx, 0x9410);
+
+ ctx->ptimer_base = ((u64)hi << 32) | lo;
+ ctx->ptimer_t0 = now;
+ ctx->ptimer_seeded = true;
+ }
+
+ val = ctx->ptimer_base + (now - ctx->ptimer_t0);
+ gsp_emu_shadow_write32(ctx, 0x9400, (u32)val);
+ gsp_emu_shadow_write32(ctx, 0x9410, (u32)(val >> 32));
+}
+
static int gsp_emu_thread_fn(void *data)
{
struct gsp_emu_context *ctx = data;
@@ -716,6 +748,9 @@ static int gsp_emu_thread_fn(void *data)
nv_vgpu_info(ctx->vgpu, "GSP emulation thread started");
while (!kthread_should_stop()) {
+ /* Keep the guest-visible GPU timer advancing (RM timed waits) */
+ gsp_emu_tick_ptimer(ctx);
+
/* Read doorbell */
doorbell_val = gsp_emu_read_doorbell(ctx);
diff --git a/kernel/gsp_emulation.h b/kernel/gsp_emulation.h
index d9a7f01..16f4db4 100644
--- a/kernel/gsp_emulation.h
+++ b/kernel/gsp_emulation.h
@@ -192,6 +192,14 @@ struct gsp_emu_context {
u32 bar0_window_base; /* last-seen NV_PBUS_BAR0_WINDOW BASE (page) */
u32 bar0_window_target; /* last-seen TARGET (0=vidmem, 2/3=sysmem) */
bool bar0_window_valid; /* base/target captured at least once */
+
+ /* Emulated PTIMER (BAR0 0x9400/0x9410): the guest RM's time source.
+ * Seeded from the shadow's current 64-bit value on the first tick so
+ * the guest-observed time stays continuous, then advanced by the host
+ * monotonic clock. Without this the RM's timed waits never expire. */
+ u64 ptimer_base;
+ u64 ptimer_t0;
+ bool ptimer_seeded;
};
/* Shadow accessors shared with gsp_rm_emulation.c */
diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c
index 6bf2b38..7e389f8 100644
--- a/kernel/gsp_sema_service.c
+++ b/kernel/gsp_sema_service.c
@@ -3554,6 +3554,51 @@ static bool svc_pdb_l0_sane(struct nv_vgpu_device *vgpu, u64 pdb, int ap)
}
/*
+ * r1780 (V35): the registered verified directory can be FREED by the guest
+ * when it rebuilds its VAS; the page then reads all-zero (measured c36:
+ * pdb 0x11d67f000 was valid at t0 and zero seconds later). The beef family
+ * is exempt from the stale-candidate demote below to avoid churn, so a
+ * dead verified entry blocked every fresh parked candidate and the ring/
+ * push walks regressed to rc=-2. Detect the all-zero page and demote it so
+ * the promotion pass can install the freshest content-verified directory.
+ */
+static bool svc_vas_registered_pdb(u32 h_vaspace, u64 *pdb, int *ap)
+{
+ unsigned long flags;
+ bool ok = false;
+ int i;
+
+ if (!h_vaspace)
+ return false;
+ spin_lock_irqsave(&svc.vas_lock, flags);
+ for (i = 0; i < SVC_MAX_VASPACES; i++)
+ if (svc.vas[i].valid && svc.vas[i].verified &&
+ svc.vas[i].h_vaspace == h_vaspace) {
+ *pdb = svc.vas[i].pdb;
+ *ap = svc.vas[i].ap;
+ ok = true;
+ break;
+ }
+ spin_unlock_irqrestore(&svc.vas_lock, flags);
+ return ok;
+}
+
+static bool svc_pdb_reads_zero(struct nv_vgpu_device *vgpu, u64 pdb, int ap)
+{
+ u64 ent[64];
+ int i;
+
+ if (!pdb)
+ return true;
+ if (svc_read_ap(vgpu, pdb, ap, ent, sizeof(ent)))
+ return true;
+ for (i = 0; i < ARRAY_SIZE(ent); i++)
+ if (ent[i])
+ return false;
+ return true;
+}
+
+/*
* Verify each parked candidate against one content-validated translation
* (the ring VA -> PA the RAM scan just proved): walk the ring page through
* the candidate and register only the directory that reproduces it. Runs on
@@ -3964,6 +4009,104 @@ static int svc_gmmu_walk(struct nv_vgpu_device *vgpu, u64 pdb, u32 entries,
}
/*
+ * r1780 (V34): bounded FB/RAM page-directory scan for a content-verified
+ * pair. The direct-GL client's VAS directory is not in the parked
+ * COPY_SERVER_PDES candidates on every boot (the retired page reads zero),
+ * yet its page tables can live in the emulated FB aperture. Walk every
+ * 4 KB-aligned candidate root in the FB shadow (and, optionally, in guest
+ * RAM up to a caller bound) for `va` and accept the root that reproduces
+ * the caller's content-verified PA. On a hit register the directory under
+ * the caller's VAS handle so the ordinary push/VAS walk resumes.
+ *
+ * This is the deterministic replacement for guessing a PDB: the pair is
+ * content truth (a GPFIFO ring page or a push page the guest wrote), not a
+ * heuristic. Bounded and read-only; a wrong root costs one failed walk.
+ */
+int gsp_sema_service_pdb_scan(u64 va, u64 pa, u32 h_vas, u64 ram_lim)
+{
+ struct nv_vgpu_device *vgpu;
+ u64 off, lim;
+ int ap_in, hits = 0;
+
+ if (!va || !pa || !h_vas)
+ return -EINVAL;
+ mutex_lock(&nv_vgpu_driver.lock);
+ list_for_each_entry(vgpu, &nv_vgpu_driver.devices, list) {
+ for (ap_in = SVC_APERTURE_FB; ap_in >= SVC_APERTURE_SYS;
+ ap_in--) {
+ if (ap_in == SVC_APERTURE_FB) {
+ if (!vgpu->bars[1].vaddr)
+ continue;
+ /* full per-type shadow (512 MiB), not the
+ * old 64 MiB cap: the client's vidmem
+ * allocations reach the top of it. */
+ lim = svc_fb_shadow_size(vgpu);
+ } else {
+ if (!ram_lim)
+ continue;
+ lim = min_t(u64, ram_lim, svc_pa_max());
+ }
+ for (off = 0; off + SEMA_PAGE_SIZE <= lim;
+ off += SEMA_PAGE_SIZE) {
+ u64 addr = 0;
+ int ap = ap_in;
+
+ if (svc_gmmu_walk(vgpu, off, 4, va, ap_in,
+ &addr, &ap, false))
+ continue;
+ if ((addr & ~(u64)(SEMA_PAGE_SIZE - 1)) !=
+ (pa & ~(u64)(SEMA_PAGE_SIZE - 1)))
+ continue;
+ {
+ struct sema_vaspace_desc d = {
+ .h_vaspace = h_vas,
+ .pdb = off,
+ .num_entries = 4,
+ .b_vidmem =
+ ap_in == SVC_APERTURE_FB,
+ .verified = true,
+ };
+
+ gsp_sema_service_vaspace_add(&d);
+ }
+ pr_info("sema-svc: pdb-scan HIT h=0x%x pdb=0x%llx entries=4 ap=%d for va=0x%llx -> pa=0x%llx (verified)\n",
+ h_vas, off, ap_in, va, addr);
+ hits++;
+ if (hits >= 4)
+ break;
+ }
+ if (hits >= 4)
+ break;
+ }
+ }
+ mutex_unlock(&nv_vgpu_driver.lock);
+ if (!hits)
+ pr_info("sema-svc: pdb-scan no root maps va=0x%llx -> pa=0x%llx (h=0x%x, fb=%s ram=0x%llx)\n",
+ va, pa, h_vas,
+ "scanned", ram_lim);
+ return hits ? 0 : -ENOENT;
+}
+EXPORT_SYMBOL_GPL(gsp_sema_service_pdb_scan);
+
+/* r1780 (V34): direct VAS registration for a directory found out-of-band
+ * (e.g. by the pdb scan above or an offline walk). "h,pdb,entries,vidmem". */
+void gsp_sema_service_vas_force(u32 h_vas, u64 pdb, u32 entries, bool vid)
+{
+ struct sema_vaspace_desc d = {
+ .h_vaspace = h_vas,
+ .pdb = pdb,
+ .num_entries = entries ? entries : 4,
+ .b_vidmem = vid,
+ .verified = true,
+ };
+
+ gsp_sema_service_vaspace_add(&d);
+ pr_info("sema-svc: vas-force h=0x%x pdb=0x%llx entries=%u ap=%d\n",
+ h_vas, pdb, entries ? entries : 4, vid ? 2 : 1);
+}
+EXPORT_SYMBOL_GPL(gsp_sema_service_vas_force);
+
+/*
* Page directories the driver published (SET_PAGE_DIRECTORY).
*/
/*
@@ -15597,6 +15740,88 @@ static bool svc_method_stream_plausible(const u8 *buf, u32 len)
}
/*
+ * r1780 (V36): FB-shadow content rescue for the direct-GL client's pushes.
+ *
+ * The GL client writes its pushbuffer through the BAR1 aperture, so the
+ * bytes land in the emulated FB shadow. Its VAS page directory is rebuilt
+ * every few seconds, so the push VA walk is dead exactly while the setup
+ * push (the 59556-byte shader/program upload) is being consumed. The
+ * linear-window guess is close but not exact (measured: push 0x4024004 read
+ * at 0x103187000 while the ring PA proved the allocation elsewhere), and
+ * the strict guest scanner cannot name the pages either. This rescue scans
+ * the shadow for a window of the demanded length that passes the same
+ * method-stream parse every trusted push passes, and pins the VA to the FB
+ * offset it found. Debug-gated (default off): a plausible window elsewhere
+ * in the shadow would execute the wrong methods.
+ */
+static bool gsp_sema_gr_fb_push;
+module_param_named(sema_gr_fb_push, gsp_sema_gr_fb_push, bool, 0644);
+MODULE_PARM_DESC(sema_gr_fb_push,
+ "Scan the FB shadow for an unreadable GR push of the same length and pin the VA to the found window (default 0)");
+
+struct svc_fb_push_hit {
+ u32 len;
+ u32 n;
+ u64 off;
+};
+
+static bool svc_fb_push_rescue(struct nv_vgpu_device *vgpu, u64 va, u32 len,
+ u8 *out, u64 *off_out)
+{
+ static struct svc_fb_push_hit hits[8];
+ u8 *base = vgpu->bars[1].vaddr;
+ u64 lim = min_t(u64, svc_fb_shadow_size(vgpu), 0x4000000ULL), off;
+ static u32 log;
+ int i;
+
+ if (!gsp_sema_gr_fb_push || !base || !len || len > 0x40000)
+ return false;
+ if (len > lim)
+ return false;
+ /* Reuse a previously proven window for this length first. */
+ for (i = 0; i < ARRAY_SIZE(hits); i++) {
+ if (hits[i].n && hits[i].len == len &&
+ hits[i].off + len <= lim &&
+ svc_method_stream_plausible(base + hits[i].off, len)) {
+ memcpy(out, base + hits[i].off, len);
+ *off_out = hits[i].off;
+ return true;
+ }
+ }
+ /* Cheap first: the first 64 bytes must parse as a stream prefix. */
+ for (off = 0; off + len <= lim; off += SEMA_PAGE_SIZE) {
+ u32 pre = min_t(u32, len, 64);
+
+ if (!svc_method_stream_plausible(base + off, pre))
+ continue;
+ if (!svc_method_stream_plausible(base + off, len))
+ continue;
+ memcpy(out, base + off, len);
+ *off_out = off;
+ for (i = 0; i < ARRAY_SIZE(hits); i++) {
+ if (!hits[i].n || hits[i].len == len) {
+ hits[i].n = 1;
+ hits[i].len = len;
+ hits[i].off = off;
+ break;
+ }
+ }
+ if (log < 16) {
+ log++;
+ pr_info("sema-chan: FB-PUSH rescue h=0x%x va=0x%llx len=%u -> fb 0x%llx (plausible window; pinned for later reads)\n",
+ va, len, off);
+ }
+ return true;
+ }
+ if (log < 16) {
+ log++;
+ pr_info("sema-chan: FB-PUSH rescue h=0x%x va=0x%llx len=%u: no plausible window in 0x%llx shadow\n",
+ va, len, lim);
+ }
+ return false;
+}
+
+/*
* Decode one page as a method stream and execute the pushes it contains.
*
* Push buffers are written back to back and pushes can span page
@@ -26948,10 +27173,22 @@ static int svc_chan_userd(struct nv_vgpu_device *vgpu, struct svc_channel *ch,
}
if (ring_pa) {
- cand[nc] = ring_pa + 0x2000; as[nc++] = ring_ap;
- cand[nc] = ring_pa + 0x2000; as[nc++] = ring_alt;
- cand[nc] = ring_pa + 0x1000; as[nc++] = ring_ap;
- cand[nc] = ring_pa + 0x1000; as[nc++] = ring_alt;
+ /*
+ * r1780 (V38): the +0x2000/+0x1000 convention places the
+ * USERD after a 1024-entry (0x2000-byte) ring. A 32768-entry
+ * GPFIFO spans 0x40000 bytes, so those addresses are GPFIFO
+ * slots, not a control block; the probe still "passed"
+ * (put=25205/get=512 from slot 1041) and the pump then wrote
+ * GP_GET into the ring. Only use the convention for small
+ * rings and let the declared/RAM-scan candidates serve the
+ * large ones.
+ */
+ if (ch->gpfifo_entries <= 1024) {
+ cand[nc] = ring_pa + 0x2000; as[nc++] = ring_ap;
+ cand[nc] = ring_pa + 0x2000; as[nc++] = ring_alt;
+ cand[nc] = ring_pa + 0x1000; as[nc++] = ring_ap;
+ cand[nc] = ring_pa + 0x1000; as[nc++] = ring_alt;
+ }
/*
* UVM's CE channel allocations place the USERD one 0x10000
* block after the ring (observed for all eight CE channels:
@@ -27487,22 +27724,23 @@ ce_fb_scan:
*put = best_put;
*get = best_get;
/*
- * SESSION 153-CONTINUED-68: deliberately do NOT
- * seed the speculative latch here. Seeding
- * armed the pump's judge on channels that
- * previously accepted the VRAM candidate
- * quietly; a pool block whose PUT advances in
- * jumps (r1133: 219 -> 996) or a stale seed
- * from an earlier RAM-scan candidate (r1132:
- * latch=2 vs put=6) then rejected on every
- * pass - adopt/reject spin at ~10 Hz (4638
- * REJECTED lines on r1132, the guest's Xorg
- * soft-locked, glxgears starved). The r1129-
- * r1131 good boots show the intended shape:
- * VRAM adopts logged, no rejects, ring
- * consumption via the walk.
+ * r1780 (V32): seed the speculative latch with
+ * the adopted block's own PUT. The previous
+ * code left latch_put at 0 (or at a previous
+ * RAM-scan candidate's value), so the pump's
+ * judge saw put != latch_put on the very first
+ * pass and rejected/cleared the just-adopted
+ * USERD - the ~10 Hz adopt/reject spin that
+ * starved the X server's channel (r1132 class,
+ * reproduced on r1780 c31: "speculative latch
+ * REJECTED (put=86 latch=512)" every pass).
+ * With the seed, the first judge pass is a
+ * no-op and a forward step confirms; a decrease
+ * or the 15 s no-advance bound still rejects a
+ * stale block.
*/
- ch->gr_ram_userd_latch_at = 0;
+ ch->gr_ram_userd_latch_put = best_put;
+ ch->gr_ram_userd_latch_at = jiffies;
ch->gr_ram_userd_confirmed = false;
pr_info("sema-chan: %s USERD h=0x%x rank=%u found by VRAM shape scan at fb 0x%llx (put=%u get=%u%s, ring-validated)\n",
ch->engine_type ? "CE" : "GR",
@@ -29894,6 +30132,123 @@ static void svc_gr_setup_hold_clear(struct svc_channel *ch)
ch->gr_setup_hold_idx = ~0u;
}
+/*
+ * r1780 (V37): eager push-page pins at the ring-latch instant.
+ *
+ * The direct-GL client rebuilds its VAS every few seconds while its GL init
+ * retries; a push-VA walk is only possible while the directory incarnation
+ * that the ring scan just content-verified is still live. The RAM content
+ * scan pins the ring page, but the pushbuffer pages were left to a later
+ * walk that almost always landed after the next rebuild (measured c37/c39:
+ * the verified pdb answered the ring at +0 s and ap0 at +15..20 s; the
+ * 59556-byte setup push then read as an implausible window guess and was
+ * skipped). Resolve and pin the entry-referenced push pages inside the same
+ * latch, while the just-verified directory is provably alive.
+ *
+ * Only successful walks are pinned. The first page of each push must also
+ * pass the method-stream prefix parse, so a directory that maps the ring to
+ * garbage cannot pin arbitrary pages. Bounded (32 entries / 64 pages per
+ * latch); later reads still validate the full stream.
+ */
+static bool gsp_sema_pin_pushes = true;
+static unsigned long svc_push_pin_at;
+module_param_named(sema_pin_pushes, gsp_sema_pin_pushes, bool, 0644);
+MODULE_PARM_DESC(sema_pin_pushes,
+ "Pin the ring entry's pushbuffer pages through the freshly verified VAS at the ring-latch instant (default 1)");
+
+/*
+ * r1780 (V38): pin every page of a just-resolved GPFIFO ring.
+ *
+ * The pump's entry fetch is VA-based and page-granular: only the base page
+ * that svc_lookup() answered is pinned, so a 32768-entry (64-page) ring
+ * silently breaks at idx=512 (`svc.gpfifo_nomap++`, no log) and no push is
+ * ever decoded. Walk the whole ring through the same registered VAS right
+ * when the translation is live and pin each page; the pins survive the
+ * client's VAS churn. Rate-limited: once a page resolves, the lookup skips
+ * it, so steady state costs only the loop.
+ */
+static void svc_pin_gr_range(struct nv_vgpu_device *vgpu,
+ struct svc_channel *ch, u64 ring_pa, int ring_ap)
+{
+ u64 nbytes = (u64)ch->gpfifo_entries * SVC_GPFIFO_ENTRY_SIZE;
+ u64 npages, i;
+ int pinned = 0;
+
+ if (!ring_pa || !ch->gpfifo_va)
+ return;
+ npages = DIV_ROUND_UP(nbytes, (u64)SEMA_PAGE_SIZE);
+ if (npages > 64)
+ npages = 64;
+ for (i = 0; i < npages; i++) {
+ u64 va = (ch->gpfifo_va + i * SEMA_PAGE_SIZE) &
+ ~(u64)(SEMA_PAGE_SIZE - 1);
+ u64 pa = 0;
+ int ap = SVC_APERTURE_SYS;
+
+ if (!svc_lookup(vgpu, va, &pa, &ap))
+ continue;
+ if (svc_walk_vaspaces(vgpu, va, &pa, &ap))
+ continue;
+ svc_pin_store(va, pa, ap);
+ svc_lat_store(va, pa, ap);
+ pinned++;
+ }
+ if (pinned)
+ pr_info_ratelimited("sema-chan: ring-range pin h=0x%x va=0x%llx pages=%d/%llu ring_pa=0x%llx ap=%d\n",
+ (u32)ch->h_object, ch->gpfifo_va, pinned,
+ npages, ring_pa, ring_ap);
+}
+
+static void svc_pin_ring_pushes(struct nv_vgpu_device *vgpu,
+ struct svc_channel *ch, u64 ring_pa, int ring_ap)
+{
+ u8 e[8];
+ int i, pinned = 0, pushes = 0;
+ static unsigned long last_log;
+
+ if (!gsp_sema_pin_pushes)
+ return;
+ for (i = 0; i < 32 && pinned < 64; i++) {
+ u32 e0, e1, len, off;
+ u64 va;
+
+ if (svc_read_va(vgpu, ch->gpfifo_va + (u64)i * 8,
+ e, sizeof(e)))
+ break;
+ memcpy(&e0, e, 4);
+ memcpy(&e1, e + 4, 4);
+ va = ((u64)(e1 & 0xff) << 32) | (u64)(e0 & 0xfffffffc);
+ len = ((e1 >> 10) & 0x1fffff) << 2;
+ if (!va || len < 4 || len > 0x40000)
+ break;
+ for (off = 0; off < len && pinned < 64; off += SEMA_PAGE_SIZE) {
+ u64 page = (va + off) & ~(u64)(SEMA_PAGE_SIZE - 1);
+ u64 wpa = 0;
+ int wap = SVC_APERTURE_SYS;
+
+ if (svc_walk_vaspaces(vgpu, page, &wpa, &wap))
+ break;
+ if (!off) {
+ u8 pre[64];
+ u32 plen = min_t(u32, len, sizeof(pre));
+
+ if (svc_read_ap(vgpu, wpa, wap, pre, plen) ||
+ !svc_method_stream_plausible(pre, plen))
+ break;
+ }
+ svc_pin_store(page, wpa, wap);
+ svc_lat_store(page, wpa, wap);
+ pinned++;
+ }
+ pushes++;
+ }
+ if (pinned && time_after(jiffies, last_log)) {
+ last_log = jiffies + HZ;
+ pr_info("sema-chan: ring-latch push pins h=0x%x ring_pa=0x%llx entries=%d pinned_pages=%d (VAS alive at latch)\n",
+ ch->h_object, ring_pa, pushes, pinned);
+ }
+}
+
static bool svc_gr_ram_ring_scan(struct nv_vgpu_device *vgpu,
struct svc_channel *ch, u64 *pa_out,
bool owner_filter, bool sparse)
@@ -31476,6 +31831,28 @@ static void svc_chan_vas_candidate_promote(struct nv_vgpu_device *vgpu,
h);
svc_vaspace_demote(h);
}
+ /*
+ * r1780 (V35): beef-family dead-directory demote. The guest
+ * frees/rebuilds the client VAS every few seconds; a verified
+ * entry whose page now reads all-zero cannot map anything and
+ * must yield to the freshest parked candidate. Cooldown 2 s
+ * keeps this from chasing its own tail.
+ */
+ if (beef && gsp_sema_vas_client_ring) {
+ static unsigned long beef_demote_at;
+ u64 rpdb = 0;
+ int rap = 0;
+
+ if (svc_vas_registered_pdb(h, &rpdb, &rap) &&
+ svc_pdb_reads_zero(vgpu, rpdb, rap) &&
+ time_after(jiffies, beef_demote_at)) {
+ beef_demote_at = jiffies +
+ msecs_to_jiffies(2000);
+ pr_info("sema-svc: VAS 0x%x verified directory pdb=0x%llx reads all-zero (guest rebuilt); demoting for re-validation\n",
+ h, rpdb);
+ svc_vaspace_demote(h);
+ }
+ }
}
/*
* r1773: bound the failed-sweep storm for the beef family (see the
@@ -32075,12 +32452,14 @@ static void svc_gr_ring_autopin(struct nv_vgpu_device *vgpu,
/* r831: keep a registered real-PA userd. */
if (ch->userd_reg) {
ch->userd_ok = true;
- } else {
+ } else if (ch->gpfifo_entries <= 1024) {
ch->userd_pa =
pin_pa + SVC_GR_USERD_OFF;
ch->userd_as = svc_ap_of(pin_ap);
ch->userd_ok = true;
}
+ /* V38: large ring needs the generic resolver
+ * (declared USERD or RAM scan), not slot data. */
svc_gr_ring_base = base;
if (svc_ap_of(pin_ap) == SVC_APERTURE_FB)
svc_fb_claim_add(ch->h_object, pin_pa,
@@ -32217,10 +32596,47 @@ static void svc_gr_ring_autopin(struct nv_vgpu_device *vgpu,
hva = ((u64)(h1 & 0xff) << 32) |
(u64)(h0 & 0xfffffffc);
hlen = ((h1 >> 10) & 0x1fffff) << 2;
- if (!hva || (hva & 0xfffff) ||
- hva < SEMA_VA_LO ||
- hva >= SEMA_VA_HI || hlen < 4 ||
- hlen > 0x10000)
+ /*
+ * r1780 (V33): the direct-GL
+ * client's ring lives in the
+ * LOW aperture (push VAs
+ * 0x4000000-class, e.g.
+ * 0x4000100/0x4000000) and
+ * its entries are not 1 MB
+ * aligned. The old high-family
+ * gate could never latch it, so
+ * the shadow scan never found
+ * the ring while the client's
+ * USERD was live (put=150 on
+ * r1780 c32). Accept the low
+ * family through the same
+ * entry-window test the RAM scan
+ * uses.
+ */
+ if (!hva || hlen < 4 ||
+ hlen > 0x100000)
+ continue;
+ if (!((hva >= SEMA_VA_LO &&
+ hva < SEMA_VA_HI &&
+ !(hva & 0xfffff)) ||
+ svc_gr_entry_win_ok(ch->gpfifo_va,
+ hva)))
+ continue;
+ /*
+ * r1780 (V39): the direct-GL ring's
+ * pushbuffer shares its low-aperture
+ * allocation window with the ring
+ * (measured: ring 0x4020000, pushes
+ * 0x4023000/0x4024004). A page-table
+ * page full of PTE-shaped dwords passed
+ * the broad entry-window test and was
+ * latched as the ring on c42
+ * (entry0 0xe107a20 len=49156). Require
+ * entry0 within 64 MiB of the ring VA;
+ * the real pushes are within 16 KiB.
+ */
+ if (hva + 0x4000000ULL < ch->gpfifo_va ||
+ hva > ch->gpfifo_va + 0x4000000ULL)
continue;
if (svc_ring_entry_dead(vgpu, h0, h1))
continue;
@@ -32276,9 +32692,14 @@ static void svc_gr_ring_autopin(struct nv_vgpu_device *vgpu,
hva = ((u64)(h1 & 0xff) << 32) |
(u64)(h0 & 0xfffffffc);
hlen = ((h1 >> 10) & 0x1fffff) << 2;
- if (!hva || (hva & 0xfffff) ||
- hva < SEMA_VA_LO || hva >= SEMA_VA_HI ||
- hlen < 4 || hlen > 0x10000)
+ /* r1780 (V33): accept the low-aperture
+ * direct-GL family too (see pass 1). */
+ if (!hva || hlen < 4 || hlen > 0x100000)
+ continue;
+ if (!((hva >= SEMA_VA_LO &&
+ hva < SEMA_VA_HI &&
+ !(hva & 0xfffff)) ||
+ svc_gr_entry_win_ok(ch->gpfifo_va, hva)))
continue;
if (svc_ring_entry_dead(vgpu, h0, h1))
continue;
@@ -32501,6 +32922,10 @@ skip_shadow_scan:
svc_vas_candidates_verify(vgpu, ch->gpfifo_va,
hit_pa);
svc_xvas_pdb_discover(vgpu, ch->gpfifo_va, hit_pa);
+ /* r1780 (V37): pin the pushes NOW, while the
+ * verified directory incarnation is still live. */
+ svc_pin_ring_pushes(vgpu, ch, hit_pa,
+ SVC_APERTURE_SYS);
/*
* Persist the latch immediately: a later stalled
* path in this pass may `continue` past the
@@ -43870,6 +44295,17 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu)
if (!rrc)
svc_pb_pair(vgpu, ch);
if (!rrc && ring_pa) {
+ /* V38: pin the whole ring while the translation
+ * is live, then the pushes it references. */
+ svc_pin_gr_range(vgpu, ch, ring_pa, ring_ap);
+ if (time_after(jiffies, svc_push_pin_at)) {
+ svc_push_pin_at = jiffies +
+ msecs_to_jiffies(500);
+ svc_pin_ring_pushes(vgpu, ch, ring_pa,
+ ring_ap);
+ }
+ }
+ if (!rrc && ring_pa) {
svc_lastgood_ring_va = ch->gpfifo_va;
svc_lastgood_ring_pa = ring_pa;
}
@@ -44232,10 +44668,18 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu)
if (!ch->gr_ram_userd_latch_at)
ch->gr_ram_userd_latch_at = jiffies;
- stepped = kfam ?
- put > ch->gr_ram_userd_latch_put :
- (put == ch->gr_ram_userd_latch_put + 1 ||
- put == ch->gr_ram_userd_latch_put + 2);
+ /*
+ * r1780 (V32): any forward PUT movement is progress.
+ * The old +1/+2 step test for non-kick channels
+ * rejected a block whose PUT advanced by more than
+ * two entries between pump passes (the X server's
+ * 2D stream does exactly that after a burst), which
+ * cleared userd_ok and produced an adopt/reject spin
+ * at ~10 Hz. A forward jump is not evidence of a
+ * stale latch: only a decrease (wrong actor / reused
+ * block) or the 15 s no-advance bound is.
+ */
+ stepped = put > ch->gr_ram_userd_latch_put;
if (stepped) {
ch->gr_ram_userd_confirmed = true;
pr_info("sema-chan: GR USERD h=0x%x pa=0x%llx speculative latch CONFIRMED (put %u -> %u)\n",
@@ -44708,6 +45152,11 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu)
ch->gpfifo_va +
idx * SVC_GPFIFO_ENTRY_SIZE,
entry, sizeof(entry))) {
+ if (gsp_sema_dbg)
+ pr_info_ratelimited("sema-chan: ring entry unreadable h=0x%x idx=%llu va=0x%llx ring_pa=0x%llx ap=%d\n",
+ (u32)ch->h_object, idx,
+ ch->gpfifo_va + idx * 8,
+ ring_pa, ring_ap);
svc.gpfifo_nomap++;
break;
}
@@ -45163,6 +45612,25 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu)
}
}
kfree(vb);
+ if (!ok &&
+ !ch->engine_type) {
+ u64 fbo = 0;
+
+ if (svc_fb_push_rescue(vgpu,
+ va, len,
+ pb, &fbo)) {
+ ok = true;
+ injected = false;
+ svc_pin_store(
+ va & ~(u64)(SEMA_PAGE_SIZE - 1),
+ fbo,
+ SVC_APERTURE_FB);
+ svc_lat_store(
+ va & ~(u64)(SEMA_PAGE_SIZE - 1),
+ fbo,
+ SVC_APERTURE_FB);
+ }
+ }
if (!ok)
unreadable = true;
} else {
diff --git a/kernel/gsp_sema_service.h b/kernel/gsp_sema_service.h
index 3f2a8dc..36f9902 100644
--- a/kernel/gsp_sema_service.h
+++ b/kernel/gsp_sema_service.h
@@ -180,6 +180,13 @@ int gsp_sema_service_vidmem_hash_snapshot(void);
int gsp_sema_service_vidmem_hash_diff(const char *tag,
unsigned int max_pages);
int gsp_sema_service_pdb_discover(u64 ring_va, u64 ring_pa);
+/*
+ * r1780 (V34): bounded page-directory scan for a content-verified
+ * {VA -> PA} pair (FB shadow first, then guest RAM up to ram_lim), and a
+ * direct VAS registration for a directory found out-of-band.
+ */
+int gsp_sema_service_pdb_scan(u64 va, u64 pa, u32 h_vas, u64 ram_lim);
+void gsp_sema_service_vas_force(u32 h_vas, u64 pdb, u32 entries, bool vid);
int gsp_sema_service_chan_rewind(u32 h_object);
int gsp_sema_service_counter_add(u32 h_object, u64 pa);
void gsp_sema_service_doorbell(u32 token);
diff --git a/kernel/main.c b/kernel/main.c
index e17840a..cb296f6 100644
--- a/kernel/main.c
+++ b/kernel/main.c
@@ -2637,6 +2637,87 @@ MODULE_PARM_DESC(sema_scrubbuf_add,
"Register the PMA scrubber finish/host-sema PAs (hex fin,hex hsem)");
/*
+ * r1780 (V34): bounded page-directory scan for a content-verified pair:
+ * echo 0x<va>,0x<pa>[,0x<h_vas>[,0x<ram_lim>]] > .../parameters/sema_pdb_scan
+ * Walks every 4 KB candidate root in the FB shadow (then optional guest
+ * RAM) for `va` and registers the root that reproduces `pa` under `h_vas`.
+ */
+static int nv_vgpu_sema_pdb_scan_set(const char *val,
+ const struct kernel_param *kp)
+{
+ char buf[80];
+ char *fields[4] = { NULL };
+ u64 v[4] = { 0 };
+ int nf = 0, i;
+ char *p;
+
+ strscpy(buf, val, sizeof(buf));
+ p = buf;
+ for (i = 0; i < 4 && p; i++) {
+ fields[nf++] = p;
+ p = strchr(p, ',');
+ if (p)
+ *p++ = '\0';
+ }
+ for (i = 0; i < nf; i++)
+ if (kstrtoull(fields[i], 0, &v[i]))
+ return -EINVAL;
+ if (nf < 2)
+ return -EINVAL;
+ return gsp_sema_service_pdb_scan(v[0], v[1],
+ nf > 2 ? (u32)v[2] : 0xbeef0300u,
+ nf > 3 ? v[3] : 0);
+}
+
+static const struct kernel_param_ops nv_vgpu_sema_pdb_scan_ops = {
+ .set = nv_vgpu_sema_pdb_scan_set,
+};
+
+module_param_cb(sema_pdb_scan, &nv_vgpu_sema_pdb_scan_ops, NULL, 0200);
+MODULE_PARM_DESC(sema_pdb_scan,
+ "Scan FB/RAM roots for a content-verified VA->PA pair and register the VAS");
+
+/*
+ * r1780 (V34): direct VAS registration:
+ * echo 0x<h_vas>,0x<pdb>[,<entries>[,<vidmem>]] > .../parameters/sema_vas_force
+ */
+static int nv_vgpu_sema_vas_force_set(const char *val,
+ const struct kernel_param *kp)
+{
+ char buf[80];
+ char *fields[4] = { NULL };
+ u64 v[4] = { 0 };
+ int nf = 0, i;
+ char *p;
+
+ strscpy(buf, val, sizeof(buf));
+ p = buf;
+ for (i = 0; i < 4 && p; i++) {
+ fields[nf++] = p;
+ p = strchr(p, ',');
+ if (p)
+ *p++ = '\0';
+ }
+ for (i = 0; i < nf; i++)
+ if (kstrtoull(fields[i], 0, &v[i]))
+ return -EINVAL;
+ if (nf < 2 || !v[0] || !v[1])
+ return -EINVAL;
+ gsp_sema_service_vas_force((u32)v[0], v[1],
+ nf > 2 ? (u32)v[2] : 4u,
+ nf > 3 ? !!v[3] : false);
+ return 0;
+}
+
+static const struct kernel_param_ops nv_vgpu_sema_vas_force_ops = {
+ .set = nv_vgpu_sema_vas_force_set,
+};
+
+module_param_cb(sema_vas_force, &nv_vgpu_sema_vas_force_ops, NULL, 0200);
+MODULE_PARM_DESC(sema_vas_force,
+ "Register a VAS directory out-of-band (h,pdb,entries,vidmem)");
+
+/*
* Debug: run the page-table walks for a GPU VA with per-level tracing,
* bypassing the pin/lat/pair map (see gsp_sema_service_walk_probe):
* echo 0x20400f000 > .../parameters/walk_probe
diff --git a/tools/guest-diag/cycle-r1780.sh b/tools/guest-diag/cycle-r1780.sh
new file mode 100755
index 0000000..23cf0d1
--- /dev/null
+++ b/tools/guest-diag/cycle-r1780.sh
@@ -0,0 +1,142 @@
+#!/bin/bash
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Open-IOV contributors
+# cycle-r1780.sh - durable vGPU cycle launcher for the r1780 campaign
+# (12 GiB file-backed guest RAM, kvmfr/kvmsr ivshmem, V31-class module).
+#
+# Usage: tools/guest-diag/cycle-r1780.sh [cycle-tag] [module.ko]
+# Env: INSOMOD_PARAMS override args file (default params-r1780.txt)
+set -euo pipefail
+
+HERE="$(cd "$(dirname "$0")" && pwd)"
+REPO="$(cd "$HERE/../.." && pwd)"
+TAG="${1:-c27}"
+MODULE="${2:-$REPO/kernel/vgsp.ko}"
+ARGV_FILE="${INSMOD_PARAMS:-$HERE/params-r1780.txt}"
+BACKUP="$REPO/backups/evidence-20261002-victory/r1780"
+DISK="${GUEST_DISK:-/home/rmcore/.local/libvf.io/kernel/linux.qcow2}"
+MDEV_PARENT="0000:01:00.0"
+MDEV_TYPE="nvidia-nvidia-vgpu-turing"
+RUNDIR="$HERE/run"
+QEMU_SCRIPT="$HERE/launch_qemu_r1780.run.sh"
+mkdir -p "$RUNDIR" "$BACKUP"
+
+echo "[cycle $TAG] stop QEMU"
+# Flush the guest before the forced teardown: a killed GL/X workload has
+# corrupted the ext4 root twice (c38 panic, c43 EIO), costing an offline fsck
+# cycle each time.
+if [ -S /tmp/qemu-guest-agent.sock ]; then
+ timeout 8 python3 - <<'PYEOF' >/dev/null 2>&1 || true
+import json, socket
+s = socket.socket(socket.AF_UNIX)
+s.settimeout(6)
+s.connect('/tmp/qemu-guest-agent.sock')
+for cmd in (["sync"], ["/bin/sh", "-c", "sync; echo 1 > /proc/sys/kernel/sysrq; echo o > /proc/sysrq-trigger"]):
+ try:
+ s.sendall(json.dumps({"execute": "guest-exec", "arguments": {"path": cmd[0], "arg": cmd[1:], "capture-output": True}}).encode() + b'\n')
+ s.recv(65536)
+ except Exception:
+ break
+s.close()
+PYEOF
+ sleep 2
+fi
+sudo pkill -f qemu-system-x86_64 2>/dev/null || true
+for i in $(seq 1 20); do pgrep -f qemu-system-x86_64 >/dev/null || break; sleep 0.5; done
+sudo pkill -9 -f qemu-system-x86_64 2>/dev/null || true
+sleep 1
+
+# Always repair the guest root after a forced teardown (cheap when clean,
+# prevents the recurring "guest-agent exec EIO / kill init" corruption class).
+DISK="/home/rmcore/.local/libvf.io/kernel/linux.qcow2"
+if [ -f "$DISK" ]; then
+ sudo modprobe nbd max_part=8 2>/dev/null || true
+ sudo qemu-nbd --disconnect /dev/nbd0 >/dev/null 2>&1 || true
+ if sudo qemu-nbd --connect=/dev/nbd0 "$DISK" -f qcow2 2>/dev/null; then
+ sleep 1
+ sudo e2fsck -fy /dev/nbd0p1 > "$RUNDIR/fsck-$TAG.log" 2>&1 || true
+ sudo qemu-nbd --disconnect /dev/nbd0 >/dev/null 2>&1 || true
+ fi
+fi
+
+echo "[cycle $TAG] remove mdevs"
+for m in /sys/bus/mdev/devices/*; do
+ [ -d "$m" ] && echo 1 | sudo tee "$m/remove" >/dev/null || true
+done
+
+echo "[cycle $TAG] unload vgsp"
+sudo rmmod vgsp 2>/dev/null || true
+for i in $(seq 1 20); do [ -d /sys/module/vgsp ] || break; sleep 0.5; done
+if [ -d /sys/module/vgsp ]; then
+ echo "FATAL: vgsp stuck loaded (see /proc/modules)"; exit 1
+fi
+
+echo "[cycle $TAG] load VFIO/mdev prerequisites"
+sudo modprobe vfio vfio_iommu_type1 vfio_pci mdev 2>/dev/null || true
+sudo modprobe vfio_iommu_type1 2>/dev/null || true
+
+echo "[cycle $TAG] stop nvidia-persistenced, power/control=on"
+sudo systemctl stop nvidia-persistenced 2>/dev/null || true
+echo on | sudo tee /sys/bus/pci/devices/0000:01:00.0/power/control >/dev/null
+echo on | sudo tee /sys/bus/pci/devices/0000:00:01.0/power/control >/dev/null 2>&1 || true
+
+echo "[cycle $TAG] insmod $(basename "$MODULE")"
+# shellcheck disable=SC2046
+sudo insmod "$MODULE" $(cat "$ARGV_FILE")
+
+U=$(uuidgen)
+echo "$U" | sudo tee "/sys/class/mdev_bus/$MDEV_PARENT/mdev_supported_types/$MDEV_TYPE/create" >/dev/null
+if [ ! -d "/sys/bus/mdev/devices/$U" ]; then echo "FATAL: mdev create failed"; exit 1; fi
+
+mkdir -p "/tmp/sockets/$U"
+sudo rm -f /tmp/qemu-qmp.sock /tmp/qemu-guest-agent.sock /tmp/qemu-harness.pid
+rm -f /dev/shm/vgpu-guest-ram 2>/dev/null || true
+touch /dev/shm/kvmfr-$U /dev/shm/kvmsr-$U 2>/dev/null || true
+chmod 660 /dev/shm/kvmfr-$U /dev/shm/kvmsr-$U 2>/dev/null || true
+
+QLOG="$BACKUP/${TAG}-qemu-session.txt"
+SL="$BACKUP/${TAG}-serial.log"
+cat > "$QEMU_SCRIPT" <<EOF
+#!/bin/bash
+exec /bin/qemu-system-x86_64 -D $QLOG -no-hpet \
+ -qmp unix:/tmp/qemu-qmp.sock,server,nowait \
+ -chardev socket,path=/tmp/qemu-guest-agent.sock,server=on,wait=off,id=qga0 \
+ -device virtio-serial-pci -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 \
+ -serial file:$SL -gdb tcp::1234 -display none \
+ -device ivshmem-plain,id=shmem0,memdev=ivshmem_kvmfr \
+ -object memory-backend-file,id=ivshmem_kvmfr,mem-path=/dev/shm/kvmfr-$U,size=128M,share=yes \
+ -device ivshmem-plain,id=shmem1,memdev=ivshmem_kvmsr \
+ -object memory-backend-file,id=ivshmem_kvmsr,mem-path=/dev/shm/kvmsr-$U,size=2M,share=yes \
+ -object memory-backend-file,id=pc.ram,size=12288M,mem-path=/dev/shm/vgpu-guest-ram,share=yes,prealloc=on \
+ -uuid $U -machine pc-q35-4.2,accel=kvm,usb=off,vmport=off,dump-guest-core=off,memory-backend=pc.ram \
+ -cpu host,ss=on,vmx=on,pcid=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaveopt=on,pdpe1gb=on,ibpb=on,amd-stibp=on,amd-ssbd=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,hv-vapic,hv_time,hv-spinlocks=0x1fff,hv-vendor-id=null,kvm=on,topoext=on \
+ -rtc clock=host,base=localtime -m 12288 -smp cores=4,threads=1,sockets=1 \
+ -hda $DISK --enable-kvm \
+ -device pcie-root-port,id=rp0,chassis=1,slot=30,bus=pcie.0 \
+ -device vfio-pci,id=hostdev0,bus=rp0,sysfsdev=/sys/bus/mdev/devices/$U,display=off \
+ -device rtl8139,netdev=net0 -netdev user,id=net0,hostfwd=tcp::2222-:22 \
+ -net user -net nic -device vhost-vsock-pci,guest-cid=85770 \
+ -daemonize -pidfile /tmp/qemu-harness.pid
+EOF
+chmod +x "$QEMU_SCRIPT"
+
+echo "[cycle $TAG] launch QEMU (mdev $U)"
+sudo bash "$QEMU_SCRIPT" > "$RUNDIR/qemu-$TAG.log" 2>&1
+sleep 3
+if ! pgrep -f qemu-system-x86_64 >/dev/null; then
+ echo "FATAL: QEMU failed"; tail -5 "$RUNDIR/qemu-$TAG.log"; exit 1
+fi
+
+echo "[cycle $TAG] mdev $U"
+echo "$U" > "$RUNDIR/current-mdev"
+echo "$TAG" > "$RUNDIR/current-tag"
+
+for i in $(seq 1 90); do
+ if timeout 5 sudo python3 "$REPO/scripts/vgpu_vm.py" exec "echo CYCLE_UP" 2>/dev/null | grep -q CYCLE_UP; then
+ echo "[cycle $TAG] guest agent up"
+ exit 0
+ fi
+ sleep 2
+done
+echo "WARN: guest agent not reachable yet (serial: $SL)"
+exit 0
diff --git a/tools/guest-diag/gzf2_video.py b/tools/guest-diag/gzf2_video.py
new file mode 100644
index 0000000..aa88aa0
--- /dev/null
+++ b/tools/guest-diag/gzf2_video.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# gzf2_video.py - turn a GZCAPV01/GZF2 guest capture into a constant-rate mp4.
+#
+# Reads the append-only frame stream written by sdl_cap.c (LD_PRELOAD
+# SDL_GL_SwapWindow shim) or xgrab.c, resamples by CLOCK_MONOTONIC timestamps
+# (nearest frame), streams raw pixels straight into ffmpeg (no PPM
+# intermediates), and validates the encoded file with ffprobe.
+#
+# Format: [0..8) = "GZCAPV01", zero pad to 4096; then records:
+# 4B "GZF2" | u32 w | u32 h | u32 stride | u32 fmt | u64 ts_ns | u64 seq |
+# stride*h bytes, top-down. fmt 32 = RGBA8 (GL swap path), 33 = BGRA8
+# (SDL_RenderReadPixels ARGB8888 / X surface). A trailing
+# "GZF2" + u64 0xffffffffffffffff marks a clean stop.
+#
+# Usage:
+# gzf2_video.py CAPTURE OUT.mp4 [--fps 60] [--skip 5] [--duration 20]
+# [--encoder libx264|nvenc] [--crf 18]
+# [--pix-fmt rgba|bgra] [--info-only]
+import argparse
+import bisect
+import hashlib
+import json
+import os
+import struct
+import subprocess
+import sys
+
+MAGIC = b"GZCAPV01"
+SENTINEL = b"\xff" * 8
+
+
+def die(msg):
+ print(f"GZF2-ERROR {msg}", file=sys.stderr)
+ sys.exit(2)
+
+
+def index_capture(path):
+ size = os.path.getsize(path)
+ recs, clean, corrupt = [], False, 0
+ with open(path, "rb") as f:
+ hdr = f.read(4096)
+ if len(hdr) < 4096 or hdr[:8] != MAGIC:
+ die(f"{path}: not a GZCAPV01 file")
+ off = 4096
+ while off + 12 <= size:
+ f.seek(off)
+ head = f.read(36)
+ if len(head) < 12 or head[:4] != b"GZF2":
+ break
+ if head[4:12] == SENTINEL:
+ clean = True
+ break
+ if len(head) < 36:
+ break
+ w, h, stride, fmt = struct.unpack_from("<IIII", head, 4)
+ ts, seq = struct.unpack_from("<QQ", head, 20)
+ if not (0 < w <= 8192 and 0 < h <= 8192
+ and 0 < stride <= 4 * 8192) or seq == 0xFFFFFFFFFFFFFFFF:
+ corrupt += 1
+ break
+ n = stride * h
+ if off + 36 + n > size:
+ corrupt += 1
+ break
+ f.seek(off + 36)
+ px = f.read(n)
+ if len(px) != n:
+ corrupt += 1
+ break
+ recs.append({
+ "off": off + 36, "ts": ts, "w": w, "h": h,
+ "stride": stride, "fmt": fmt, "size": n,
+ "md5": hashlib.md5(px).hexdigest(),
+ })
+ off += 36 + n
+ if not recs:
+ die(f"{path}: header only (no frames captured)")
+ return recs, clean, corrupt
+
+
+def sampled_black(px):
+ s = px[:65536]
+ return sum(s[0::4]) + sum(s[1::4]) + sum(s[2::4]) == 0
+
+
+def pick_frames(recs, fps, skip_s, duration_s):
+ t0 = recs[0]["ts"] + int(skip_s * 1e9)
+ cand = [r for r in recs if r["ts"] >= t0] or recs
+ ts = [r["ts"] for r in cand]
+ span = (ts[-1] - ts[0]) / 1e9
+ n = int(duration_s * fps) if duration_s else max(1, int(span * fps))
+ picks, held = [], 0
+ for i in range(n):
+ want = ts[0] + int(i * 1e9 / fps)
+ j = bisect.bisect_left(ts, want)
+ if j == 0:
+ k = 0
+ elif j >= len(cand):
+ k, held = len(cand) - 1, held + 1
+ else:
+ k = j if ts[j] - want < want - ts[j - 1] else j - 1
+ picks.append(cand[k])
+ return picks, span, held
+
+
+def encoder_args(name, crf):
+ if name == "nvenc":
+ return ["-c:v", "h264_nvenc", "-preset", "p6", "-rc", "vbr",
+ "-cq", str(crf), "-b:v", "12M"]
+ return ["-c:v", "libx264", "-preset", "slow", "-crf", str(crf)]
+
+
+def encode(cap, picks, args, encoder):
+ first = picks[0]
+ w, h, fmt = first["w"], first["h"], args.pix_fmt
+ if any(p["w"] != w or p["h"] != h for p in picks):
+ die("geometry changes inside the capture; resize before recording")
+ if args.pix_fmt == "auto":
+ fmt = "bgra" if first["fmt"] == 33 else "rgba"
+ cmd = ["ffmpeg", "-hide_banner", "-v", "error", "-y",
+ "-f", "rawvideo", "-pixel_format", fmt,
+ "-video_size", f"{w}x{h}", "-framerate", str(args.fps),
+ "-i", "pipe:0", "-an", *encoder_args(encoder, args.crf),
+ "-frames:v", str(len(picks)), "-pix_fmt", "yuv420p",
+ "-movflags", "+faststart", args.out]
+ print(f"GZF2-ENCODE {encoder} {w}x{h} {fmt} {len(picks)}f @ {args.fps}fps")
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
+ last, last_px = None, None
+ try:
+ with open(cap, "rb") as f:
+ for p in picks:
+ if p["off"] != last:
+ f.seek(p["off"])
+ last_px = f.read(p["size"])
+ if len(last_px) != p["size"]:
+ raise IOError(f"short read at {p['off']}")
+ last = p["off"]
+ if p["stride"] == w * 4:
+ proc.stdin.write(last_px)
+ else:
+ row = w * 4
+ proc.stdin.write(b"".join(
+ last_px[y * p["stride"]:y * p["stride"] + row]
+ for y in range(h)))
+ except (BrokenPipeError, IOError) as e:
+ print(f"GZF2-ENCODE write failed: {e}", file=sys.stderr)
+ finally:
+ if proc.stdin:
+ try:
+ proc.stdin.close()
+ except BrokenPipeError:
+ pass
+ return proc.wait()
+
+
+def ffprobe(path):
+ r = subprocess.run(
+ ["ffprobe", "-v", "error", "-count_frames", "-select_streams", "v:0",
+ "-show_entries",
+ "stream=codec_name,width,height,r_frame_rate,nb_read_frames",
+ "-show_entries", "format=duration,size",
+ "-of", "json", path], capture_output=True, text=True)
+ if r.returncode:
+ die(f"ffprobe failed: {r.stderr.strip()}")
+ return json.loads(r.stdout)
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("capture")
+ ap.add_argument("out")
+ ap.add_argument("--fps", type=int, default=60)
+ ap.add_argument("--skip", type=float, default=0.0,
+ help="seconds of menu/load to drop from the start")
+ ap.add_argument("--duration", type=float, default=0.0,
+ help="output seconds (0 = whole remaining span)")
+ ap.add_argument("--encoder", choices=["libx264", "nvenc"],
+ default="libx264")
+ ap.add_argument("--crf", type=int, default=18)
+ ap.add_argument("--pix-fmt", choices=["auto", "rgba", "bgra"],
+ default="auto", help="auto uses record fmt (33=BGRA)")
+ ap.add_argument("--info-only", action="store_true")
+ args = ap.parse_args()
+
+ recs, clean, corrupt = index_capture(args.capture)
+ uniq = len({r["md5"] for r in recs})
+ span = (recs[-1]["ts"] - recs[0]["ts"]) / 1e9
+ rate = len(recs) / span if span else 0.0
+ blacks = 0
+ with open(args.capture, "rb") as f:
+ for i, r in enumerate(recs):
+ if i % 60 == 0:
+ f.seek(r["off"])
+ blacks += sampled_black(f.read(min(r["size"], 65536)))
+ print(f"GZF2-CAPTURE frames={len(recs)} span={span:.2f}s rate={rate:.1f}/s "
+ f"unique={uniq} ({100.0 * uniq / len(recs):.1f}%) "
+ f"black_sampled={blacks} clean_end={clean} corrupt={corrupt} "
+ f"size={os.path.getsize(args.capture)}B "
+ f"geom={recs[0]['w']}x{recs[0]['h']} fmt={recs[0]['fmt']}")
+ if args.info_only:
+ return
+ picks, span, held = pick_frames(recs, args.fps, args.skip,
+ args.duration or None)
+ md5_by_off = {r["off"]: r["md5"] for r in recs}
+ pu = len({md5_by_off[p["off"]] for p in picks})
+ print(f"GZF2-RESAMPLE out={len(picks)}f span={span:.2f}s "
+ f"unique={pu}/{len(picks)} ({100.0 * pu / len(picks):.1f}%) "
+ f"held_last={held}")
+ if pu < len(picks) // 10:
+ print("GZF2-WARN mostly static/black output", file=sys.stderr)
+
+ rc = encode(args.capture, picks, args, args.encoder)
+ if rc != 0 and args.encoder == "nvenc":
+ print("GZF2-WARN nvenc failed; retrying libx264", file=sys.stderr)
+ rc = encode(args.capture, picks, args, "libx264")
+ if rc != 0:
+ die(f"ffmpeg exited {rc}")
+ info = ffprobe(args.out)
+ st = info["streams"][0]
+ print("GZF2-OUTPUT " + " ".join(
+ f"{k}={st.get(k)}" for k in
+ ("codec_name", "width", "height", "r_frame_rate", "nb_read_frames")))
+ print(f"GZF2-OUTPUT duration={info['format'].get('duration')} "
+ f"size={info['format'].get('size')}")
+ if int(st.get("nb_read_frames", 0)) != len(picks):
+ die("encoded frame count mismatch")
+ print("GZF2-OK", args.out)
+
+
+if __name__ == "__main__":
+ main() \ No newline at end of file
diff --git a/tools/guest-diag/params-r1780.txt b/tools/guest-diag/params-r1780.txt
new file mode 100644
index 0000000..70985c7
--- /dev/null
+++ b/tools/guest-diag/params-r1780.txt
@@ -0,0 +1,2 @@
+gsp_phys_gpu=1 gsp_phys_hazard_ack=1 gsp_phys_boot=1 gsp_phys_nocat=1 gsp_phys_fw=nvidia/tu102/gsp/gsp-570.144.bin gsp_phys_guest_mem=1 gsp_phys_gr_forward=1 cuda_bridge=0 gsp_phys_rmcall_engine=1 gsp_phys_rmcall_golden=1 gsp_phys_rmcall_gr_promote=35 gsp_phys_rmcall_fwclient_mask=0xE3F gsp_phys_rmcall_vas_shared=1 gsp_phys_gr_nowrite_retries=0 gsp_phys_gr_retry_fast=1 gsp_phys_gr_first_poll_ms=2500 gsp_sema_winfill=1 gsp_sema_gr_swrel_gate=1 gsp_phys_rmcall_srv_ctxcommit=0 gsp_phys_rmcall_gr_qmdwin=1
+sema_dbg=1 sema_fake_rel=0