[PATCH 2/6] gpu: nova-core: add NVKV encoder
From: Eliot Courtney
Date: Mon Aug 17 2026 - 08:59:42 EST
Add an encoder for NVKV, which is the wire format for GMCAPI. The
encoded stream is a sequence of 64-bit values. The first 64-bit value
encodes an op word which describes the function of the next N values.
Essentially, the format encodes a sequence of calls to some function
f(key, index, value), where value is a [u8], u32, u64, [u32], or a
[u64]. The key is a u16 and the index is a 12 bit integer. The
interpretation of these function calls is per GMCAPI.
Add tests for the wire encoding for each primitive.
Signed-off-by: Eliot Courtney <ecourtney@xxxxxxxxxx>
---
drivers/gpu/nova-core/gsp.rs | 1 +
drivers/gpu/nova-core/gsp/nvkv.rs | 79 ++++++++++
drivers/gpu/nova-core/gsp/nvkv/encode.rs | 245 +++++++++++++++++++++++++++++++
3 files changed, 325 insertions(+)
diff --git a/drivers/gpu/nova-core/gsp.rs b/drivers/gpu/nova-core/gsp.rs
index 13f361406a6c..84dfe07ae6ba 100644
--- a/drivers/gpu/nova-core/gsp.rs
+++ b/drivers/gpu/nova-core/gsp.rs
@@ -24,6 +24,7 @@
pub(crate) mod cmdq;
pub(crate) mod commands;
mod fw;
+mod nvkv;
mod regs;
mod sequencer;
diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
new file mode 100644
index 000000000000..b908f66e760d
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+//! Codec for NVKV, the binary key-value format of GMCAPI.
+//!
+//! Essentially, the format encodes a sequence of calls to some function f(key, index, value),
+//! where value is a [u8], u32, u64, [u32], or a [u64]. The key is a u16 and the index is a 12 bit
+//! integer. The interpretation of these function calls is per GMCAPI. Generally speaking, the
+//! 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.
+
+#![expect(unused_imports)]
+
+use kernel::{
+ bitfield,
+ num::Bounded,
+ prelude::*, //
+};
+
+mod encode;
+pub(crate) use encode::*;
+
+/// The identifier of an NVKV key.
+pub(crate) type KeyId = u16;
+
+/// The index of an NVKV value.
+pub(crate) type Index = Bounded<u64, 12>;
+
+bitfield! {
+ /// The op word that starts each NVKV operation.
+ struct Op(u64) {
+ 15:0 key;
+ 27:16 index => Index;
+ 31:28 opcode ?=> Opcode;
+ 63:32 value;
+ }
+}
+
+/// Describes the format of the following NVKV operation.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[repr(u8)]
+enum Opcode {
+ /// A 32-bit value in the op word.
+ Imm32 = 0,
+ /// 32-bit values for consecutive keys, starting at the op word's key.
+ Seq32 = 1,
+ /// 64-bit values for consecutive keys, starting at the op word's key.
+ Seq64 = 2,
+ /// An array of bytes.
+ Array8 = 3,
+ /// An array of 32-bit elements.
+ Array32 = 4,
+ /// An array of 64-bit elements.
+ Array64 = 5,
+}
+
+// TODO[FPRI]: This is a temporary solution to be replaced with the corresponding derive macros once
+// they land.
+impl TryFrom<Bounded<u64, 4>> for Opcode {
+ type Error = Error;
+
+ fn try_from(value: Bounded<u64, 4>) -> Result<Self> {
+ match value.get() {
+ 0 => Ok(Self::Imm32),
+ 1 => Ok(Self::Seq32),
+ 2 => Ok(Self::Seq64),
+ 3 => Ok(Self::Array8),
+ 4 => Ok(Self::Array32),
+ 5 => Ok(Self::Array64),
+ _ => Err(EINVAL),
+ }
+ }
+}
+
+impl From<Opcode> for Bounded<u64, 4> {
+ fn from(value: Opcode) -> Self {
+ Bounded::from_expr(value as u64)
+ }
+}
diff --git a/drivers/gpu/nova-core/gsp/nvkv/encode.rs b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
new file mode 100644
index 000000000000..6da81a371651
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -0,0 +1,245 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+#![cfg_attr(not(CONFIG_KUNIT), expect(dead_code))]
+
+use kernel::prelude::*;
+
+use super::{
+ Index,
+ KeyId,
+ Op,
+ Opcode, //
+};
+
+/// An encoder for an NVKV stream.
+pub(crate) struct Encoder {
+ backing: KVVec<u64>,
+}
+
+impl Encoder {
+ /// Creates an empty encoder.
+ pub(crate) fn new() -> Self {
+ Self {
+ backing: KVVec::new(),
+ }
+ }
+
+ /// Appends `bytes` to the stream, padded to a multiple of 8 bytes.
+ fn push_bytes_with_padding(&mut self, bytes: &[u8]) -> Result {
+ let num_entries = bytes.len().div_ceil(size_of::<u64>());
+ self.backing.reserve(num_entries, GFP_KERNEL)?;
+
+ let spare = self.backing.spare_capacity_mut();
+ let dst = spare.as_mut_ptr().cast::<u8>();
+
+ // SAFETY: At least `bytes.len()` bytes of space are guaranteed since `num_entries`
+ // worth of space was just reserved.
+ unsafe { core::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()) };
+
+ let padding = num_entries * size_of::<u64>() - bytes.len();
+ if padding > 0 {
+ // SAFETY: At least `num_entries * size_of::<u64>()` bytes of space are guaranteed.
+ unsafe { core::ptr::write_bytes(dst.add(bytes.len()), 0, padding) };
+ }
+
+ // SAFETY: These bytes were just initialized and every bit pattern is valid for `u64`.
+ unsafe { self.backing.inc_len(num_entries) };
+
+ Ok(())
+ }
+
+ /// Returns the encoded data.
+ #[must_use = "encoded data must be consumed"]
+ pub(crate) fn finish(self) -> KVVec<u64> {
+ self.backing
+ }
+
+ #[inline(always)]
+ fn encode_op(&mut self, op: Op) -> Result {
+ self.backing.push(op.into_raw(), GFP_KERNEL)?;
+ Ok(())
+ }
+
+ /// Encodes a 32-bit value as an IMM32 pair, with the value in the op word.
+ #[inline(always)]
+ pub(crate) fn encode_u32(&mut self, key: KeyId, index: Index, value: u32) -> Result {
+ // TODO: Consider automatically merging sequential keys.
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Imm32)
+ .with_value(value),
+ )?;
+ Ok(())
+ }
+
+ /// Encodes a 64-bit value as a single-element SEQ64 pair.
+ #[inline(always)]
+ pub(crate) fn encode_u64(&mut self, key: KeyId, index: Index, value: u64) -> Result {
+ // TODO: Consider automatically merging sequential keys.
+ const KEY_COUNT: u32 = 1;
+ self.backing.reserve(2, GFP_KERNEL)?;
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Seq64)
+ .with_value(KEY_COUNT),
+ )?;
+ self.backing.push_within_capacity(value)?;
+ Ok(())
+ }
+
+ /// Encodes a byte array as an ARRAY8 pair, zero-padded to a multiple of 8 bytes.
+ #[inline(always)]
+ pub(crate) fn encode_array8(&mut self, key: KeyId, index: Index, array: &[u8]) -> Result {
+ let value_count = u32::try_from(array.len()).map_err(|_| EMSGSIZE)?;
+ let num_entries = array.len().div_ceil(size_of::<u64>());
+ self.backing.reserve(num_entries + 1, GFP_KERNEL)?;
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Array8)
+ .with_value(value_count),
+ )?;
+ self.push_bytes_with_padding(array.as_bytes())?;
+ Ok(())
+ }
+
+ /// Encodes a 32-bit array as an ARRAY32 pair, zero-padded to a multiple of 8 bytes.
+ #[inline(always)]
+ pub(crate) fn encode_array32(&mut self, key: KeyId, index: Index, array: &[u32]) -> Result {
+ let value_count = u32::try_from(array.len()).map_err(|_| EMSGSIZE)?;
+ let num_entries = array.len().div_ceil(2);
+ self.backing.reserve(num_entries + 1, GFP_KERNEL)?;
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Array32)
+ .with_value(value_count),
+ )?;
+ self.push_bytes_with_padding(array.as_bytes())?;
+ Ok(())
+ }
+
+ /// Encodes a 64-bit array as an ARRAY64 pair.
+ #[inline(always)]
+ pub(crate) fn encode_array64(&mut self, key: KeyId, index: Index, array: &[u64]) -> Result {
+ let value_count = u32::try_from(array.len()).map_err(|_| EMSGSIZE)?;
+ self.backing.reserve(array.len() + 1, GFP_KERNEL)?;
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Array64)
+ .with_value(value_count),
+ )?;
+ self.push_bytes_with_padding(array.as_bytes())?;
+ Ok(())
+ }
+}
+
+#[kunit_tests(nova_core_nvkv_encode)]
+mod tests {
+ use super::*;
+
+ // Tests that each kind of value is encoded to NVKV wire format properly.
+ #[test]
+ fn encode_all_value_kinds() -> Result {
+ // All keys, indexes, and values are distinct but arbitrary values to make it easier for the
+ // test to catch bugs in the encoded output.
+ const U32_KEY: KeyId = 0x1001;
+ const U64_KEY: KeyId = 0x1002;
+ const ARRAY8_KEY: KeyId = 0x1003;
+ const ARRAY32_KEY: KeyId = 0x1004;
+ const ARRAY64_KEY: KeyId = 0x1005;
+
+ const U32_VALUE: u32 = 0x1111_2222;
+ const U64_VALUE: u64 = 0x3333_4444_5555_6666;
+ const ARRAY8_VALUE: &[u8] = &[0xaa, 0xbb, 0xcc];
+ const ARRAY32_VALUE: &[u32] = &[0xbbbb_cccc, 0xdddd_eeee];
+ const ARRAY64_VALUE: &[u64] = &[0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210];
+
+ let mut encoder = Encoder::new();
+ encoder.encode_u32(U32_KEY, Index::new::<0>(), U32_VALUE)?;
+ encoder.encode_u64(U64_KEY, Index::new::<1>(), U64_VALUE)?;
+ encoder.encode_array8(ARRAY8_KEY, Index::new::<2>(), ARRAY8_VALUE)?;
+ encoder.encode_array32(ARRAY32_KEY, Index::new::<3>(), ARRAY32_VALUE)?;
+ encoder.encode_array64(ARRAY64_KEY, Index::new::<4>(), ARRAY64_VALUE)?;
+
+ let encoded = encoder.finish();
+ assert_eq!(encoded.len(), 10);
+
+ // IMM32 has its value in the op word.
+ assert_eq!(
+ encoded[0],
+ Op::zeroed()
+ .with_key(U32_KEY)
+ .with_index(Index::new::<0>())
+ .with_opcode(Opcode::Imm32)
+ .with_value(U32_VALUE)
+ .into_raw()
+ );
+
+ // The SEQ64 op word followed by the value.
+ assert_eq!(
+ encoded[1],
+ Op::zeroed()
+ .with_key(U64_KEY)
+ .with_index(Index::new::<1>())
+ .with_opcode(Opcode::Seq64)
+ .with_value(1u32)
+ .into_raw()
+ );
+ assert_eq!(encoded[2], U64_VALUE);
+
+ // The ARRAY8 op word has the byte count. The bytes follow, padded out to a whole word.
+ assert_eq!(
+ encoded[3],
+ Op::zeroed()
+ .with_key(ARRAY8_KEY)
+ .with_index(Index::new::<2>())
+ .with_opcode(Opcode::Array8)
+ .with_value(3u32)
+ .into_raw()
+ );
+ assert_eq!(
+ encoded[4],
+ u64::from_le_bytes([0xaa, 0xbb, 0xcc, 0, 0, 0, 0, 0])
+ );
+
+ // The ARRAY32 op word has the element count. The two elements follow in little endian.
+ assert_eq!(
+ encoded[5],
+ Op::zeroed()
+ .with_key(ARRAY32_KEY)
+ .with_index(Index::new::<3>())
+ .with_opcode(Opcode::Array32)
+ .with_value(2u32)
+ .into_raw()
+ );
+ assert_eq!(
+ encoded[6],
+ u64::from(ARRAY32_VALUE[1]) << 32 | u64::from(ARRAY32_VALUE[0])
+ );
+
+ // The ARRAY64 op word has the element count with the two elements after.
+ assert_eq!(
+ encoded[7],
+ Op::zeroed()
+ .with_key(ARRAY64_KEY)
+ .with_index(Index::new::<4>())
+ .with_opcode(Opcode::Array64)
+ .with_value(2u32)
+ .into_raw()
+ );
+ assert_eq!(encoded[8], ARRAY64_VALUE[0]);
+ assert_eq!(encoded[9], ARRAY64_VALUE[1]);
+
+ Ok(())
+ }
+}
--
2.55.0