[PATCH v2 26/32] gpu: nova-core: vgpu: add GSP plugin RPC transactions

From: Zhi Wang

Date: Mon Sep 14 2026 - 04:06:54 EST


The GSP plugin processes requests after the vGPU manager rings its VF
doorbell and publishes completion in the shared response buffer.

Add PluginRpc to own the communication mapping and track request
sequences. Copy each payload before publishing its sequence, ring the
doorbell and wait for the matching completion, checking firmware status
and enforcing a timeout.

Signed-off-by: Zhi Wang <zhiw@xxxxxxxxxx>
---
drivers/gpu/nova-core/regs.rs | 33 +++-
drivers/gpu/nova-core/vgpu.rs | 1 +
drivers/gpu/nova-core/vgpu/fw.rs | 11 ++
drivers/gpu/nova-core/vgpu/gsp_plugin_comm.rs | 49 +++++-
drivers/gpu/nova-core/vgpu/gsp_plugin_rpc.rs | 150 ++++++++++++++++++
drivers/gpu/nova-core/vgpu/instance.rs | 17 +-
6 files changed, 251 insertions(+), 10 deletions(-)
create mode 100644 drivers/gpu/nova-core/vgpu/gsp_plugin_rpc.rs

diff --git a/drivers/gpu/nova-core/regs.rs b/drivers/gpu/nova-core/regs.rs
index bf43c71a53ee..af953aedb87b 100644
--- a/drivers/gpu/nova-core/regs.rs
+++ b/drivers/gpu/nova-core/regs.rs
@@ -7,13 +7,17 @@
Io,
Mmio, //
},
+ prelude::*,
sizes::SizeConstants,
time, //
};
use pin_init::Zeroable;

use crate::{
- driver::NovaRegisters,
+ driver::{
+ Bar0,
+ NovaRegisters, //
+ },
falcon::{
DmaTrfCmdSize,
FalconCoreRev,
@@ -39,6 +43,33 @@
pub(crate) NV_PBUS_SW_SCRATCH(u32)[64] @ 0x00001400 {}
}

+// VIRTUAL_FUNCTION
+
+register! {
+ base: NovaRegisters;
+
+ // PF BAR0 exposes the virtual-function register window at 0x00b8_0000.
+ pub(crate) NV_VIRTUAL_FUNCTION_PRIV_DOORBELL(u32) @ 0x00b8_2200 {
+ 31:0 handle;
+ }
+}
+
+impl NV_VIRTUAL_FUNCTION_PRIV_DOORBELL {
+ const DOORBELL_STRIDE: u32 = 32;
+ const DOORBELL_VECTOR: u32 = 17;
+
+ /// Notify the GSP plugin for the given guest function and read back the doorbell.
+ pub(crate) fn ring_gsp_plugin(bar0: Bar0<'_>, gfid: u32) -> Result {
+ let value = gfid
+ .checked_mul(Self::DOORBELL_STRIDE)
+ .and_then(|value| value.checked_add(Self::DOORBELL_VECTOR))
+ .ok_or(EOVERFLOW)?;
+ bar0.try_write_reg(Self::zeroed().with_handle(value))?;
+ bar0.try_read(NV_VIRTUAL_FUNCTION_PRIV_DOORBELL)?;
+ Ok(())
+ }
+}
+
// PGC6 register space.
//
// `GC6` is a GPU low-power state where VRAM is in self-refresh and the GPU is powered down (except
diff --git a/drivers/gpu/nova-core/vgpu.rs b/drivers/gpu/nova-core/vgpu.rs
index 8919322dc057..221a2804f5da 100644
--- a/drivers/gpu/nova-core/vgpu.rs
+++ b/drivers/gpu/nova-core/vgpu.rs
@@ -25,6 +25,7 @@
mod commands;
mod fw;
mod gsp_plugin_comm;
+mod gsp_plugin_rpc;
mod hal;
mod instance;
mod vram;
diff --git a/drivers/gpu/nova-core/vgpu/fw.rs b/drivers/gpu/nova-core/vgpu/fw.rs
index 03225b9833c3..9925d5ccf71f 100644
--- a/drivers/gpu/nova-core/vgpu/fw.rs
+++ b/drivers/gpu/nova-core/vgpu/fw.rs
@@ -44,3 +44,14 @@

pub(super) const GMCAPI_CMD_CLEANUP_GSP_VGPU_PLUGIN_RESOURCES: u32 =
bindings::GMCAPI_COMMANDS_GMCAPI_CMD_CLEANUP_GSP_VGPU_PLUGIN_RESOURCES;
+
+/// State observed in the response buffer for an expected RPC sequence.
+pub(super) enum RpcResponse {
+ Pending {
+ /// Last sequence completed by firmware.
+ sequence: u32,
+ },
+ Complete {
+ status: u32,
+ },
+}
diff --git a/drivers/gpu/nova-core/vgpu/gsp_plugin_comm.rs b/drivers/gpu/nova-core/vgpu/gsp_plugin_comm.rs
index 5d129a1b908f..20695929a58c 100644
--- a/drivers/gpu/nova-core/vgpu/gsp_plugin_comm.rs
+++ b/drivers/gpu/nova-core/vgpu/gsp_plugin_comm.rs
@@ -17,7 +17,8 @@
use super::fw::{
self,
RawControlRegion,
- RawResponseRegion, //
+ RawResponseRegion,
+ RpcResponse, //
};

/// Physical VRAM regions containing the vGPU plugin logs.
@@ -238,7 +239,6 @@ pub(super) fn is_plugin_ready(&self) -> Result<bool> {
}

/// Initialize the shared control and response buffers for plugin RPC.
- #[expect(dead_code)]
pub(super) fn initialize(&self) -> Result {
self.write_u64(
&self.control,
@@ -336,6 +336,51 @@ pub(super) fn initialize(&self) -> Result {
)
}

+ /// Copy and publish one RPC request to firmware.
+ pub(super) fn submit(&self, message: u32, sequence: u32, data: &[u8]) -> Result {
+ if u64::try_from(data.len()).map_err(|_| EOVERFLOW)? > self.message.size() {
+ return Err(E2BIG);
+ }
+
+ for (index, chunk) in data.chunks(size_of::<u32>()).enumerate() {
+ let mut bytes = [0u8; size_of::<u32>()];
+ bytes[..chunk.len()].copy_from_slice(chunk);
+ let field = index.checked_mul(size_of::<u32>()).ok_or(EOVERFLOW)?;
+ self.write_u32(&self.message, field, u32::from_le_bytes(bytes))?;
+ }
+
+ self.write_u32(
+ &self.control,
+ core::mem::offset_of!(RawControlRegion, __bindgen_anon_1.message_type),
+ message,
+ )?;
+ self.write_u32(
+ &self.control,
+ core::mem::offset_of!(RawControlRegion, __bindgen_anon_1.message_seq_num),
+ sequence,
+ )
+ }
+
+ /// Read firmware's response for an expected RPC sequence.
+ pub(super) fn response(&self, expected_sequence: u32) -> Result<RpcResponse> {
+ let sequence = self.read_u32(
+ &self.response,
+ core::mem::offset_of!(
+ RawResponseRegion,
+ __bindgen_anon_1.message_seq_num_processed
+ ),
+ )?;
+ if sequence != expected_sequence {
+ return Ok(RpcResponse::Pending { sequence });
+ }
+
+ let status = self.read_u32(
+ &self.response,
+ core::mem::offset_of!(RawResponseRegion, __bindgen_anon_1.result_code),
+ )?;
+ Ok(RpcResponse::Complete { status })
+ }
+
/// Invalidate the PTEs and release the communication mapping.
pub(super) fn destroy(self, mm: &mut GpuMm<'_>) -> Result {
self.map.destroy(mm)
diff --git a/drivers/gpu/nova-core/vgpu/gsp_plugin_rpc.rs b/drivers/gpu/nova-core/vgpu/gsp_plugin_rpc.rs
new file mode 100644
index 000000000000..bc5d576d8601
--- /dev/null
+++ b/drivers/gpu/nova-core/vgpu/gsp_plugin_rpc.rs
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+//! GSP plugin RPC.
+//!
+//! ```text
+//! Host (PluginRpc) Shared RPC buffer (VRAM) GSP plugin
+//! | | |
+//! |-- BAR1: payload --------->| Message |
+//! |-- BAR1: type, sequence -->| Control |
+//! | | |
+//! |-- BAR0: VF doorbell ----------------------------->|
+//! | |<-- read request ------|
+//! | | | process RPC
+//! | |<-- completion --------|
+//! |-- poll processed seq ---->| Response |
+//! |<-- matching seq, status --| |
+//! ```
+
+use kernel::{
+ device,
+ prelude::*,
+ time::{
+ delay::fsleep,
+ Delta,
+ Instant,
+ Monotonic, //
+ },
+};
+
+use crate::{
+ driver::Bar0,
+ mm::GpuMm,
+ regs::NV_VIRTUAL_FUNCTION_PRIV_DOORBELL, //
+};
+
+use super::{
+ fw::RpcResponse,
+ gsp_plugin_comm::CommBufferRegion,
+ instance::Gfid, //
+};
+
+/// BAR1-backed channel used to communicate with one GSP plugin.
+pub(super) struct PluginRpc<'map, 'gpu> {
+ comm: CommBufferRegion<'map, 'gpu>,
+ message_sequence: u32,
+}
+
+impl<'map, 'gpu> PluginRpc<'map, 'gpu> {
+ pub(super) fn new(comm: CommBufferRegion<'map, 'gpu>) -> Self {
+ Self {
+ comm,
+ message_sequence: 0,
+ }
+ }
+
+ pub(super) fn comm(&self) -> &CommBufferRegion<'map, 'gpu> {
+ &self.comm
+ }
+
+ /// Initialize the control and response buffers for the first RPC.
+ #[expect(dead_code)]
+ pub(super) fn init_rpc(&mut self) -> Result {
+ self.comm.initialize()?;
+ self.message_sequence = 0;
+ Ok(())
+ }
+
+ fn next_sequence(&self) -> u32 {
+ let sequence = self.message_sequence.wrapping_add(1);
+ if sequence == 0 {
+ 1
+ } else {
+ sequence
+ }
+ }
+
+ /// Write one RPC message, ring the VF doorbell, and wait for its response.
+ #[expect(dead_code)]
+ pub(super) fn rpc_call(
+ &mut self,
+ dev: &device::Device<device::Bound>,
+ bar0: Bar0<'_>,
+ gfid: Gfid,
+ message_type: u32,
+ data: &[u8],
+ ) -> Result {
+ let sequence = self.next_sequence();
+ self.comm.submit(message_type, sequence, data)?;
+ self.message_sequence = sequence;
+
+ dev_dbg!(
+ dev,
+ "vGPU RPC: gfid={} type={} bytes={} sequence={}\n",
+ gfid.0,
+ message_type,
+ data.len(),
+ sequence,
+ );
+
+ NV_VIRTUAL_FUNCTION_PRIV_DOORBELL::ring_gsp_plugin(bar0, gfid.0)?;
+ self.wait_response(dev, sequence)
+ }
+
+ fn wait_response(&self, dev: &device::Device<device::Bound>, expected_sequence: u32) -> Result {
+ let start = Instant::<Monotonic>::now();
+ let timeout = Delta::from_secs(120);
+
+ loop {
+ match self.comm.response(expected_sequence)? {
+ RpcResponse::Complete { status } => {
+ if status != 0 {
+ dev_dbg!(
+ dev,
+ "vGPU RPC: sequence {} failed with status {}\n",
+ expected_sequence,
+ status,
+ );
+ return Err(EIO);
+ }
+
+ dev_dbg!(
+ dev,
+ "vGPU RPC: sequence {} completed after {:?}\n",
+ expected_sequence,
+ start.elapsed(),
+ );
+ return Ok(());
+ }
+ RpcResponse::Pending { sequence } => {
+ if start.elapsed() >= timeout {
+ dev_dbg!(
+ dev,
+ "vGPU RPC: sequence {} timed out; last response was {}\n",
+ expected_sequence,
+ sequence,
+ );
+ return Err(ETIMEDOUT);
+ }
+ }
+ }
+ fsleep(Delta::from_millis(1));
+ }
+ }
+
+ /// Release the BAR1 mapping.
+ pub(super) fn destroy(self, mm: &mut GpuMm<'_>) -> Result {
+ self.comm.destroy(mm)
+ }
+}
diff --git a/drivers/gpu/nova-core/vgpu/instance.rs b/drivers/gpu/nova-core/vgpu/instance.rs
index 535e5ab7c2ad..994b046802c1 100644
--- a/drivers/gpu/nova-core/vgpu/instance.rs
+++ b/drivers/gpu/nova-core/vgpu/instance.rs
@@ -31,6 +31,7 @@

use super::{
gsp_plugin_comm::CommBufferRegion,
+ gsp_plugin_rpc::PluginRpc,
vram::{
VgpuVramLayout,
VgpuVramSlot,
@@ -135,7 +136,7 @@ pub(super) struct VgpuInstance<'gpu> {
chids: ChannelIdReservation<'gpu>,
num_plugin_channels: u32,
vram_slot: VgpuVramSlot,
- comm: CommBufferRegion<'gpu, 'gpu>,
+ pub(super) plugin_rpc: PluginRpc<'gpu, 'gpu>,
needs_teardown: bool,
}

@@ -149,7 +150,7 @@ fn bootload(
) -> Result {
let fb = &self.vram_slot.fbmem;
let mgmt = &self.vram_slot.mgmt_heap;
- let logs = self.comm.plugin_logs();
+ let logs = self.plugin_rpc.comm().plugin_logs();

let payload = encode_vgpu_bootload(
self.dbdf,
@@ -182,11 +183,11 @@ fn bootload(
payload.len() * size_of::<u64>(),
);

- self.comm.clear_plugin_ready()?;
+ self.plugin_rpc.comm().clear_plugin_ready()?;
self.needs_teardown = true;
send_bootload(cmdq, &payload)?;

- wait_plugin_ready(dev, &self.comm)?;
+ wait_plugin_ready(dev, self.plugin_rpc.comm())?;

dev_dbg!(dev, "bootload: gfid={} plugin ready\n", self.gfid.0);
Ok(())
@@ -263,9 +264,11 @@ fn release_vram_slot(&mut self, slot: VgpuVramSlot) -> Result {

fn release_instance(&mut self, instance: VgpuInstance<'gpu>, mm: &mut GpuMm<'_>) -> Result {
let VgpuInstance {
- comm, vram_slot, ..
+ plugin_rpc,
+ vram_slot,
+ ..
} = instance;
- let result = comm.destroy(mm);
+ let result = plugin_rpc.destroy(mm);
self.release_vram_slot(vram_slot)?;
result
}
@@ -340,7 +343,7 @@ pub(super) fn allocate_instance(
chids,
num_plugin_channels: 3,
vram_slot,
- comm,
+ plugin_rpc: PluginRpc::new(comm),
needs_teardown: false,
};
match self.instances.push_within_capacity(instance) {