[PATCH v3 8/8] gpu: nova-core: add NVKV GSP_INIT schemas

From: Eliot Courtney

Date: Mon Sep 28 2026 - 04:46:37 EST


Add the first user of NVKV encode/decode which is the request and
response for GSP init. For now this is exercised via unit tests. Later
patches will support GMCAPI in `Cmdq` and use these messages.

Signed-off-by: Eliot Courtney <ecourtney@xxxxxxxxxx>
---
drivers/gpu/nova-core/gsp/fw/commands.rs | 447 ++++++++++++++++++++++++++++++-
drivers/gpu/nova-core/gsp/nvkv.rs | 3 -
drivers/gpu/nova-core/gsp/nvkv/decode.rs | 1 +
drivers/gpu/nova-core/gsp/nvkv/encode.rs | 1 +
4 files changed, 448 insertions(+), 4 deletions(-)

diff --git a/drivers/gpu/nova-core/gsp/fw/commands.rs b/drivers/gpu/nova-core/gsp/fw/commands.rs
index 32856ff74183..02de225af917 100644
--- a/drivers/gpu/nova-core/gsp/fw/commands.rs
+++ b/drivers/gpu/nova-core/gsp/fw/commands.rs
@@ -4,6 +4,8 @@
use core::ops::Range;

use kernel::{
+ alloc::ArrayVec,
+ bitfield,
device,
pci,
prelude::*,
@@ -15,7 +17,21 @@

use crate::{
gpu::Chipset,
- gsp::GSP_PAGE_SIZE,
+ gsp::{
+ nvkv::{
+ nvkv_decode,
+ nvkv_encode,
+ Accumulated,
+ Array,
+ DecoderValue,
+ Encodable,
+ Encoder,
+ Key,
+ KeyId,
+ Required, //
+ },
+ GSP_PAGE_SIZE, //
+ },
num::IntoSafeCast, //
};

@@ -230,3 +246,432 @@ unsafe impl AsBytes for UnloadingGuestDriver {}
// SAFETY: This struct only contains integer types for which all bit patterns
// are valid.
unsafe impl FromBytes for UnloadingGuestDriver {}
+
+/// The host CPU architecture.
+#[derive(Clone, Copy)]
+pub(crate) enum HostArch {
+ None = 0,
+ X86_64 = 1,
+ Ppc64le = 2,
+ Arm = 3,
+ Aarch64 = 4,
+ Riscv64 = 5,
+}
+
+// TODO[FPRI]: This is a temporary solution to be replaced with the corresponding derive macros once
+// they land.
+impl TryFrom<u32> for HostArch {
+ type Error = Error;
+
+ fn try_from(value: u32) -> Result<Self> {
+ match value {
+ 0 => Ok(Self::None),
+ 1 => Ok(Self::X86_64),
+ 2 => Ok(Self::Ppc64le),
+ 3 => Ok(Self::Arm),
+ 4 => Ok(Self::Aarch64),
+ 5 => Ok(Self::Riscv64),
+ _ => Err(EINVAL),
+ }
+ }
+}
+
+impl From<HostArch> for u32 {
+ fn from(value: HostArch) -> Self {
+ value as u32
+ }
+}
+
+nvkv_encode! {
+ /// A GSP registry entry.
+ struct RegKey {
+ key_name: Key<&'static [u8], { Self::REGKEY_NAME_KEY }>,
+ key_value: Key<u32, { Self::REGKEY_VALUE_U32_KEY }>,
+ }
+}
+
+impl RegKey {
+ // Define the Key IDs read/written by GSP.
+ const REGKEY_NAME_KEY: KeyId = 0x3070;
+ const REGKEY_VALUE_U32_KEY: KeyId = 0x3071;
+}
+
+impl Encodable for KVVec<RegKey> {
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ for regkey in self {
+ regkey.encode(encoder)?;
+ }
+ Ok(())
+ }
+}
+
+nvkv_encode! {
+ /// SR-IOV virtual function information.
+ struct VfInfo {
+ total_vfs: Key<u32, { Self::VF_TOTAL_VFS_KEY }>,
+ first_vf_offset: Key<u32, { Self::VF_FIRST_VF_OFFSET_KEY }>,
+ flags: Key<u64, { Self::VF_FLAGS_KEY }>,
+ first_bar0_address: Key<u64, { Self::VF_FIRST_BAR0_ADDRESS_KEY }>,
+ first_bar1_address: Key<u64, { Self::VF_FIRST_BAR1_ADDRESS_KEY }>,
+ first_bar2_address: Key<u64, { Self::VF_FIRST_BAR2_ADDRESS_KEY }>,
+ }
+}
+
+impl VfInfo {
+ // Define the Key IDs read/written by GSP.
+ const VF_TOTAL_VFS_KEY: KeyId = 0x0080;
+ const VF_FIRST_VF_OFFSET_KEY: KeyId = 0x0081;
+ const VF_FLAGS_KEY: KeyId = 0x1003;
+ const VF_FIRST_BAR0_ADDRESS_KEY: KeyId = 0x1050;
+ const VF_FIRST_BAR1_ADDRESS_KEY: KeyId = 0x1051;
+ const VF_FIRST_BAR2_ADDRESS_KEY: KeyId = 0x1052;
+}
+
+nvkv_encode! {
+ /// Payload of the `GSP_INIT` command.
+ // TODO: expect() doesn't work here due to Self:: reference, fixed in 1.97.0
+ // https://github.com/rust-lang/rust/pull/154377
+ #[cfg_attr(not(CONFIG_KUNIT), allow(dead_code))]
+ struct GspInitRequest {
+ pci_device_id: Key<u32, { Self::PCI_DEVICE_ID_KEY }>,
+ pci_sub_device_id: Key<u32, { Self::PCI_SUBDEVICE_ID_KEY }>,
+ pci_revision_id: Key<u32, { Self::PCI_REVISION_ID_KEY }>,
+ pci_config_mirror_base: Key<u32, { Self::PCI_CONFIG_MIRROR_BASE_KEY }>,
+ pci_config_mirror_size: Key<u32, { Self::PCI_CONFIG_MIRROR_SIZE_KEY }>,
+ host_arch: Key<HostArch, { Self::HOST_ARCH_KEY }, u32>,
+ bus_device_func: Key<u64, { Self::NV_DOMAIN_BUS_DEVICE_FUNC_KEY }>,
+ regkeys: KVVec<RegKey>,
+ vf_info: Option<VfInfo>,
+ }
+}
+
+impl GspInitRequest {
+ // Define the Key IDs read/written by GSP.
+ const PCI_DEVICE_ID_KEY: KeyId = 0x0001;
+ const PCI_SUBDEVICE_ID_KEY: KeyId = 0x0002;
+ const PCI_REVISION_ID_KEY: KeyId = 0x0003;
+ const PCI_CONFIG_MIRROR_BASE_KEY: KeyId = 0x0010;
+ const PCI_CONFIG_MIRROR_SIZE_KEY: KeyId = 0x0011;
+ const HOST_ARCH_KEY: KeyId = 0x0070;
+ const NV_DOMAIN_BUS_DEVICE_FUNC_KEY: KeyId = 0x1020;
+}
+
+// Decode:
+
+// Should decode with UnknownKeyPolicy::Ignore.
+nvkv_decode! {
+ /// Schema for the `GSP_INIT` response.
+ // TODO: expect() doesn't work here due to Self:: reference, fixed in 1.97.0
+ // https://github.com/rust-lang/rust/pull/154377
+ #[cfg_attr(not(CONFIG_KUNIT), allow(dead_code))]
+ struct GspInitResponseSchema => GspInitResponse {
+ gpu_name:
+ Array<u8, { GspInitResponse::MAX_GPU_NAME_LEN }, { Self::GPU_NAME_STRING_KEY }>,
+ fb_regions: Accumulated<FbRegionSchema>,
+ bar1_pde_base: Required<u64, { Self::BAR1_PDE_BASE_KEY }>,
+ vmmu_segment_size: Key<u64, { Self::VMMU_SEGMENT_SIZE_KEY }>,
+ }
+}
+
+impl GspInitResponseSchema {
+ // Define the Key IDs read/written by GSP.
+ const GPU_NAME_STRING_KEY: KeyId = 0x2000;
+ const BAR1_PDE_BASE_KEY: KeyId = 0x1020;
+ const VMMU_SEGMENT_SIZE_KEY: KeyId = 0x1050;
+}
+
+/// Payload of the `GSP_INIT` response.
+struct GspInitResponse {
+ gpu_name: ArrayVec<u8, { Self::MAX_GPU_NAME_LEN }>,
+ fb_regions: KVVec<FbRegion>,
+ bar1_pde_base: u64,
+ vmmu_segment_size: u64,
+}
+
+impl GspInitResponse {
+ const MAX_GPU_NAME_LEN: usize = 64;
+}
+
+nvkv_decode! {
+ /// Schema for one FB region of the `GSP_INIT` response.
+ struct FbRegionSchema => FbRegion {
+ base: Required<u64, { Self::BASE_KEY }>,
+ limit: Required<u64, { Self::LIMIT_KEY }>,
+ flags: Required<FbRegionFlags, { Self::FLAGS_KEY }>,
+ tag: Required<u32, { Self::TAG_KEY }>,
+ }
+}
+
+impl FbRegionSchema {
+ // Define the Key IDs read/written by GSP.
+ const BASE_KEY: KeyId = 0x1011;
+ const LIMIT_KEY: KeyId = 0x1012;
+ const FLAGS_KEY: KeyId = 0x0012;
+ const TAG_KEY: KeyId = 0x0013;
+}
+
+bitfield! {
+ /// FB region attribute flags.
+ struct FbRegionFlags(u32) {
+ 0:0 support_compressed => bool;
+ 1:1 support_iso => bool;
+ 2:2 protected => bool;
+ }
+}
+
+impl TryFrom<DecoderValue<'_>> for FbRegionFlags {
+ type Error = Error;
+
+ fn try_from(value: DecoderValue<'_>) -> Result<Self> {
+ if let DecoderValue::Scalar32(v) = value {
+ Ok(v.into())
+ } else {
+ Err(EINVAL)
+ }
+ }
+}
+
+/// One FB memory region.
+struct FbRegion {
+ base: u64,
+ limit: u64,
+ flags: FbRegionFlags,
+ tag: u32,
+}
+
+#[kunit_tests(nova_core_fw_commands)]
+mod tests {
+ use crate::gsp::nvkv::{
+ Decoder,
+ DecoderValue,
+ Index,
+ Schema,
+ UnknownKeyPolicy,
+ Visit, //
+ };
+
+ use super::*;
+
+ /// Decodes the registry keys of a `GspInitRequest` into (name, value) pairs.
+ #[derive(Default)]
+ struct RegKeysSchema<'d> {
+ pairs: KVVec<(&'d [u8], u32)>,
+ // A name whose value has not been decoded yet.
+ name: Option<&'d [u8]>,
+ }
+
+ impl<'d> Schema for RegKeysSchema<'d> {
+ type Target = KVVec<(&'d [u8], u32)>;
+
+ fn init() -> impl Init<Self> {
+ Self::default()
+ }
+
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ // A name must be followed by its value.
+ if self.name.take().is_some() {
+ return Err(EINVAL);
+ }
+ Ok(core::mem::take(&mut self.pairs))
+ }
+ }
+
+ impl<'d> Visit<'d> for RegKeysSchema<'d> {
+ fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'d>) -> Result<bool> {
+ if key != RegKey::REGKEY_NAME_KEY && key != RegKey::REGKEY_VALUE_U32_KEY {
+ return Ok(false);
+ }
+ if index != Index::new::<0>() {
+ return Err(EINVAL);
+ }
+ if key == RegKey::REGKEY_NAME_KEY {
+ // A name must be followed by its value before the next name.
+ if self.name.replace(value.try_into()?).is_some() {
+ return Err(EINVAL);
+ }
+ } else {
+ let name = self.name.take().ok_or(EINVAL)?;
+ self.pairs.push((name, value.try_into()?), GFP_KERNEL)?;
+ }
+ Ok(true)
+ }
+ }
+
+ // Tests that `GspInitRequest` encodes correctly.
+ #[test]
+ fn gsp_init_request() -> Result {
+ let mut encoder = Encoder::new();
+
+ let mut regkeys = KVVec::new();
+ regkeys.push(
+ RegKey {
+ key_name: b"testkey1\0".into(),
+ key_value: 0xdead_beef.into(),
+ },
+ GFP_KERNEL,
+ )?;
+ regkeys.push(
+ RegKey {
+ key_name: b"testkey2\0".into(),
+ key_value: 0x1234_5678.into(),
+ },
+ GFP_KERNEL,
+ )?;
+
+ let gsp_init = GspInitRequest {
+ pci_device_id: 45.into(),
+ pci_sub_device_id: 67.into(),
+ pci_revision_id: 3.into(),
+ pci_config_mirror_base: 0x1234_5678.into(),
+ pci_config_mirror_size: 0x1000.into(),
+ host_arch: HostArch::Aarch64.into(),
+ bus_device_func: 0x0001_0203_0405_0607.into(),
+ regkeys,
+ vf_info: Some(VfInfo {
+ total_vfs: 8.into(),
+ first_vf_offset: 1.into(),
+ flags: 0x7.into(),
+ first_bar0_address: 0x1000_0000.into(),
+ first_bar1_address: 0x2000_0000.into(),
+ first_bar2_address: 0x3000_0000.into(),
+ }),
+ };
+
+ gsp_init.encode(&mut encoder)?;
+ let encoded = encoder.finish();
+ assert_eq!(encoded.len(), 26);
+
+ type Req = GspInitRequest;
+ let index0 = Index::new::<0>();
+ let mut expected = Encoder::new();
+ expected.encode_u32(Req::PCI_DEVICE_ID_KEY, index0, 45)?;
+ expected.encode_u32(Req::PCI_SUBDEVICE_ID_KEY, index0, 67)?;
+ expected.encode_u32(Req::PCI_REVISION_ID_KEY, index0, 3)?;
+ expected.encode_u32(Req::PCI_CONFIG_MIRROR_BASE_KEY, index0, 0x1234_5678)?;
+ expected.encode_u32(Req::PCI_CONFIG_MIRROR_SIZE_KEY, index0, 0x1000)?;
+ expected.encode_u32(Req::HOST_ARCH_KEY, index0, HostArch::Aarch64.into())?;
+ expected.encode_u64(
+ Req::NV_DOMAIN_BUS_DEVICE_FUNC_KEY,
+ index0,
+ 0x0001_0203_0405_0607,
+ )?;
+ // Each registry key is its name followed by its value, both at index 0.
+ expected.encode_array8(RegKey::REGKEY_NAME_KEY, index0, b"testkey1\0")?;
+ expected.encode_u32(RegKey::REGKEY_VALUE_U32_KEY, index0, 0xdead_beef)?;
+ expected.encode_array8(RegKey::REGKEY_NAME_KEY, index0, b"testkey2\0")?;
+ expected.encode_u32(RegKey::REGKEY_VALUE_U32_KEY, index0, 0x1234_5678)?;
+ expected.encode_u32(VfInfo::VF_TOTAL_VFS_KEY, index0, 8)?;
+ expected.encode_u32(VfInfo::VF_FIRST_VF_OFFSET_KEY, index0, 1)?;
+ expected.encode_u64(VfInfo::VF_FLAGS_KEY, index0, 0x7)?;
+ expected.encode_u64(VfInfo::VF_FIRST_BAR0_ADDRESS_KEY, index0, 0x1000_0000)?;
+ expected.encode_u64(VfInfo::VF_FIRST_BAR1_ADDRESS_KEY, index0, 0x2000_0000)?;
+ expected.encode_u64(VfInfo::VF_FIRST_BAR2_ADDRESS_KEY, index0, 0x3000_0000)?;
+ assert_eq!(*encoded, *expected.finish());
+
+ // Decode the registry keys back out of the request, ignoring the other keys.
+ let decoder = Decoder::new(&encoded, UnknownKeyPolicy::Ignore);
+ let mut schema = KBox::init(RegKeysSchema::init(), GFP_KERNEL)?;
+ let decoded = KBox::try_init(decoder.decode(&mut *schema)?, GFP_KERNEL)?;
+ assert_eq!(
+ decoded.as_slice(),
+ &[
+ (&b"testkey1\0"[..], 0xdead_beef),
+ (&b"testkey2\0"[..], 0x1234_5678),
+ ]
+ );
+
+ Ok(())
+ }
+
+ // Tests that FB region decoding fails when required keys are missing.
+ #[test]
+ fn decode_fb_region_missing_required_fails() -> Result {
+ let mut encoder = Encoder::new();
+ encoder.encode_u64(FbRegionSchema::BASE_KEY, Index::new::<0>(), 0x1000_0000)?;
+ let data = encoder.finish();
+
+ let decoder = Decoder::new(&data, UnknownKeyPolicy::Ignore);
+ let mut schema = KBox::init(FbRegionSchema::init(), GFP_KERNEL)?;
+ let init = decoder.decode(&mut *schema)?;
+ assert!(KBox::try_init(init, GFP_KERNEL).is_err());
+
+ Ok(())
+ }
+
+ // Tests that a minimal and a full `GSP_INIT` response decode correctly.
+ #[test]
+ fn gsp_init_response() -> Result {
+ let name = b"test name\0";
+ const BAR1_PDE_BASE: u64 = 0xdead_0000;
+ const FB_REGION0_BASE: u64 = 0x1000_0000;
+ const FB_REGION0_LIMIT: u64 = 0x1fff_ffff;
+ const FB_REGION0_FLAGS: u32 = 0x7;
+ const FB_REGION0_TAG: u32 = 0;
+ const FB_REGION1_BASE: u64 = 0x2000_0000;
+ const FB_REGION1_LIMIT: u64 = 0x2fff_ffff;
+ const FB_REGION1_FLAGS: u32 = 0x3;
+ const FB_REGION1_TAG: u32 = 1;
+ const VMMU_SEGMENT_SIZE: u64 = 0x0200_0000;
+
+ type Resp = GspInitResponseSchema;
+
+ let index0 = Index::new::<0>();
+ let index1 = Index::new::<1>();
+
+ // A minimal response: only the BAR1 PDE base, so the FB region list stays empty.
+ let mut encoder = Encoder::new();
+ encoder.encode_u64(Resp::BAR1_PDE_BASE_KEY, index0, BAR1_PDE_BASE)?;
+ let data = encoder.finish();
+
+ let decoder = Decoder::new(&data, UnknownKeyPolicy::Ignore);
+ let mut schema = KBox::init(Resp::init(), GFP_KERNEL)?;
+ let response = KBox::try_init(decoder.decode(&mut *schema)?, GFP_KERNEL)?;
+ assert_eq!(response.bar1_pde_base, BAR1_PDE_BASE);
+ assert!(response.fb_regions.is_empty());
+
+ // A full response.
+ let mut encoder = Encoder::new();
+ encoder.encode_array8(Resp::GPU_NAME_STRING_KEY, index0, name)?;
+ encoder.encode_u64(Resp::BAR1_PDE_BASE_KEY, index0, BAR1_PDE_BASE)?;
+ encoder.encode_u64(FbRegionSchema::BASE_KEY, index0, FB_REGION0_BASE)?;
+ encoder.encode_u64(FbRegionSchema::LIMIT_KEY, index0, FB_REGION0_LIMIT)?;
+ encoder.encode_u32(FbRegionSchema::FLAGS_KEY, index0, FB_REGION0_FLAGS)?;
+ encoder.encode_u32(FbRegionSchema::TAG_KEY, index0, FB_REGION0_TAG)?;
+
+ // Test that this unrelated key can safely interleave.
+ encoder.encode_u64(Resp::VMMU_SEGMENT_SIZE_KEY, index0, VMMU_SEGMENT_SIZE)?;
+
+ encoder.encode_u64(FbRegionSchema::BASE_KEY, index1, FB_REGION1_BASE)?;
+ encoder.encode_u64(FbRegionSchema::LIMIT_KEY, index1, FB_REGION1_LIMIT)?;
+ encoder.encode_u32(FbRegionSchema::FLAGS_KEY, index1, FB_REGION1_FLAGS)?;
+ encoder.encode_u32(FbRegionSchema::TAG_KEY, index1, FB_REGION1_TAG)?;
+ let data = encoder.finish();
+
+ let decoder = Decoder::new(&data, UnknownKeyPolicy::Error);
+ let mut schema = KBox::init(Resp::init(), GFP_KERNEL)?;
+ let response = KBox::try_init(decoder.decode(&mut *schema)?, GFP_KERNEL)?;
+
+ assert_eq!(&*response.gpu_name, &name[..]);
+ assert_eq!(response.bar1_pde_base, BAR1_PDE_BASE);
+ assert_eq!(response.fb_regions.len(), 2);
+
+ let fb_region0 = &response.fb_regions[0];
+ assert_eq!(fb_region0.base, FB_REGION0_BASE);
+ assert_eq!(fb_region0.limit, FB_REGION0_LIMIT);
+ assert_eq!(fb_region0.flags.into_raw(), FB_REGION0_FLAGS);
+ assert!(fb_region0.flags.support_compressed());
+ assert!(fb_region0.flags.support_iso());
+ assert!(fb_region0.flags.protected());
+ assert_eq!(fb_region0.tag, FB_REGION0_TAG);
+
+ let fb_region1 = &response.fb_regions[1];
+ assert_eq!(fb_region1.base, FB_REGION1_BASE);
+ assert_eq!(fb_region1.limit, FB_REGION1_LIMIT);
+ assert_eq!(fb_region1.flags.into_raw(), FB_REGION1_FLAGS);
+ assert_eq!(fb_region1.tag, FB_REGION1_TAG);
+
+ assert_eq!(response.vmmu_segment_size, VMMU_SEGMENT_SIZE);
+
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index 5791df07a7fa..353125e2d321 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -9,9 +9,6 @@
//! function calls will map to some struct - for example, f(GPU_NAME_STRING_KEY, 0, b"some gpu")
//! naturally maps to storing a &str with the GPU name.

-#![cfg_attr(not(CONFIG_KUNIT), expect(unused_imports))]
-#![cfg_attr(not(CONFIG_KUNIT), expect(unused_macros))]
-
use core::{
marker::PhantomData,
ops::{
diff --git a/drivers/gpu/nova-core/gsp/nvkv/decode.rs b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
index 24dad31296cb..c592d3098816 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/decode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
@@ -132,6 +132,7 @@ fn visit(
}
};
}
+pub(crate) use nvkv_decode;

impl<T: Default, const KEY_ID: KeyId> Schema for Key<T, KEY_ID> {
type Target = T;
diff --git a/drivers/gpu/nova-core/gsp/nvkv/encode.rs b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
index 2cc0cfbad814..6d06682fca5f 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/encode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -62,6 +62,7 @@ fn encode(&self, encoder: &mut $crate::gsp::nvkv::Encoder) -> ::kernel::error::R
}
};
}
+pub(crate) use nvkv_encode;

/// A value with a specific index that encodes under the NVKV key `KEY_ID`.
struct IndexedKey<T, const KEY_ID: KeyId> {

--
2.55.0