diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-11-12 14:55:25 -0500 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-11-12 14:55:25 -0500 |
| commit | 10c043a669fed5340eb953a17e341a35a4bce1b5 (patch) | |
| tree | b4fc5a197bd4ce7518ffe5356d420a0e9809caab | |
| parent | 6260daa1eb66094e86a1d02128b97708242074e8 (diff) | |
| download | open-iov-10c043a669fed5340eb953a17e341a35a4bce1b5.tar.gz open-iov-10c043a669fed5340eb953a17e341a35a4bce1b5.zip | |
vgsp: direct-GL ring/push rescue, VAS/pdb scanners and PTIMER tick; r1780 cycle tooling
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | kernel/gsp_emulation.c | 35 | ||||
| -rw-r--r-- | kernel/gsp_emulation.h | 8 | ||||
| -rw-r--r-- | kernel/gsp_sema_service.c | 530 | ||||
| -rw-r--r-- | kernel/gsp_sema_service.h | 7 | ||||
| -rw-r--r-- | kernel/main.c | 81 | ||||
| -rwxr-xr-x | tools/guest-diag/cycle-r1780.sh | 142 | ||||
| -rw-r--r-- | tools/guest-diag/gzf2_video.py | 232 | ||||
| -rw-r--r-- | tools/guest-diag/params-r1780.txt | 2 |
9 files changed, 1010 insertions, 31 deletions
@@ -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 |
