summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-01 10:13:04 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-01 10:13:04 -0400
commit582696da869374455c3bcdaf63cbcd28352da14e (patch)
treece0832a6c1caf050d3fdb4706d13a738e3fbbdcc
parentb589a88ff559833a80ff60c86d1e2c823bd66839 (diff)
downloadopen-iov-582696da869374455c3bcdaf63cbcd28352da14e.tar.gz
open-iov-582696da869374455c3bcdaf63cbcd28352da14e.zip
gsp-phys: command-queue TX and the GSP-RM unload RPC for clean teardown
Adds a minimal msgq producer on the command queue (element layout, checksum, sequence, dma_wmb before writePtr, doorbell) and submits the UNLOADING_GUEST_DRIVER RPC so GSP-RM shuts down and sets the scratch14 handoff bit the Booter Unload gate needs. Release attempts the RPC once before quiescing when a boot ran; on-demand testing is available through the debugfs file gsp-phys-unload-rpc while the module owns the GPU. GSP-RM processor suspend is polled on the GSP mailbox.
-rw-r--r--docs/03-architecture/08-physical-gpu-integration.md38
-rw-r--r--kernel/gsp_phys_booter.c433
-rw-r--r--kernel/gsp_phys_booter.h40
-rw-r--r--kernel/gsp_phys_gpu.c9
-rw-r--r--kernel/main.c8
5 files changed, 519 insertions, 9 deletions
diff --git a/docs/03-architecture/08-physical-gpu-integration.md b/docs/03-architecture/08-physical-gpu-integration.md
index 0c18e84..f4dfa68 100644
--- a/docs/03-architecture/08-physical-gpu-integration.md
+++ b/docs/03-architecture/08-physical-gpu-integration.md
@@ -202,12 +202,14 @@ event before it raises `GSP_INIT_DONE` (0x1001). **A plain release is not yet
possible:** the Booter Unload gate requires all four secure scratch14 handoff
bits (24–27); a
host-driven boot sets only bit 24, and bit 27 is set by the server's shutdown
-path (host RPC `UNLOADING_GUEST_DRIVER`), which is not implemented, while the
+path (host RPC `UNLOADING_GUEST_DRIVER`; implemented in the module as the
+rung-4b command-queue TX + shutdown path and pending its first live run),
+while the
host-side seed/unlock alternative is fuse-locked on this part. Until that RPC
-lands, a green session is left claimed with the harness `--keep` flag and
-returning the GPU to the distribution driver takes a power cycle. The
-supervised harness modes are `--boot` (the only write mode: full boot
-lifecycle), `--boot-dryrun` (zero-write staging rehearsal) and `--keep`
+has run in a live session, a green session is left claimed with the harness
+`--keep` flag and returning the GPU to the distribution driver takes a power
+cycle. The supervised harness modes are `--boot` (the only write mode: full
+boot lifecycle), `--boot-dryrun` (zero-write staging rehearsal) and `--keep`
(leave a verified claimed session running); `--fwsec` is retired.
### 4.4 Host-side resource-manager client
@@ -321,10 +323,28 @@ per-context scheduling. Tenant policy and fairness are a later layer on top.
executing the sequencer buffer carried in that event. Clean release is not
yet possible: the Booter Unload gate requires all four secure scratch14
handoff bits (24–27), a host-driven boot sets only bit 24, bit 27 is set by
- the server's shutdown path (host RPC `UNLOADING_GUEST_DRIVER`, not
- implemented), and host-side seed/unlock is fuse-locked on this part. The
- live session is therefore left claimed (`--keep`) and a fresh cycle needs a
- power cycle. See §4.3.
+ the server's shutdown path (host RPC `UNLOADING_GUEST_DRIVER`), and
+ host-side seed/unlock is fuse-locked on this part. The live session is
+ therefore left claimed (`--keep`) and a fresh cycle needs a power cycle.
+ See §4.3.
+* **Update (2026-10-01) — rung 4b implemented (build-only):** the module now
+ produces real command-queue records to GSP-RM and attempts a clean shutdown
+ on release. The producer writes the 48-byte transport element header plus
+ the 32-byte rpc header (signature `0x43505256`, header version `0x03000000`,
+ `rpc.length = 32 + payload`), an XOR-fold 64→32 checksum over the
+ zero-padded record that makes a consumer's fold read zero, an incrementing
+ sequence number, and publishes the wrap-around `writePtr` under a DMA write
+ barrier before ringing the queue-head doorbell at `0x110c00`. The shutdown
+ RPC is `UNLOADING_GUEST_DRIVER` (function 47) with `bInPMTransition=0`,
+ `bGc6Entering=0`, `newLevel=0`; the wait criterion is the GSP falcon
+ mailbox0 reading `0x80000000` (processor-suspended), bounded at 10 s. On a
+ release the RPC runs once (only when the full boot stage ran) before the
+ existing quiesce/FWSEC teardown; without a full boot the fail-safe
+ behaviour is unchanged. A write-only debugfs trigger
+ (`open-iov-vfio/gsp-phys-unload-rpc`, write "1") lets a live claimed
+ session test the shutdown without touching WPR2. All of it is inert unless
+ the full boot stage ran; the code has not yet run live, and this bullet
+ must be updated with the live outcome.
## 9. Interaction sequence (end state)
diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c
index d50a4ad..fe52a96 100644
--- a/kernel/gsp_phys_booter.c
+++ b/kernel/gsp_phys_booter.c
@@ -34,6 +34,9 @@
#include <linux/dma-mapping.h>
#include <linux/slab.h>
#include <linux/string.h>
+#include <linux/fs.h>
+#include <linux/debugfs.h>
+#include <linux/uaccess.h>
#include <linux/jiffies.h>
#include <linux/byteorder/generic.h>
#include <asm/barrier.h>
@@ -42,6 +45,7 @@
#include "gsp_phys_booter.h"
#include "gsp_phys_falcon.h"
#include "gsp_phys_fwsec.h"
+#include "gsp_phys_gpu.h"
#include "gsp_phys_booter_load_img.h"
#include "gsp_phys_gsp_rm_boot_img.h"
@@ -138,6 +142,43 @@ bool gsp_phys_boot_requested(void)
#define GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF \
(GSP_PHYS_MSG_RPC_HEADER_OFF + GSP_PHYS_MSG_RPC_HEADER_SIZE)
#define GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF_LEGACY 68
+#define GSP_PHYS_MSG_PAYLOAD_MAX \
+ (GSP_PHYS_MSG_SIZE - GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF)
+
+/*
+ * Command-queue TX wire constants (rpc_headers.h, rpc_global_enums.h,
+ * rpc_common.c, message_queue_priv.h):
+ * - rpc.header_version = MAJOR_TOT(3) << 24 | MINOR_TOT(0) << 16;
+ * - rpc.signature = NV_VGPU_MSG_SIGNATURE_VALID;
+ * - rpc.length = sizeof(rpc_message_header_v=32) + payload (so the element
+ * record is 48 + rpc.length bytes and the checksum covers exactly that);
+ * - rpc_result/rpc_result_private = NV_VGPU_MSG_RESULT_RPC_PENDING while in
+ * flight, rpc.sequence = 0 on the GSP client path (rpcWriteCommonHeader
+ * zeroes the buffer and does not set it).
+ */
+#define GSP_PHYS_RPC_HEADER_VERSION 0x03000000u
+#define GSP_PHYS_RPC_SIGNATURE 0x43505256u
+#define GSP_PHYS_RPC_RESULT_PENDING 0xffffffffu
+#define GSP_PHYS_RPC_FUNC_UNLOADING_GUEST_DRIVER 47u
+
+/*
+ * GSP command-queue doorbell. After publishing writePtr the reference
+ * producer rings NV_PGSP_QUEUE_HEAD(RPC_TASK_RM_QUEUE_IDX=0) = 0x110c00
+ * (kgspSetCmdQueueHead_TU102); the only caller passes value 0.
+ */
+#define GSP_PHYS_QUEUE_HEAD_REG 0x00110c00
+#define GSP_PHYS_QUEUE_HEAD_IDX 0
+
+/*
+ * GSP-RM suspend criterion after UNLOADING_GUEST_DRIVER
+ * (kgspWaitForProcessorSuspend_TU102 -> _kgspIsProcessorSuspended): the GSP
+ * falcon MAILBOX0 reads LIBOS_INTERRUPT_PROCESSOR_SUSPENDED. Bounded here to
+ * 10 s (the reference uses the platform thread-state timeout via
+ * gpuTimeoutCondWait).
+ */
+#define GSP_PHYS_LIBOS_PROCESSOR_SUSPENDED 0x80000000u
+#define GSP_PHYS_RM_UNLOAD_TIMEOUT_MS 10000
+#define GSP_PHYS_RM_UNLOAD_POLL_MS 10
/*
* CPU-sequencer execution (rmgspseq.h and kernel_gsp.c in the reference).
@@ -269,6 +310,21 @@ struct gsp_phys_seq_request {
__le32 reg_save_area[GSP_PHYS_SEQ_REG_SAVE_SIZE];
};
+/*
+ * rpc_unloading_guest_driver_v1F_07 (g_rpc-structures.h): two NvBool (NvU8)
+ * flags then the level, 8 bytes with the natural 2-byte tail padding. A
+ * normal unload sends all zeroes (kgspUnloadRm_IMPL with
+ * KGSP_UNLOAD_MODE_NORMAL); newLevel 0 / NV_FALSE / NV_FALSE is the same
+ * payload the distribution driver's gpu.c unload path sends for
+ * GPU_STATE_FLAGS_FAST_UNLOAD.
+ */
+struct gsp_phys_rpc_unloading_guest_driver {
+ u8 b_in_pm_transition;
+ u8 b_gc6_entering;
+ u8 pad[2];
+ __le32 new_level;
+};
+
/* GSP_SEQ_BUF_OPCODE (rmgspseq.h): opcode dword then its payload dwords. */
enum gsp_phys_seq_opcode {
GSP_PHYS_SEQ_OP_REG_WRITE = 0,
@@ -309,6 +365,8 @@ static struct gsp_phys_boot_stage {
bool ran; /* stage 3d reached the device sequence */
bool riscv_active;
bool dryrun;
+ bool rm_unload_submitted; /* UNLOADING_GUEST_DRIVER was published */
+ u32 tx_seq_num; /* next command-queue element seqNum */
struct gsp_phys_boot_chunk fmc;
struct gsp_phys_radix3 radix3;
struct gsp_phys_boot_chunk signature;
@@ -1645,6 +1703,369 @@ void gsp_phys_boot_rmwait(struct gsp_phys_boot *boot, void __iomem *bar0,
}
}
+/* ============================================================
+ * Command-queue TX: host -> GSP-RM msgq producer
+ * ============================================================ */
+
+/*
+ * XOR-fold checksum (message_queue_priv.h _checkSum32): XOR every 8-byte
+ * little-endian word of the record, fold the 64-bit result to 32 bits. The
+ * producer zeroes the checkSum field first and stores this value, so a
+ * consumer XORing the whole record (checkSum included) gets 0; the caller
+ * guarantees len is 8-byte aligned. GSP-RM's receive path rejects a record
+ * whose folded value is nonzero.
+ */
+static u32 gsp_phys_msgq_checksum(const u8 *data, u32 len)
+{
+ u64 sum = 0;
+ u32 off;
+
+ for (off = 0; off < len; off += 8)
+ sum ^= get_unaligned_le64(data + off);
+ return upper_32_bits(sum) ^ lower_32_bits(sum);
+}
+
+/*
+ * Submit one command-queue record (single element). Validates the staged
+ * command-queue TX header (the one GSP-RM linked against), logs it together
+ * with the GSP-RM command readPtr and the status TX header, builds the
+ * element in a zeroed staging buffer, copies it into the next slot and
+ * publishes writePtr under dma_wmb() before ringing the queue-head doorbell.
+ * Bounded: one slot, payload <= one element.
+ *
+ * Pointer placement note (msgq.c MSGQ_FLAGS_SWAP_RX, both sides flags=1):
+ * the GSP-RM read pointer for the command queue lives at
+ * status + (status tx header rxHdrOff), exactly where the emulated GSP-RM
+ * publishes its cmd_read (gsp_rm_poll_commands), not at cmd + rxHdrOff.
+ */
+int gsp_phys_queue_tx_submit(void __iomem *bar0, u32 function,
+ const void *payload, u32 payload_len)
+{
+ const struct gsp_phys_boot_chunk *q = &boot_stage.queue;
+ const struct gsp_phys_msgq_tx_header *cmd_hdr;
+ const struct gsp_phys_msgq_tx_header *stat_hdr;
+ struct gsp_phys_msgq_element *e;
+ __le32 *write_ptr;
+ u8 *elem, *slot_va;
+ u32 version, size, msg_size, msg_count, wp, entry_off, rx_hdr_off;
+ u32 stat_version, stat_msg_size, stat_msg_count, stat_entry_off;
+ u32 stat_rx_hdr_off, stat_wp, cmd_read = 0;
+ u32 msg_len, aligned_len, elem_count, checksum, new_wp, slot;
+ bool stat_linked;
+
+ if (!bar0 || !boot_stage.primed || !q->pages) {
+ pr_err("gsp-phys-boot: TX refused: command-queue staging is not primed\n");
+ return -EINVAL;
+ }
+ if (payload_len > GSP_PHYS_MSG_PAYLOAD_MAX) {
+ pr_err("gsp-phys-boot: TX refused: payload %u exceeds %u bytes\n",
+ payload_len, GSP_PHYS_MSG_PAYLOAD_MAX);
+ return -EINVAL;
+ }
+
+ msg_len = GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF + payload_len;
+ aligned_len = (msg_len + 7u) & ~7u;
+ elem_count = DIV_ROUND_UP(msg_len, GSP_PHYS_MSG_SIZE);
+ if (elem_count != 1) {
+ pr_err("gsp-phys-boot: TX refused: message spans %u elements\n",
+ elem_count);
+ return -EINVAL;
+ }
+
+ cmd_hdr = gsp_phys_chunk_page_va(q,
+ GSP_PHYS_QUEUE_CMD_OFF / GSP_PHYS_PAGE_SIZE);
+ stat_hdr = gsp_phys_chunk_page_va(q,
+ GSP_PHYS_QUEUE_STAT_OFF / GSP_PHYS_PAGE_SIZE);
+ version = le32_to_cpu(READ_ONCE(cmd_hdr->version));
+ size = le32_to_cpu(READ_ONCE(cmd_hdr->size));
+ msg_size = le32_to_cpu(READ_ONCE(cmd_hdr->msg_size));
+ msg_count = le32_to_cpu(READ_ONCE(cmd_hdr->msg_count));
+ wp = le32_to_cpu(READ_ONCE(cmd_hdr->write_ptr));
+ rx_hdr_off = le32_to_cpu(READ_ONCE(cmd_hdr->rx_hdr_off));
+ entry_off = le32_to_cpu(READ_ONCE(cmd_hdr->entry_off));
+
+ if (version != 0 || size != GSP_PHYS_QUEUE_CMD_SIZE ||
+ msg_size != GSP_PHYS_MSG_SIZE ||
+ msg_count != (GSP_PHYS_QUEUE_CMD_SIZE - GSP_PHYS_PAGE_SIZE) /
+ GSP_PHYS_MSG_SIZE ||
+ entry_off != GSP_PHYS_PAGE_SIZE ||
+ rx_hdr_off < GSP_PHYS_MSGQ_TX_WRITEPTR_OFF + 4 ||
+ rx_hdr_off + 4 > entry_off || wp >= msg_count) {
+ pr_err("gsp-phys-boot: TX refused: command-queue TX header mismatch (version=%u size=0x%x msgSize=%u msgCount=%u entryOff=0x%x rxHdrOff=0x%x writePtr=%u); GSP-RM did not link the staged header\n",
+ version, size, msg_size, msg_count, entry_off, rx_hdr_off,
+ wp);
+ return -EIO;
+ }
+
+ /*
+ * Status TX header (GSP-RM's producer header) and the GSP-RM command
+ * readPtr at status+rxHdrOff (MSGQ_FLAGS_SWAP_RX). Best-effort: the
+ * reference allows TX before its RX link, so a not-yet-linked status
+ * header is logged as a warning, not a refusal.
+ */
+ stat_version = le32_to_cpu(READ_ONCE(stat_hdr->version));
+ stat_msg_size = le32_to_cpu(READ_ONCE(stat_hdr->msg_size));
+ stat_msg_count = le32_to_cpu(READ_ONCE(stat_hdr->msg_count));
+ stat_entry_off = le32_to_cpu(READ_ONCE(stat_hdr->entry_off));
+ stat_rx_hdr_off = le32_to_cpu(READ_ONCE(stat_hdr->rx_hdr_off));
+ stat_wp = le32_to_cpu(READ_ONCE(stat_hdr->write_ptr));
+ stat_linked = stat_version == 0 &&
+ stat_msg_size == GSP_PHYS_MSG_SIZE &&
+ stat_msg_count != 0 &&
+ stat_entry_off + (u64)stat_msg_count * stat_msg_size <=
+ GSP_PHYS_QUEUE_STAT_SIZE &&
+ stat_rx_hdr_off + 4 <= stat_entry_off;
+ if (stat_linked)
+ cmd_read = le32_to_cpu(READ_ONCE(*(const __le32 *)
+ ((const u8 *)stat_hdr + stat_rx_hdr_off)));
+
+ pr_info("gsp-phys-boot: TX preflight: cmd hdr v%u size=0x%x msgSize=%u msgCount=%u entryOff=0x%x rxHdrOff=0x%x writePtr(slot)=%u; GSP-RM cmd readPtr=%u (status+0x%x); status hdr v%u writePtr=%u msgCount=%u %s\n",
+ version, size, msg_size, msg_count, entry_off, rx_hdr_off, wp,
+ cmd_read, stat_rx_hdr_off, stat_version, stat_wp,
+ stat_msg_count,
+ stat_linked ? "(status TX linked)" :
+ "(WARNING: status TX header not linked)");
+
+ elem = kmalloc(GSP_PHYS_MSG_SIZE, GFP_KERNEL);
+ if (!elem)
+ return -ENOMEM;
+ memset(elem, 0, GSP_PHYS_MSG_SIZE);
+
+ e = (struct gsp_phys_msgq_element *)elem;
+ e->seq_num = cpu_to_le32(boot_stage.tx_seq_num);
+ e->elem_count = cpu_to_le32(elem_count);
+ e->rpc_header_version = cpu_to_le32(GSP_PHYS_RPC_HEADER_VERSION);
+ e->rpc_signature = cpu_to_le32(GSP_PHYS_RPC_SIGNATURE);
+ e->rpc_length = cpu_to_le32(GSP_PHYS_MSG_RPC_HEADER_SIZE + payload_len);
+ e->rpc_function = cpu_to_le32(function);
+ e->rpc_result = cpu_to_le32(GSP_PHYS_RPC_RESULT_PENDING);
+ e->rpc_result_private = cpu_to_le32(GSP_PHYS_RPC_RESULT_PENDING);
+ e->rpc_sequence = cpu_to_le32(0);
+ e->rpc_union = cpu_to_le32(0);
+ if (payload_len)
+ memcpy(elem + GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF, payload,
+ payload_len);
+ checksum = gsp_phys_msgq_checksum(elem, aligned_len);
+ e->check_sum = cpu_to_le32(checksum);
+
+ slot = wp;
+ /*
+ * entry_off and msg_size are validated page-sized, so the element is
+ * exactly one chunk page: index it through the chunk page helper (the
+ * per-page fallback layout is not virtually contiguous).
+ */
+ slot_va = (u8 *)gsp_phys_chunk_page_va(q,
+ (GSP_PHYS_QUEUE_CMD_OFF + entry_off) /
+ GSP_PHYS_PAGE_SIZE + slot);
+ memcpy(slot_va, elem, msg_size);
+ kfree(elem);
+
+ /* Entries first, then writePtr; dma_wmb() orders both. */
+ dma_wmb();
+ new_wp = slot + 1;
+ if (new_wp >= msg_count)
+ new_wp -= msg_count;
+ write_ptr = (__le32 *)cmd_hdr + GSP_PHYS_MSGQ_TX_WRITEPTR_OFF / 4;
+ WRITE_ONCE(*write_ptr, cpu_to_le32(new_wp));
+ dma_wmb();
+
+ /*
+ * Ring the queue-head doorbell the way the reference producer does
+ * (kgspSetCmdQueueHead_TU102 writes 0 to NV_PGSP_QUEUE_HEAD(qIdx)).
+ */
+ writel(0, bar0 + GSP_PHYS_QUEUE_HEAD_REG);
+
+ pr_info("gsp-phys-boot: TX submitted: function=%u (0x%x) slot=%u length=%u (rpc.length=%u elemCount=%u seqNum=%u checksum=0x%08x) writePtr slot %u -> %u; doorbell QUEUE_HEAD(%u)@0x%x<-0\n",
+ function, function, slot, msg_len,
+ GSP_PHYS_MSG_RPC_HEADER_SIZE + payload_len, elem_count,
+ boot_stage.tx_seq_num, checksum, wp, new_wp,
+ GSP_PHYS_QUEUE_HEAD_IDX, GSP_PHYS_QUEUE_HEAD_REG);
+ boot_stage.tx_seq_num++;
+ return 0;
+}
+
+static DEFINE_MUTEX(gsp_phys_rm_unload_lock);
+
+int gsp_phys_boot_rm_unload(void __iomem *bar0)
+{
+ struct gsp_phys_rpc_unloading_guest_driver params;
+ struct gsp_phys_scratch14 s14;
+ unsigned long start, deadline;
+ u32 mailbox;
+ bool all_done;
+ int ret;
+
+ if (!bar0)
+ return -EINVAL;
+
+ mutex_lock(&gsp_phys_rm_unload_lock);
+ /*
+ * Quiet no-op when the boot stages did not run: gsp_phys_gpu_release()
+ * calls this unconditionally on a healthy board.
+ */
+ if (!boot_stage.primed || !boot_stage.ran) {
+ pr_debug("gsp-phys-boot: UNLOADING_GUEST_DRIVER skipped: stage 3d did not run (primed=%u ran=%u)\n",
+ boot_stage.primed, boot_stage.ran);
+ ret = -EINVAL;
+ goto out;
+ }
+ if (boot_stage.rm_unload_submitted) {
+ pr_info("gsp-phys-boot: UNLOADING_GUEST_DRIVER already submitted for this boot; not sending it twice\n");
+ ret = -EALREADY;
+ goto out;
+ }
+
+ /* Normal unload: bInPMTransition=0, bGc6Entering=0, newLevel=0. */
+ memset(&params, 0, sizeof(params));
+ gsp_phys_scratch14_log(bar0, "pre-unload-rpc");
+ pr_info("gsp-phys-boot: sending UNLOADING_GUEST_DRIVER (function %u): bInPMTransition=0 bGc6Entering=0 newLevel=0\n",
+ GSP_PHYS_RPC_FUNC_UNLOADING_GUEST_DRIVER);
+ ret = gsp_phys_queue_tx_submit(bar0,
+ GSP_PHYS_RPC_FUNC_UNLOADING_GUEST_DRIVER,
+ &params, sizeof(params));
+ if (ret) {
+ pr_err("gsp-phys-boot: UNLOADING_GUEST_DRIVER submit failed: %d; GSP-RM stays up, WPR2 teardown keeps its existing fail-safe\n",
+ ret);
+ goto out;
+ }
+ boot_stage.rm_unload_submitted = true;
+
+ /*
+ * kgspWaitForProcessorSuspend_TU102: the GSP falcon MAILBOX0 becomes
+ * LIBOS_INTERRUPT_PROCESSOR_SUSPENDED (0x80000000) once GSP-RM has
+ * shut down. Bounded to 10 s here.
+ */
+ start = jiffies;
+ deadline = start + msecs_to_jiffies(GSP_PHYS_RM_UNLOAD_TIMEOUT_MS);
+ mailbox = readl(bar0 + GSP_PHYS_GSP_BASE + GSP_PHYS_FALCON_MAILBOX0);
+ while (mailbox != GSP_PHYS_LIBOS_PROCESSOR_SUSPENDED) {
+ if (time_after_eq(jiffies, deadline)) {
+ gsp_phys_scratch14_log(bar0,
+ "unload-rpc-suspend-timeout");
+ pr_err("gsp-phys-boot: GSP-RM processor suspend not observed within %u ms (GSP MAILBOX0=0x%08x, want 0x%08x); the unload RPC may have been dropped (checksum/seqNum/link) or GSP-RM is stuck\n",
+ GSP_PHYS_RM_UNLOAD_TIMEOUT_MS, mailbox,
+ GSP_PHYS_LIBOS_PROCESSOR_SUSPENDED);
+ ret = -ETIMEDOUT;
+ goto out;
+ }
+ msleep(GSP_PHYS_RM_UNLOAD_POLL_MS);
+ mailbox = readl(bar0 + GSP_PHYS_GSP_BASE +
+ GSP_PHYS_FALCON_MAILBOX0);
+ }
+
+ gsp_phys_fwsec_scratch14_read(bar0, &s14);
+ all_done = s14.booter_load && s14.ahesasc_booter_reload &&
+ s14.booter_reload_acr_unload && s14.acr_unload_booter_unload;
+ gsp_phys_scratch14_log(bar0, "post-unload-rpc");
+ pr_info("gsp-phys-boot: UNLOADING_GUEST_DRIVER done: GSP-RM processor suspended (GSP MAILBOX0=0x%08x) after %u ms; scratch14 handoffs 24|25|26|27 all DONE=%u\n",
+ mailbox, jiffies_to_msecs(jiffies - start), all_done);
+ if (!all_done) {
+ pr_err("gsp-phys-boot: GSP-RM suspended but scratch14 handoff bits are not all DONE; the booter-unload gate will refuse (WPR2 stays up, fail-safe keeps the hand-back)\n");
+ ret = -EIO;
+ goto out;
+ }
+ ret = 0;
+
+out:
+ mutex_unlock(&gsp_phys_rm_unload_lock);
+ return ret;
+}
+
+/* ============================================================
+ * Debugfs trigger (test-enabling; inert unless a live boot ran)
+ * ============================================================ */
+
+static struct dentry *gsp_phys_debugfs_root;
+
+static ssize_t gsp_phys_unload_rpc_write(struct file *file,
+ const char __user *ubuf, size_t len,
+ loff_t *ppos)
+{
+ char buf[8];
+ unsigned long value;
+ void __iomem *bar0;
+ int ret;
+
+ if (len == 0 || len >= sizeof(buf))
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, len))
+ return -EFAULT;
+ buf[len] = '\0';
+
+ if (kstrtoul(strim(buf), 0, &value))
+ return -EINVAL;
+ if (value != 1) {
+ pr_info("gsp-phys-boot: gsp-phys-unload-rpc: write ignored (write \"1\" to trigger)\n");
+ return len;
+ }
+ if (!gsp_phys_boot_enabled()) {
+ pr_info("gsp-phys-boot: gsp-phys-unload-rpc: no-op: gsp_phys_boot is not enabled\n");
+ return len;
+ }
+ if (!gsp_phys_gpu_claimed()) {
+ pr_info("gsp-phys-boot: gsp-phys-unload-rpc: no-op: the physical GPU is not claimed\n");
+ return len;
+ }
+ if (!boot_stage.ran) {
+ pr_info("gsp-phys-boot: gsp-phys-unload-rpc: no-op: stage 3d did not run (primed=%u kicked=%u dryrun=%u)\n",
+ boot_stage.primed, boot_stage.kicked,
+ boot_stage.dryrun);
+ return len;
+ }
+ bar0 = gsp_phys_gpu_bar0();
+ if (!bar0) {
+ pr_warn("gsp-phys-boot: gsp-phys-unload-rpc: no-op: BAR0 is not mapped\n");
+ return len;
+ }
+
+ ret = gsp_phys_boot_rm_unload(bar0);
+ pr_info("gsp-phys-boot: gsp-phys-unload-rpc: UNLOADING_GUEST_DRIVER attempt returned %d (WPR2 untouched; run release/rmmod to tear down WPR2)\n",
+ ret);
+ return len;
+}
+
+static const struct file_operations gsp_phys_unload_rpc_fops = {
+ .open = simple_open,
+ .write = gsp_phys_unload_rpc_write,
+ .llseek = no_llseek,
+};
+
+int gsp_phys_boot_debugfs_init(void)
+{
+ struct dentry *file;
+
+ gsp_phys_debugfs_root = debugfs_create_dir("open-iov-vfio", NULL);
+ if (IS_ERR_OR_NULL(gsp_phys_debugfs_root)) {
+ long err = IS_ERR(gsp_phys_debugfs_root) ?
+ PTR_ERR(gsp_phys_debugfs_root) : -ENODEV;
+
+ gsp_phys_debugfs_root = NULL;
+ pr_warn("gsp-phys-boot: debugfs dir open-iov-vfio creation failed: %ld\n",
+ err);
+ return (int)err;
+ }
+ file = debugfs_create_file("gsp-phys-unload-rpc", 0200,
+ gsp_phys_debugfs_root, NULL,
+ &gsp_phys_unload_rpc_fops);
+ if (IS_ERR_OR_NULL(file)) {
+ long err = IS_ERR(file) ? PTR_ERR(file) : -ENODEV;
+
+ debugfs_remove_recursive(gsp_phys_debugfs_root);
+ gsp_phys_debugfs_root = NULL;
+ pr_warn("gsp-phys-boot: debugfs file gsp-phys-unload-rpc creation failed: %ld\n",
+ err);
+ return (int)err;
+ }
+ pr_info("gsp-phys-boot: debugfs trigger /sys/kernel/debug/open-iov-vfio/gsp-phys-unload-rpc ready (write \"1\" on a booted claim; inert unless gsp_phys_boot=1)\n");
+ return 0;
+}
+
+void gsp_phys_boot_debugfs_fini(void)
+{
+ debugfs_remove_recursive(gsp_phys_debugfs_root);
+ gsp_phys_debugfs_root = NULL;
+}
+
void gsp_phys_boot_quiesce(void __iomem *bar0)
{
if (!bar0)
@@ -1738,6 +2159,18 @@ static_assert(offsetof(struct gsp_phys_msgq_element, rpc_sequence) == 72,
"rpc.sequence must live at element offset 72 (reference ABI)");
static_assert(offsetof(struct gsp_phys_msgq_element, rpc_union) == 76,
"the rpc union word must live at element offset 76 (reference ABI)");
+static_assert(sizeof(struct gsp_phys_rpc_unloading_guest_driver) == 8,
+ "rpc_unloading_guest_driver_v1F_07 must be 8 bytes (two NvBool + u32 with 2-byte tail padding)");
+static_assert(offsetof(struct gsp_phys_rpc_unloading_guest_driver,
+ b_gc6_entering) == 1,
+ "bGc6Entering must follow bInPMTransition (reference NvBool=NvU8)");
+static_assert(offsetof(struct gsp_phys_rpc_unloading_guest_driver,
+ new_level) == 4,
+ "newLevel must live at payload offset 4 (reference ABI)");
+static_assert(GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF +
+ sizeof(struct gsp_phys_rpc_unloading_guest_driver) <=
+ GSP_PHYS_MSG_SIZE,
+ "the unload RPC payload must fit one queue element");
static_assert(sizeof(struct gsp_phys_seq_request) == GSP_PHYS_SEQ_REQUEST_SIZE,
"rpc_run_cpu_sequencer_v17_00 fixed header must be 40 bytes (reference ABI)");
static_assert(GSP_PHYS_MSG_ELEMENT_PAYLOAD_OFF ==
diff --git a/kernel/gsp_phys_booter.h b/kernel/gsp_phys_booter.h
index d76439d..a669bf3 100644
--- a/kernel/gsp_phys_booter.h
+++ b/kernel/gsp_phys_booter.h
@@ -71,6 +71,11 @@ void gsp_phys_boot_rmwait(struct gsp_phys_boot *boot, void __iomem *bar0,
/*
* Release sequence, in this order (gsp_phys_gpu_release):
+ * 0. gsp_phys_boot_rm_unload(): if stage 3d ran and the
+ * UNLOADING_GUEST_DRIVER RPC has not been submitted yet, submit it once
+ * (bounded) and wait for the GSP-RM processor suspend so the scratch14
+ * handoff bit the Booter Unload gate needs can be set; a no-op when
+ * stage 3d did not run or the RPC was already sent in this boot;
* 1. gsp_phys_boot_quiesce(): if a Booter Load kick was issued, reset the
* GSP falcon (stops RISC-V/GSP-RM) BEFORE the FWSEC teardown runs; no
* device access when bar0 is NULL (unresponsive-board abort);
@@ -82,4 +87,39 @@ void gsp_phys_boot_rmwait(struct gsp_phys_boot *boot, void __iomem *bar0,
void gsp_phys_boot_quiesce(void __iomem *bar0);
void gsp_phys_boot_staging_free(struct pci_dev *pdev);
+/*
+ * Command-queue TX: build a single-element msgq record with the 570.144
+ * element/rpc header layout, compute the XOR-fold checksum, copy it into the
+ * next command-queue slot, publish the new writePtr with a DMA barrier and
+ * ring the GSP queue-head doorbell. Requires the stage-3d command-queue
+ * staging; bounded and refuses (with a log) when the TX header does not match
+ * what was staged. Does not touch WPR2 or the falcons.
+ */
+int gsp_phys_queue_tx_submit(void __iomem *bar0, u32 function,
+ const void *payload, u32 payload_len);
+
+/*
+ * Send NV_VGPU_MSG_FUNCTION_UNLOADING_GUEST_DRIVER (function 47) with a normal
+ * unload payload (bInPMTransition=0, bGc6Entering=0, newLevel=0), then wait
+ * (bounded, 10 s) for the GSP-RM processor to suspend the way
+ * kgspWaitForProcessorSuspend_TU102 does: GSP falcon MAILBOX0 == 0x80000000.
+ * Sends at most once per boot (tracked across release and the debugfs
+ * trigger). Returns 0 only when the suspend was observed and all four
+ * scratch14 handoff bits (24..27) read DONE, -EALREADY if already sent,
+ * -EINVAL when stage 3d did not run, -ETIMEDOUT on the suspend bound and
+ * -EIO when the handoff bits are not all DONE after the suspend. Never writes
+ * WPR2.
+ */
+int gsp_phys_boot_rm_unload(void __iomem *bar0);
+
+/*
+ * Module-owned debugfs trigger (dir "open-iov-vfio", file
+ * "gsp-phys-unload-rpc", write-only). Writing "1" runs
+ * gsp_phys_boot_rm_unload() on a live claim; every other value or state
+ * (parameter off, GPU unclaimed, stage 3d not run, already submitted) is a
+ * logged no-op. Registered by nv_vgpu_init() and removed by nv_vgpu_exit().
+ */
+int gsp_phys_boot_debugfs_init(void);
+void gsp_phys_boot_debugfs_fini(void);
+
#endif /* GSP_PHYS_BOOTER_H */
diff --git a/kernel/gsp_phys_gpu.c b/kernel/gsp_phys_gpu.c
index 79c79bf..88d13f2 100644
--- a/kernel/gsp_phys_gpu.c
+++ b/kernel/gsp_phys_gpu.c
@@ -898,6 +898,15 @@ void gsp_phys_gpu_release(void)
wpr2_ret = -EIO;
gsp_phys_boot_staging_free(phys.pdev);
} else {
+ /*
+ * Clean GSP-RM shutdown: after a stage-3d boot, send
+ * UNLOADING_GUEST_DRIVER once (no-op otherwise) so
+ * GSP-RM suspends and sets the scratch14 handoff bits
+ * the Booter Unload gate needs; then quiesce the GSP
+ * falcon and run the existing FWSEC teardown. A failed
+ * RPC leaves the existing fail-safe in charge.
+ */
+ gsp_phys_boot_rm_unload(phys.bar0);
gsp_phys_boot_quiesce(phys.bar0);
wpr2_ret = gsp_phys_fwsec_release(&phys.boot,
phys.pdev,
diff --git a/kernel/main.c b/kernel/main.c
index f86dc98..30156ab 100644
--- a/kernel/main.c
+++ b/kernel/main.c
@@ -32,6 +32,7 @@
#include "nv-vgpu-vfio-interface.h"
#include "gsp_phys_gpu.h"
+#include "gsp_phys_booter.h"
#define NV_VERSION_STRING "550.163.01"
@@ -1453,6 +1454,12 @@ static int __init nv_vgpu_init(void)
ret);
}
+ /* Test-enabling trigger for the GSP-RM unload RPC; inert unless a
+ * stage-3d boot ran on a live claim (gsp_phys_boot=1). Removed on
+ * module exit. Creation failure is non-fatal. */
+ if (gsp_phys_boot_debugfs_init())
+ pr_warn("gsp-resman-vfio: gsp-phys-unload-rpc debugfs trigger unavailable\n");
+
pr_info("NVIDIA vGPU VFIO driver loaded (version %s)\n", NVIDIA_VGPU_VFIO_VERSION);
/* Start the emulated-GPU progress service: it discovers the guest's
* GPU-VA -> guest-PA map and applies copy-engine semaphore releases so
@@ -1488,6 +1495,7 @@ static void __exit nv_vgpu_exit(void)
pr_info("gsp-resman-vfio: nv_vgpu_exit: clearing VFIO ops to break circular dependency\n");
g_vgpu_vfio_ops = NULL;
+ gsp_phys_boot_debugfs_fini();
gsp_phys_gpu_release();
if (nv_vgpu_driver.device_count > 0) {