* [PATCH v2 0/8] gpu: nova-core: add NVKV codec
@ 2026-08-27 14:12 Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 1/8] rust: alloc: add Vec::try_push_init Eliot Courtney
` (7 more replies)
0 siblings, 8 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
This series adds support for the NVKV wire format for communicating
with GSP.
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 (RPC interface
used in firmwares later than r570). 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.
This series adds a general encoder and decoder that works with the
base formats used ([u8], u32, u64, [u32], or a [u64]) for encode and a
general `Schema` trait for decode. This could be used directly, but
since most messages are struct-like, it's more ergonomic to use some
typed helpers for this declarative use case. So this series adds two
simple macros for encode and decode of structs, plus some general
types and implementations that help with using these.
Future patches will wire this up through the command queue.
This is based on drm-rust-next.
---
Changes in v2:
- Tweak Schema trait to avoid materializing copies on the stack
- Move ArrayVec to alloc module (+add potentially useful methods) (Danilo)
- Add Vec::try_push_init (Danilo)
- Add sum error type for try_push_init, PushInitError
- Use Danilo's EncodedStream abstraction
- Add some stack space asserts (Danilo) - needs to skip on clippy tho
- Using zerocopy e.g. ref_from_prefix_with_elems requires updated zerocopy to
avoid linking error, so added those as prereqs.
- Link to v1: https://patch.msgid.link/20260817-b4-nvkv-v1-0-b84db5e84b67@nvidia.com
---
Eliot Courtney (8):
rust: alloc: add Vec::try_push_init
rust: alloc: add Vec::push_init
rust: alloc: add ArrayVec
gpu: nova-core: add NVKV encoder
gpu: nova-core: add NVKV decoder
gpu: nova-core: add NVKV typed encoding
gpu: nova-core: add NVKV typed decoding
gpu: nova-core: add NVKV GSP_INIT schemas
drivers/gpu/nova-core/gsp.rs | 1 +
drivers/gpu/nova-core/gsp/fw/commands.rs | 350 +++++++++++++++
drivers/gpu/nova-core/gsp/nvkv.rs | 197 ++++++++
drivers/gpu/nova-core/gsp/nvkv/decode.rs | 741 +++++++++++++++++++++++++++++++
drivers/gpu/nova-core/gsp/nvkv/encode.rs | 388 ++++++++++++++++
rust/kernel/alloc.rs | 3 +
rust/kernel/alloc/arrayvec.rs | 347 +++++++++++++++
rust/kernel/alloc/kvec.rs | 73 ++-
rust/kernel/alloc/kvec/errors.rs | 30 ++
9 files changed, 2128 insertions(+), 2 deletions(-)
---
base-commit: 4c9ba407018e8deb06dbc643112bac8f40404f95
change-id: 20260812-b4-nvkv-131af5c2661c
prerequisite-message-id: 20260625231919.692444-1-ojeda@kernel.org
prerequisite-patch-id: 1387aaf8fd9dbfaf04e6e8d52c3b39fbd9ff9432
prerequisite-message-id: 20260709211311.142544-1-ojeda@kernel.org
prerequisite-patch-id: 75036fb19a16a9c89bcba34e233a4b6de4ae421e
Best regards,
--
Eliot Courtney <ecourtney@nvidia.com>
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 1/8] rust: alloc: add Vec::try_push_init
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 2/8] rust: alloc: add Vec::push_init Eliot Courtney
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
Add `Vec::try_push_init` for fallible initializers (`impl Init<T, E>`)
and a new sum error type `PushInitError<I, E>` that it returns. If
allocation fails, it hands back the original initializer. A From impl
for `Error` lets callers decay the `PushInitError<I, E>` to a regular
Error if they want.
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
rust/kernel/alloc/kvec.rs | 56 ++++++++++++++++++++++++++++++++++++++--
rust/kernel/alloc/kvec/errors.rs | 30 +++++++++++++++++++++
2 files changed, 84 insertions(+), 2 deletions(-)
diff --git a/rust/kernel/alloc/kvec.rs b/rust/kernel/alloc/kvec.rs
index c7546b9da4fa..fe86530624c1 100644
--- a/rust/kernel/alloc/kvec.rs
+++ b/rust/kernel/alloc/kvec.rs
@@ -52,10 +52,18 @@
}, //
};
-use pin_init::Zeroable;
+use pin_init::{
+ Init,
+ Zeroable, //
+};
mod errors;
-pub use self::errors::{InsertError, PushError, RemoveError};
+pub use self::errors::{
+ InsertError,
+ PushError,
+ PushInitError,
+ RemoveError, //
+};
/// Create a [`KVec`] containing the arguments.
///
@@ -359,6 +367,49 @@ pub fn push(&mut self, v: T, flags: Flags) -> Result<(), AllocError> {
Ok(())
}
+ /// Appends an element to the back of the [`Vec`] instance by initializing it in place.
+ ///
+ /// Unlike [`Vec::push`], the initializer may be fallible. If the allocation fails, the
+ /// original initializer `init` is handed back in [`PushInitError::AllocError`]. If the
+ /// initializer itself fails, its error is returned in [`PushInitError::InitError`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// struct Element {
+ /// buf: KVec<u8>,
+ /// }
+ ///
+ /// impl Element {
+ /// fn new() -> impl Init<Self, Error> {
+ /// try_init!(Element {
+ /// buf: KVec::with_capacity(16, GFP_KERNEL)?,
+ /// }? Error)
+ /// }
+ /// }
+ ///
+ /// let mut v: KVec<Element> = KVec::new();
+ /// v.try_push_init(Element::new(), GFP_KERNEL)?;
+ /// assert!(v[0].buf.is_empty());
+ /// # Ok::<(), Error>(())
+ /// ```
+ pub fn try_push_init<I, E>(&mut self, init: I, flags: Flags) -> Result<(), PushInitError<I, E>>
+ where
+ I: Init<T, E>,
+ {
+ if self.reserve(1, flags).is_err() {
+ return Err(PushInitError::AllocError(init));
+ }
+ // SAFETY: The call to `reserve` was successful, so there is at least one spare slot.
+ unsafe { init.__init(self.spare_capacity_mut().as_mut_ptr().cast::<T>()) }
+ .map_err(PushInitError::InitError)?;
+ // SAFETY: The call to `__init` returned `Ok`, so the first spare slot now holds an
+ // initialized `T`. The new length does not exceed the capacity because `reserve` ensured
+ // the capacity is greater than the length by at least one.
+ unsafe { self.inc_len(1) };
+ Ok(())
+ }
+
/// Appends an element to the back of the [`Vec`] instance without reallocating.
///
/// Fails if the vector does not have capacity for the new element.
@@ -1174,6 +1225,7 @@ fn eq(&self, other: &$rhs) -> bool { self[..] == other[..] }
)*
}
}
+pub(super) use impl_slice_eq;
impl_slice_eq! {
[A1: Allocator, A2: Allocator] Vec<T, A1>, Vec<U, A2>,
diff --git a/rust/kernel/alloc/kvec/errors.rs b/rust/kernel/alloc/kvec/errors.rs
index aaca6446516a..4e4be9a46d83 100644
--- a/rust/kernel/alloc/kvec/errors.rs
+++ b/rust/kernel/alloc/kvec/errors.rs
@@ -25,6 +25,36 @@ fn from(_: PushError<T>) -> Error {
}
}
+/// Error type for [`Vec::try_push_init`].
+pub enum PushInitError<I, E> {
+ /// The allocation failed. Hand the initializer back.
+ AllocError(I),
+ /// The initializer failed.
+ InitError(E),
+}
+
+impl<I, E> fmt::Debug for PushInitError<I, E> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ PushInitError::AllocError(_) => write!(f, "Failed to allocate"),
+ PushInitError::InitError(_) => write!(f, "Initializer failed"),
+ }
+ }
+}
+
+impl<I, E> From<PushInitError<I, E>> for Error
+where
+ Error: From<E>,
+{
+ #[inline]
+ fn from(e: PushInitError<I, E>) -> Error {
+ match e {
+ PushInitError::AllocError(_) => ENOMEM,
+ PushInitError::InitError(e) => Error::from(e),
+ }
+ }
+}
+
/// Error type for [`Vec::remove`].
pub struct RemoveError;
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 2/8] rust: alloc: add Vec::push_init
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 1/8] rust: alloc: add Vec::try_push_init Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 3/8] rust: alloc: add ArrayVec Eliot Courtney
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
Add `Vec::push_init` which is the init-infallible version of
`try_push_init`.
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
rust/kernel/alloc/kvec.rs | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/rust/kernel/alloc/kvec.rs b/rust/kernel/alloc/kvec.rs
index fe86530624c1..bb4da220293b 100644
--- a/rust/kernel/alloc/kvec.rs
+++ b/rust/kernel/alloc/kvec.rs
@@ -369,7 +369,24 @@ pub fn push(&mut self, v: T, flags: Flags) -> Result<(), AllocError> {
/// Appends an element to the back of the [`Vec`] instance by initializing it in place.
///
- /// Unlike [`Vec::push`], the initializer may be fallible. If the allocation fails, the
+ /// # Examples
+ ///
+ /// ```
+ /// use pin_init::init_zeroed;
+ ///
+ /// let mut v = KVec::<[u8; 200]>::new();
+ /// v.push_init(init_zeroed(), GFP_KERNEL)?;
+ /// assert_eq!(v[0], [0; 200]);
+ /// # Ok::<(), Error>(())
+ /// ```
+ pub fn push_init(&mut self, init: impl Init<T>, flags: Flags) -> Result<(), AllocError> {
+ self.try_push_init(init, flags)
+ .map_err(|PushInitError::AllocError(_)| AllocError)
+ }
+
+ /// Appends an element to the back of the [`Vec`] instance by initializing it in place.
+ ///
+ /// Unlike [`Vec::push_init`], the initializer may be fallible. If the allocation fails, the
/// original initializer `init` is handed back in [`PushInitError::AllocError`]. If the
/// initializer itself fails, its error is returned in [`PushInitError::InitError`].
///
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 3/8] rust: alloc: add ArrayVec
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 1/8] rust: alloc: add Vec::try_push_init Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 2/8] rust: alloc: add Vec::push_init Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 4/8] gpu: nova-core: add NVKV encoder Eliot Courtney
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
Add a fixed capacity vector backed by [MaybeUninit<T>; N]. The ArrayVec
is also initializable with a closure, returning an Init instance, to
avoid constructing it on the stack. ArrayVec is useful for small but
varying size arrays stored on the stack, to avoid a heap allocation, or,
for larger varying size arrays initialized into caller provided memory
but not wanting to provide an allocator.
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
rust/kernel/alloc.rs | 3 +
rust/kernel/alloc/arrayvec.rs | 347 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 350 insertions(+)
diff --git a/rust/kernel/alloc.rs b/rust/kernel/alloc.rs
index 21067bde6860..510e2c7f9f72 100644
--- a/rust/kernel/alloc.rs
+++ b/rust/kernel/alloc.rs
@@ -3,10 +3,13 @@
//! Implementation of the kernel's memory allocation infrastructure.
pub mod allocator;
+pub mod arrayvec;
pub mod kbox;
pub mod kvec;
pub mod layout;
+pub use self::arrayvec::ArrayVec;
+
pub use self::kbox::Box;
pub use self::kbox::KBox;
pub use self::kbox::KVBox;
diff --git a/rust/kernel/alloc/arrayvec.rs b/rust/kernel/alloc/arrayvec.rs
new file mode 100644
index 000000000000..4172a982e477
--- /dev/null
+++ b/rust/kernel/alloc/arrayvec.rs
@@ -0,0 +1,347 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Implementation of [`ArrayVec`].
+
+use crate::{
+ alloc::kvec::{
+ impl_slice_eq,
+ PushError, //
+ },
+ const_assert,
+ error::{
+ code::EINVAL,
+ Error,
+ Result, //
+ },
+ fmt, //
+};
+
+use core::{
+ borrow::{
+ Borrow,
+ BorrowMut, //
+ },
+ mem::MaybeUninit,
+ ops::{
+ Deref,
+ DerefMut, //
+ },
+ ptr,
+ slice, //
+};
+
+use pin_init::{
+ init_from_closure,
+ Init,
+ Zeroable, //
+};
+
+/// A fixed capacity vector that holds at most `N` elements.
+///
+/// # Invariants
+///
+/// - `len` is at most `N`.
+/// - The first `len` elements of `data` are initialized.
+///
+/// # Examples
+///
+/// ```
+/// use kernel::alloc::ArrayVec;
+///
+/// let mut v = ArrayVec::<u8, 4>::new();
+/// v.extend_from_slice(b"abc")?;
+/// assert_eq!(*v, *b"abc");
+///
+/// assert!(v.extend_from_slice(b"ab").is_err());
+///
+/// v.push(4u8)?;
+/// assert_eq!(*v, *b"abc\x04");
+/// assert!(v.push(5u8).is_err());
+///
+/// v.clear();
+/// assert!(v.is_empty());
+/// # Ok::<(), Error>(())
+/// ```
+#[derive(Zeroable)]
+pub struct ArrayVec<T, const N: usize> {
+ data: [MaybeUninit<T>; N],
+ len: usize,
+}
+
+impl<T, const N: usize> ArrayVec<T, N> {
+ /// Creates an empty [`ArrayVec`].
+ #[inline]
+ pub const fn new() -> Self {
+ // Clippy triggers this even if the enclosing function is never called, so skip if clippy is
+ // on.
+ const_assert!(
+ cfg!(clippy) || size_of::<Self>() <= 512,
+ "use `init_with` instead of constructing a large ArrayVec on the stack"
+ );
+
+ // INVARIANT: An empty ArrayVec trivially has all its elements initialized.
+ Self {
+ data: [const { MaybeUninit::uninit() }; N],
+ len: 0,
+ }
+ }
+
+ /// Creates an initializer for an [`ArrayVec`] populated by `f`.
+ ///
+ /// `f` gets an empty [`ArrayVec`] and can fill it in place.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use kernel::alloc::ArrayVec;
+ ///
+ /// let v = KBox::init(
+ /// ArrayVec::<u8, 4096>::init_with(|v| v.extend_from_slice(b"abc")),
+ /// GFP_KERNEL,
+ /// )?;
+ /// assert_eq!(**v, *b"abc");
+ /// # Ok::<(), Error>(())
+ /// ```
+ pub fn init_with<E>(f: impl FnOnce(&mut Self) -> Result<(), E>) -> impl Init<Self, E> {
+ let init = move |slot: *mut Self| {
+ // SAFETY: By the initializer contract `slot` is valid for writes. Once `len` is zero
+ // the slot holds a valid empty ArrayVec, since `data` requires no initialization.
+ // INVARIANT: An empty ArrayVec trivially has all its elements initialized.
+ unsafe { ptr::addr_of_mut!((*slot).len).write(0) };
+
+ // SAFETY: `slot` holds a valid ArrayVec and no other reference to it exists.
+ let v = unsafe { &mut *slot };
+ f(v).inspect_err(|_| {
+ // SAFETY: `slot` holds a valid ArrayVec, and on failure the slot is never accessed
+ // again, so the elements can't be dropped twice.
+ unsafe { ptr::drop_in_place(slot) }
+ })
+ };
+
+ // SAFETY: `init` fully initializes the slot on success and drops the potentially filled
+ // ArrayVec on failure.
+ unsafe { init_from_closure(init) }
+ }
+
+ /// Appends an element to the back of the [`ArrayVec`].
+ ///
+ /// Fails when the [`ArrayVec`] is full, handing the element back in [`PushError`].
+ pub fn push(&mut self, v: T) -> Result<(), PushError<T>> {
+ self.try_push_init(v)
+ .map_err(|PushInitError::Full(v)| PushError(v))
+ }
+
+ /// Appends an element to the back of the [`ArrayVec`] by initializing it in place.
+ ///
+ /// Fails with [`FullError`] when the [`ArrayVec`] is full.
+ pub fn push_init(&mut self, init: impl Init<T>) -> Result<(), FullError> {
+ self.try_push_init(init)
+ .map_err(|PushInitError::Full(_)| FullError)
+ }
+
+ /// Appends an element to the back of the [`ArrayVec`] by initializing it in place.
+ ///
+ /// Unlike [`ArrayVec::push_init`], the initializer may be fallible. If the [`ArrayVec`] is
+ /// full, the original initializer `init` is handed back in [`PushInitError::Full`]. If the
+ /// initializer itself fails, its error is returned in [`PushInitError::InitError`].
+ pub fn try_push_init<I, E>(&mut self, init: I) -> Result<(), PushInitError<I, E>>
+ where
+ I: Init<T, E>,
+ {
+ let Some(slot) = self.spare_capacity_mut().first_mut() else {
+ return Err(PushInitError::Full(init));
+ };
+
+ // SAFETY: `slot` refers to allocated, aligned memory valid for a write of one `T`.
+ unsafe { init.__init(slot.as_mut_ptr()) }.map_err(PushInitError::InitError)?;
+
+ // INVARIANT: The element at index `len` was just initialized, and the new `len` does not
+ // exceed `N` because a spare slot existed.
+ self.len += 1;
+
+ Ok(())
+ }
+
+ /// Appends a clone of each element in `slice` to the back of the [`ArrayVec`].
+ ///
+ /// Fails with [`EINVAL`] if `slice` is longer than the remaining capacity.
+ pub fn extend_from_slice(&mut self, slice: &[T]) -> Result
+ where
+ T: Clone,
+ {
+ let Some(dst) = self.spare_capacity_mut().get_mut(..slice.len()) else {
+ return Err(EINVAL);
+ };
+
+ for (d, s) in dst.iter_mut().zip(slice) {
+ d.write(s.clone());
+ }
+ // INVARIANT: The next `slice.len()` elements after `len` were just initialized, and the
+ // new `len` does not exceed `N` because the spare capacity was enough.
+ self.len += slice.len();
+
+ Ok(())
+ }
+
+ /// Removes all elements.
+ #[inline]
+ pub fn clear(&mut self) {
+ let elems: *mut [T] = self.as_mut_slice();
+ // INVARIANT: An empty ArrayVec trivially has all its elements initialized.
+ self.len = 0;
+ // SAFETY: There are no references to the elements since we hold `&mut self`. The elements
+ // can't be dropped again because `len` is already 0.
+ unsafe { ptr::drop_in_place(elems) };
+ }
+
+ /// Returns the initialized elements as a slice.
+ #[inline]
+ pub fn as_slice(&self) -> &[T] {
+ let ptr = self.data.as_ptr().cast::<T>();
+ // SAFETY: `MaybeUninit<T>` has the same layout as `T`, and by the type invariants the first
+ // `len` elements of `data` are initialized.
+ unsafe { slice::from_raw_parts(ptr, self.len) }
+ }
+
+ /// Returns the initialized elements as a mutable slice.
+ #[inline]
+ pub fn as_mut_slice(&mut self) -> &mut [T] {
+ let ptr = self.data.as_mut_ptr().cast::<T>();
+ // SAFETY: `MaybeUninit<T>` has the same layout as `T`, and by the type invariants the first
+ // `len` elements of `data` are initialized.
+ unsafe { slice::from_raw_parts_mut(ptr, self.len) }
+ }
+
+ /// Returns a slice of `MaybeUninit<T>` for the remaining spare capacity of the [`ArrayVec`].
+ fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit<T>] {
+ // PANIC: `len` never exceeds `N` by the type invariants.
+ &mut self.data[self.len..]
+ }
+}
+
+/// Error type for [`ArrayVec::try_push_init`].
+pub enum PushInitError<I, E> {
+ /// The [`ArrayVec`] is full. Hand the initializer back.
+ Full(I),
+ /// The initializer failed.
+ InitError(E),
+}
+
+impl<I, E> fmt::Debug for PushInitError<I, E> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ PushInitError::Full(_) => write!(f, "Not enough capacity"),
+ PushInitError::InitError(_) => write!(f, "Initializer failed"),
+ }
+ }
+}
+
+impl<I, E> From<PushInitError<I, E>> for Error
+where
+ Error: From<E>,
+{
+ #[inline]
+ fn from(e: PushInitError<I, E>) -> Error {
+ match e {
+ PushInitError::Full(_) => EINVAL,
+ PushInitError::InitError(e) => Error::from(e),
+ }
+ }
+}
+
+/// Error type for [`ArrayVec::push_init`].
+pub struct FullError;
+
+impl fmt::Debug for FullError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Not enough capacity")
+ }
+}
+
+impl From<FullError> for Error {
+ #[inline]
+ fn from(_: FullError) -> Error {
+ EINVAL
+ }
+}
+
+impl<T, const N: usize> Default for ArrayVec<T, N> {
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<T, const N: usize> Drop for ArrayVec<T, N> {
+ fn drop(&mut self) {
+ // SAFETY: The slice holds initialized elements that are never accessed again after this
+ // point.
+ unsafe { ptr::drop_in_place(self.as_mut_slice()) };
+ }
+}
+
+impl<T, const N: usize> Deref for ArrayVec<T, N> {
+ type Target = [T];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ self.as_slice()
+ }
+}
+
+impl<T, const N: usize> DerefMut for ArrayVec<T, N> {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.as_mut_slice()
+ }
+}
+
+impl<T, const N: usize> Borrow<[T]> for ArrayVec<T, N> {
+ fn borrow(&self) -> &[T] {
+ self.as_slice()
+ }
+}
+
+impl<T, const N: usize> BorrowMut<[T]> for ArrayVec<T, N> {
+ fn borrow_mut(&mut self) -> &mut [T] {
+ self.as_mut_slice()
+ }
+}
+
+impl<T: Eq, const N: usize> Eq for ArrayVec<T, N> {}
+
+impl_slice_eq! {
+ [const N: usize, const M: usize] ArrayVec<T, N>, ArrayVec<U, M>,
+ [const N: usize] ArrayVec<T, N>, &[U],
+ [const N: usize] ArrayVec<T, N>, &mut [U],
+ [const N: usize] &[T], ArrayVec<U, N>,
+ [const N: usize] &mut [T], ArrayVec<U, N>,
+ [const N: usize] ArrayVec<T, N>, [U],
+ [const N: usize] [T], ArrayVec<U, N>,
+ [const N: usize, const M: usize] ArrayVec<T, N>, [U; M],
+ [const N: usize, const M: usize] ArrayVec<T, N>, &[U; M],
+}
+
+impl<'a, T, const N: usize> IntoIterator for &'a ArrayVec<T, N> {
+ type Item = &'a T;
+ type IntoIter = slice::Iter<'a, T>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+impl<'a, T, const N: usize> IntoIterator for &'a mut ArrayVec<T, N> {
+ type Item = &'a mut T;
+ type IntoIter = slice::IterMut<'a, T>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter_mut()
+ }
+}
+
+impl<T: fmt::Debug, const N: usize> fmt::Debug for ArrayVec<T, N> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(self.as_slice(), f)
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 4/8] gpu: nova-core: add NVKV encoder
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
` (2 preceding siblings ...)
2026-08-27 14:12 ` [PATCH v2 3/8] rust: alloc: add ArrayVec Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 5/8] gpu: nova-core: add NVKV decoder Eliot Courtney
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
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@nvidia.com>
---
drivers/gpu/nova-core/gsp.rs | 1 +
drivers/gpu/nova-core/gsp/nvkv.rs | 142 +++++++++++++++++++++
drivers/gpu/nova-core/gsp/nvkv/encode.rs | 210 +++++++++++++++++++++++++++++++
3 files changed, 353 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..a8e16687a134
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -0,0 +1,142 @@
+// 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 core::ops::Deref;
+
+use kernel::{
+ alloc::{
+ allocator::KVmalloc,
+ Allocator, //
+ },
+ bitfield,
+ num::Bounded,
+ prelude::*, //
+};
+use zerocopy::Immutable;
+
+mod encode;
+pub(crate) use encode::*;
+
+/// The allocator backing [`EncodedStream`].
+type StreamAllocator = KVmalloc;
+
+/// An encoded NVKV byte stream.
+///
+/// # Invariants
+///
+/// The byte length is always a multiple of `size_of::<u64>()`.
+pub(crate) struct EncodedStream(Vec<u8, StreamAllocator>);
+
+impl EncodedStream {
+ /// Creates an empty stream.
+ fn new() -> Self {
+ // INVARIANT: An empty stream's byte length is 0, a multiple of `size_of::<u64>()`.
+ Self(Vec::new())
+ }
+
+ /// Appends a single `u64` to the stream.
+ fn push_u64(&mut self, value: u64) -> Result {
+ // INVARIANT: Appending `size_of::<u64>()` bytes keeps the byte length a multiple of
+ // `size_of::<u64>()`.
+ Ok(self.0.extend_from_slice(&value.to_ne_bytes(), GFP_KERNEL)?)
+ }
+
+ /// Appends `data` as bytes to the stream, zero-padded to a `u64` boundary.
+ fn extend_with_padding<T: IntoBytes + Immutable + ?Sized>(&mut self, data: &T) -> Result {
+ let bytes = data.as_bytes();
+ let padded = bytes.len().next_multiple_of(size_of::<u64>());
+ // Reserve so that a failed allocation can't leave the invariant violated.
+ self.0.reserve(padded, GFP_KERNEL)?;
+ self.0.extend_from_slice(bytes, GFP_KERNEL)?;
+ // INVARIANT: The padding ensures the total length remains a multiple of
+ // `size_of::<u64>()`.
+ Ok(self.0.extend_with(padded - bytes.len(), 0u8, GFP_KERNEL)?)
+ }
+}
+
+// The Deref to &[u64] relies on this alignment guarantee.
+static_assert!(align_of::<u64>() <= StreamAllocator::MIN_ALIGN);
+
+impl Deref for EncodedStream {
+ type Target = [u64];
+
+ fn deref(&self) -> &Self::Target {
+ // An empty `Vec`'s pointer isn't necessarily aligned by `StreamAllocator::MIN_ALIGN`.
+ if self.0.is_empty() {
+ return &[];
+ }
+
+ // PANIC: By the type invariants the byte length is a multiple of `size_of::<u64>()`, and
+ // the backing buffer of a non-empty vector has at least `u64` alignment per
+ // `StreamAllocator`'s minimum alignment.
+ <[u64]>::ref_from_bytes(&self.0).expect("EncodedStream invariant violated")
+ }
+}
+
+/// 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..6c1a9cbd90e8
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -0,0 +1,210 @@
+// 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::{
+ EncodedStream,
+ Index,
+ KeyId,
+ Op,
+ Opcode, //
+};
+
+/// An encoder for an NVKV stream.
+pub(crate) struct Encoder {
+ stream: EncodedStream,
+}
+
+impl Encoder {
+ /// Creates an empty encoder.
+ pub(crate) fn new() -> Self {
+ Self {
+ stream: EncodedStream::new(),
+ }
+ }
+
+ /// Returns the encoded data.
+ #[must_use = "encoded stream must be consumed"]
+ pub(crate) fn finish(self) -> EncodedStream {
+ self.stream
+ }
+
+ #[inline(always)]
+ fn encode_op(&mut self, op: Op) -> Result {
+ self.stream.push_u64(op.into_raw())
+ }
+
+ /// 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),
+ )
+ }
+
+ /// 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.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Seq64)
+ .with_value(KEY_COUNT),
+ )?;
+ self.stream.push_u64(value)
+ }
+
+ /// 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)?;
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Array8)
+ .with_value(value_count),
+ )?;
+ self.stream.extend_with_padding(array)
+ }
+
+ /// 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)?;
+ self.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Array32)
+ .with_value(value_count),
+ )?;
+ self.stream.extend_with_padding(array)
+ }
+
+ /// 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.encode_op(
+ Op::zeroed()
+ .with_key(key)
+ .with_index(index)
+ .with_opcode(Opcode::Array64)
+ .with_value(value_count),
+ )?;
+ self.stream.extend_with_padding(array)
+ }
+}
+
+#[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
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 5/8] gpu: nova-core: add NVKV decoder
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
` (3 preceding siblings ...)
2026-08-27 14:12 ` [PATCH v2 4/8] gpu: nova-core: add NVKV encoder Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 6/8] gpu: nova-core: add NVKV typed encoding Eliot Courtney
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
Add a decoder for NVKV. This is for receiving messages from GSP for
GMCAPI calls. The NVKV format essentially encodes a sequence of function
calls f(key, index, value). This decoder reads an encoded stream and
invokes a type implementing the new `Schema` visitor trait. The
`Schema` trait can either consume the value or not, which is useful for
composing Schemas. If a (key, index, value) is not consumed, error out
depending on `UnknownKeyPolicy`. Whether ignoring unknown keys is ok or
not is per each GMCAPI call.
Add kunit tests for the decoder.
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
drivers/gpu/nova-core/gsp/nvkv.rs | 3 +
drivers/gpu/nova-core/gsp/nvkv/decode.rs | 265 +++++++++++++++++++++++++++++++
2 files changed, 268 insertions(+)
diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index a8e16687a134..cbeee7f376b6 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -27,6 +27,9 @@
mod encode;
pub(crate) use encode::*;
+mod decode;
+pub(crate) use decode::*;
+
/// The allocator backing [`EncodedStream`].
type StreamAllocator = KVmalloc;
diff --git a/drivers/gpu/nova-core/gsp/nvkv/decode.rs b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
new file mode 100644
index 000000000000..ceb97e73e100
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
@@ -0,0 +1,265 @@
+// 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 crate::gsp::nvkv::{
+ Index,
+ KeyId,
+ Op,
+ Opcode, //
+};
+use crate::num;
+
+/// A decoded NVKV value.
+#[derive(Copy, Clone)]
+pub(crate) enum DecoderValue<'a> {
+ Scalar32(u32),
+ Scalar64(u64),
+ Array8(&'a [u8]),
+ Array32(&'a [u32]),
+ Array64(&'a [u64]),
+}
+
+/// Implements `TryFrom` from the given `DecoderValue` variant to the given type.
+///
+/// `TryFrom` is used by the `Schema` implementations in this file to convert from the
+/// `DecoderValue`s into the types to store. Provide the implementations for basic types here.
+macro_rules! impl_try_from_decoder_value {
+ ($ty:ty, $variant:ident) => {
+ impl<'a> TryFrom<DecoderValue<'a>> for $ty {
+ type Error = Error;
+
+ fn try_from(value: DecoderValue<'a>) -> Result<Self> {
+ if let DecoderValue::$variant(v) = value {
+ Ok(v)
+ } else {
+ Err(EINVAL)
+ }
+ }
+ }
+ };
+}
+
+impl_try_from_decoder_value!(u32, Scalar32);
+impl_try_from_decoder_value!(u64, Scalar64);
+impl_try_from_decoder_value!(&'a [u8], Array8);
+impl_try_from_decoder_value!(&'a [u32], Array32);
+impl_try_from_decoder_value!(&'a [u64], Array64);
+
+/// A visitor that consumes decoded NVKV and produces a `Target`.
+pub(crate) trait Schema {
+ type Target;
+
+ /// Visits one decoded pair. Returns `Ok(true)` if the schema consumed it.
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool>;
+
+ /// Returns an initializer that makes the decoded `Target`.
+ ///
+ /// After the returned initializer runs, the schema should be empty again.
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_;
+}
+
+/// A read position in an NVKV stream.
+struct Cursor<'a> {
+ data: &'a [u64],
+}
+
+impl<'a> Cursor<'a> {
+ fn new(data: &'a [u64]) -> Self {
+ Self { data }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.data.is_empty()
+ }
+
+ fn take_u64(&mut self) -> Result<u64> {
+ // PANIC: `take_u64s(1)` returns exactly one element on success.
+ Ok(self.take_u64s(1)?[0])
+ }
+
+ fn take_u8s(&mut self, count: usize) -> Result<&[u8]> {
+ let values = self.take_u64s(count.div_ceil(8))?;
+ values.as_bytes().get(..count).ok_or(EINVAL)
+ }
+
+ fn take_u32s(&mut self, count: usize) -> Result<&[u32]> {
+ let values = self.take_u64s(count.div_ceil(2))?;
+ <[u32]>::ref_from_prefix_with_elems(values.as_bytes(), count)
+ .map(|(elems, _)| elems)
+ .map_err(|_| EINVAL)
+ }
+
+ fn take_u64s(&mut self, count: usize) -> Result<&[u64]> {
+ let (prefix, suffix) = self.data.split_at_checked(count).ok_or(EINVAL)?;
+ self.data = suffix;
+ Ok(prefix)
+ }
+}
+
+/// A decoder for an NVKV stream.
+pub(crate) struct Decoder<'a> {
+ data: &'a [u64],
+ policy: UnknownKeyPolicy,
+}
+
+impl<'a> Decoder<'a> {
+ /// Creates a decoder for `data` that handles unknown keys per `policy`.
+ pub(crate) fn new(data: &'a [u64], policy: UnknownKeyPolicy) -> Self {
+ Self { data, policy }
+ }
+
+ fn visit<S: Schema>(
+ &self,
+ schema: &mut S,
+ key: KeyId,
+ index: Index,
+ value: DecoderValue<'_>,
+ ) -> Result {
+ let consumed = schema.visit(key, index, value)?;
+ if !consumed && self.policy == UnknownKeyPolicy::Error {
+ Err(EINVAL)
+ } else {
+ Ok(())
+ }
+ }
+
+ fn seq_key(base: KeyId, offset: usize) -> Result<KeyId> {
+ base.checked_add(KeyId::try_from(offset)?).ok_or(EINVAL)
+ }
+
+ /// Decodes every pair into `schema` and returns the result of [`Schema::finish`].
+ pub(crate) fn decode<'s, S: Schema>(
+ &self,
+ schema: &'s mut S,
+ ) -> Result<impl Init<S::Target, Error> + 's> {
+ let mut cursor = Cursor::new(self.data);
+ while !cursor.is_empty() {
+ let op: Op = cursor.take_u64()?.into();
+
+ let key = op.key().into();
+ let index = op.index();
+ let op_value: u32 = op.value().into();
+ match op.opcode()? {
+ Opcode::Imm32 => {
+ self.visit(schema, key, index, DecoderValue::Scalar32(op_value))?;
+ }
+ Opcode::Seq32 => {
+ let values = cursor.take_u32s(num::u32_as_usize(op_value))?;
+ for (i, &value) in values.iter().enumerate() {
+ let key = Self::seq_key(key, i)?;
+ self.visit(schema, key, index, DecoderValue::Scalar32(value))?;
+ }
+ }
+ Opcode::Seq64 => {
+ let values = cursor.take_u64s(num::u32_as_usize(op_value))?;
+ for (i, &value) in values.iter().enumerate() {
+ let key = Self::seq_key(key, i)?;
+ self.visit(schema, key, index, DecoderValue::Scalar64(value))?;
+ }
+ }
+ Opcode::Array8 => {
+ let value = cursor.take_u8s(num::u32_as_usize(op_value))?;
+ self.visit(schema, key, index, DecoderValue::Array8(value))?;
+ }
+ Opcode::Array32 => {
+ let value = cursor.take_u32s(num::u32_as_usize(op_value))?;
+ self.visit(schema, key, index, DecoderValue::Array32(value))?;
+ }
+ Opcode::Array64 => {
+ let value = cursor.take_u64s(num::u32_as_usize(op_value))?;
+ self.visit(schema, key, index, DecoderValue::Array64(value))?;
+ }
+ };
+ }
+ Ok(schema.finish())
+ }
+}
+
+/// This is defined per call.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum UnknownKeyPolicy {
+ Ignore,
+ Error,
+}
+
+#[kunit_tests(nova_core_nvkv_decode)]
+mod tests {
+ use super::*;
+
+ use crate::gsp::nvkv::Encoder;
+
+ // Tests that basic decoding into a manually implemented `Schema` works correctly.
+ #[test]
+ fn decode_raw_schema() -> Result {
+ // Decodes an IMM32 pair and a SEQ64 pair (the encoder emits a u64 as a single-element
+ // SEQ64) with a hand written `Schema`. Keys and value constants chosen to distinguish e.g.
+ // saving the wrong value to the wrong location.
+ const SCALAR32_KEY: KeyId = 0x1001;
+ const SCALAR64_KEY: KeyId = 0x1002;
+ const UNKNOWN_KEY: KeyId = 0x2001;
+
+ const SCALAR32_VALUE: u32 = 0x1111_2222;
+ const SCALAR64_VALUE: u64 = 0x3333_4444_5555_6666;
+
+ // The output type of the hand written Schema. In this case, we can have it also implement
+ // `Schema` on itself rather than having a separate carrier type, since the `Schema`
+ // implementation is completely stateless.
+ #[derive(Default)]
+ struct RawSchema {
+ scalar32: u32,
+ scalar64: u64,
+ }
+
+ impl Schema for RawSchema {
+ type Target = Self;
+
+ fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'_>) -> Result<bool> {
+ if index != Index::new::<0>() {
+ return Err(EINVAL);
+ }
+ match key {
+ SCALAR32_KEY => self.scalar32 = value.try_into()?,
+ SCALAR64_KEY => self.scalar64 = value.try_into()?,
+ _ => return Ok(false),
+ }
+ Ok(true)
+ }
+
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ Ok(core::mem::take(self))
+ }
+ }
+
+ let mut encoder = Encoder::new();
+ encoder.encode_u32(SCALAR32_KEY, Index::new::<0>(), SCALAR32_VALUE)?;
+ encoder.encode_u64(SCALAR64_KEY, Index::new::<0>(), SCALAR64_VALUE)?;
+ let serialized = encoder.finish();
+
+ let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+ let mut schema = RawSchema::default();
+ let decoded = KBox::try_init(decoder.decode(&mut schema)?, GFP_KERNEL)?;
+
+ assert_eq!(decoded.scalar32, SCALAR32_VALUE);
+ assert_eq!(decoded.scalar64, SCALAR64_VALUE);
+
+ // An unknown key should fail with under `UnknownKeyPolicy::Error` and be skipped under
+ // `UnknownKeyPolicy::Ignore`.
+ let mut encoder = Encoder::new();
+ encoder.encode_u32(UNKNOWN_KEY, Index::new::<0>(), 1)?;
+
+ let serialized = encoder.finish();
+ let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+ assert!(decoder.decode(&mut RawSchema::default()).is_err());
+
+ let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Ignore);
+ let mut schema = RawSchema::default();
+ let decoded = KBox::try_init(decoder.decode(&mut schema)?, GFP_KERNEL)?;
+ assert_eq!(decoded.scalar32, 0);
+
+ Ok(())
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 6/8] gpu: nova-core: add NVKV typed encoding
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
` (4 preceding siblings ...)
2026-08-27 14:12 ` [PATCH v2 5/8] gpu: nova-core: add NVKV decoder Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 7/8] gpu: nova-core: add NVKV typed decoding Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 8/8] gpu: nova-core: add NVKV GSP_INIT schemas Eliot Courtney
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
For struct-like GMCAPI messages encoding field by field manually is
noisy. Add some type machinery and a macro to automate encoding of
struct-like messages. The `Encodeable` trait can be implemented by any
type to say that it can be encoded into an NVKV `Encoder`. Add a simple
`nvkv_encode!` macro that works on structs and encodes each field in
order. Provide some base types, such as `Key` which statically
associates a NVKV key with some value, to avoid having to make a lot of
newtypes and implement `Encodeable` on them.
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
drivers/gpu/nova-core/gsp/nvkv.rs | 49 ++++++++-
drivers/gpu/nova-core/gsp/nvkv/encode.rs | 178 +++++++++++++++++++++++++++++++
2 files changed, 226 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index cbeee7f376b6..10dcbb9e602c 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -10,8 +10,13 @@
//! naturally maps to storing a &str with the GPU name.
#![expect(unused_imports)]
+#![cfg_attr(not(CONFIG_KUNIT), expect(unused_macros))]
-use core::ops::Deref;
+use core::marker::PhantomData;
+use core::ops::{
+ Deref,
+ DerefMut, //
+};
use kernel::{
alloc::{
@@ -92,6 +97,48 @@ fn deref(&self) -> &Self::Target {
/// The index of an NVKV value.
pub(crate) type Index = Bounded<u64, 12>;
+/// A static association between an NVKV key `KEY_ID` and the storage of its value.
+///
+/// Use with the encoder or decoder macros `nvkv_encode!` and `nvkv_decode!` to let them know how to
+/// map the value `Key<T, KEY_ID, As>` to/from encoded data. For brevity, `As` inserts an additional
+/// conversion (`From`) to avoid having to implement [`Encodable`] for many types. For example,
+/// enums that are easily convertible to a u32 can have `As = u32` and rely on the existing encoding
+/// for u32.
+#[repr(transparent)]
+pub(crate) struct Key<T, const KEY_ID: KeyId, As = T>(pub(crate) T, PhantomData<As>);
+
+impl<T, const KEY_ID: KeyId, As> From<T> for Key<T, KEY_ID, As> {
+ fn from(value: T) -> Self {
+ Self(value, PhantomData)
+ }
+}
+
+impl<'a, T, const KEY_ID: KeyId, As, const N: usize> From<&'a [T; N]> for Key<&'a [T], KEY_ID, As> {
+ fn from(value: &'a [T; N]) -> Self {
+ Self(&value[..], PhantomData)
+ }
+}
+
+impl<T, const KEY_ID: KeyId, As> Deref for Key<T, KEY_ID, As> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T, const KEY_ID: KeyId, As> DerefMut for Key<T, KEY_ID, As> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl<T: Default, const KEY_ID: KeyId, As> Default for Key<T, KEY_ID, As> {
+ fn default() -> Self {
+ Self(T::default(), PhantomData)
+ }
+}
+
bitfield! {
/// The op word that starts each NVKV operation.
struct Op(u64) {
diff --git a/drivers/gpu/nova-core/gsp/nvkv/encode.rs b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
index 6c1a9cbd90e8..0047be65e8a9 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/encode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -8,11 +8,153 @@
use super::{
EncodedStream,
Index,
+ Key,
KeyId,
Op,
Opcode, //
};
+/// A type that can encode itself into an [`Encoder`].
+pub(crate) trait Encodable {
+ /// Encodes `self` into `encoder`.
+ fn encode(&self, encoder: &mut Encoder) -> Result;
+}
+
+/// Defines a struct together with its [`Encodable`] implementation.
+///
+/// The implementation encodes each field in declaration order. Each field type must implement
+/// [`Encodable`], which is done already for types like `Key<T, KEY_ID>`.
+///
+/// # Examples
+///
+/// ```
+/// nvkv_encode! {
+/// struct Request {
+/// id: Key<u32, 0x0001>,
+/// name: Key<&'static [u8], 0x0002>,
+/// }
+/// }
+/// ```
+macro_rules! nvkv_encode {
+ (
+ $(#[$attr:meta])*
+ $vis:vis struct $name:ident {
+ $(
+ $(#[$field_attr:meta])*
+ $field_vis:vis $field:ident : $ty:ty
+ ),* $(,)?
+ }
+ ) => {
+ $(#[$attr])*
+ $vis struct $name {
+ $(
+ $(#[$field_attr])*
+ $field_vis $field: $ty,
+ )*
+ }
+
+ impl $crate::gsp::nvkv::Encodable for $name {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut $crate::gsp::nvkv::Encoder) -> ::kernel::error::Result {
+ $( $crate::gsp::nvkv::Encodable::encode(&self.$field, encoder)?; )*
+ Ok(())
+ }
+ }
+ };
+}
+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> {
+ index: Index,
+ value: T,
+}
+
+impl<T, const KEY_ID: KeyId> IndexedKey<T, KEY_ID> {
+ /// Creates a key with the given index and value.
+ pub(crate) fn new(index: Index, value: T) -> Self {
+ Self { index, value }
+ }
+}
+
+impl<const KEY_ID: KeyId> Encodable for IndexedKey<u32, KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_u32(KEY_ID, self.index, self.value)
+ }
+}
+
+impl<const KEY_ID: KeyId> Encodable for IndexedKey<u64, KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_u64(KEY_ID, self.index, self.value)
+ }
+}
+
+impl<const KEY_ID: KeyId> Encodable for IndexedKey<&[u8], KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_array8(KEY_ID, self.index, self.value)
+ }
+}
+
+impl<const KEY_ID: KeyId> Encodable for IndexedKey<&[u32], KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_array32(KEY_ID, self.index, self.value)
+ }
+}
+
+impl<const KEY_ID: KeyId> Encodable for IndexedKey<&[u64], KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_array64(KEY_ID, self.index, self.value)
+ }
+}
+
+impl<const N: usize, const KEY_ID: KeyId> Encodable for IndexedKey<[u8; N], KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_array8(KEY_ID, self.index, &self.value)
+ }
+}
+
+impl<const N: usize, const KEY_ID: KeyId> Encodable for IndexedKey<[u32; N], KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_array32(KEY_ID, self.index, &self.value)
+ }
+}
+
+impl<const N: usize, const KEY_ID: KeyId> Encodable for IndexedKey<[u64; N], KEY_ID> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ encoder.encode_array64(KEY_ID, self.index, &self.value)
+ }
+}
+
+impl<T, const KEY_ID: KeyId, As> Encodable for Key<T, KEY_ID, As>
+where
+ IndexedKey<As, KEY_ID>: Encodable,
+ As: From<T>,
+ T: Copy,
+{
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ IndexedKey::new(Index::new::<0>(), As::from(self.0)).encode(encoder)
+ }
+}
+
+impl<T: Encodable> Encodable for Option<T> {
+ #[inline(always)]
+ fn encode(&self, encoder: &mut Encoder) -> Result {
+ if let Some(value) = self {
+ value.encode(encoder)?;
+ }
+ Ok(())
+ }
+}
+
/// An encoder for an NVKV stream.
pub(crate) struct Encoder {
stream: EncodedStream,
@@ -207,4 +349,40 @@ fn encode_all_value_kinds() -> Result {
Ok(())
}
+
+ // Tests that encoding via the `nvkv_encode!` macro works correctly.
+ #[test]
+ fn encode_typed_struct() -> Result {
+ const U32_KEY: KeyId = 0x0001;
+ const U64_KEY: KeyId = 0x0002;
+ const NAME_KEY: KeyId = 0x0003;
+ const FIXED_KEY: KeyId = 0x0004;
+ const OPT_KEY: KeyId = 0x0005;
+
+ nvkv_encode! {
+ struct TypedRequest {
+ a: Key<u32, { U32_KEY }>,
+ b: Key<u64, { U64_KEY }>,
+ name: Key<&'static [u8], { NAME_KEY }>,
+ fixed: Key<[u8; 4], { FIXED_KEY }>,
+ opt: Option<Key<u32, { OPT_KEY }>>,
+ }
+ }
+
+ let request = TypedRequest {
+ a: 0x89ab_cdef.into(),
+ b: 0x0123_4567_89ab_cdef.into(),
+ name: b"name\0".into(),
+ fixed: [1u8, 2, 3, 4].into(),
+ opt: None,
+ };
+
+ let mut encoder = Encoder::new();
+ request.encode(&mut encoder)?;
+ let encoded = encoder.finish();
+
+ assert_eq!(encoded.len(), 7);
+
+ Ok(())
+ }
}
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 7/8] gpu: nova-core: add NVKV typed decoding
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
` (5 preceding siblings ...)
2026-08-27 14:12 ` [PATCH v2 6/8] gpu: nova-core: add NVKV typed encoding Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 8/8] gpu: nova-core: add NVKV GSP_INIT schemas Eliot Courtney
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
Similar to the typed encoding layer, add some decoding type machinery.
Add a simple macro `nvkv_decode!` which implements `Schema` for a struct
by composing visit calls to each member. Add some common `Schema` kinds,
such as `Array` which collects an array value into a fixed maximum size
array, and `Required` which fails a decode if the value is not sent.
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
drivers/gpu/nova-core/gsp/nvkv.rs | 12 +-
drivers/gpu/nova-core/gsp/nvkv/decode.rs | 480 ++++++++++++++++++++++++++++++-
2 files changed, 488 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index 10dcbb9e602c..7d58ca91cbc3 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -9,7 +9,7 @@
//! 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)]
+#![cfg_attr(not(CONFIG_KUNIT), expect(unused_imports))]
#![cfg_attr(not(CONFIG_KUNIT), expect(unused_macros))]
use core::marker::PhantomData;
@@ -21,7 +21,8 @@
use kernel::{
alloc::{
allocator::KVmalloc,
- Allocator, //
+ Allocator,
+ ArrayVec, //
},
bitfield,
num::Bounded,
@@ -139,6 +140,13 @@ fn default() -> Self {
}
}
+/// A schema field for an array value under the NVKV key `KEY_ID`.
+#[derive(Default)]
+#[repr(transparent)]
+pub(crate) struct Array<T: Default + Copy, const N: usize, const KEY_ID: KeyId> {
+ vec: ArrayVec<T, N>,
+}
+
bitfield! {
/// The op word that starts each NVKV operation.
struct Op(u64) {
diff --git a/drivers/gpu/nova-core/gsp/nvkv/decode.rs b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
index ceb97e73e100..7f5310857764 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/decode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
@@ -3,16 +3,356 @@
#![cfg_attr(not(CONFIG_KUNIT), expect(dead_code))]
-use kernel::prelude::*;
+use core::convert::Infallible;
+use core::marker::PhantomData;
+
+use kernel::{
+ alloc::ArrayVec,
+ prelude::*, //
+};
+use pin_init::init_array_from_fn;
use crate::gsp::nvkv::{
+ Array,
Index,
+ Key,
KeyId,
Op,
Opcode, //
};
use crate::num;
+/// Defines a schema struct together with its [`Schema`] implementation that decodes into `$target`.
+///
+/// Each member of the struct should implement `Schema`. For every (key, index, value) triple
+/// decoded from the NVKV stream, the generated parent `Schema` implementation will call each member
+/// in declaration order with that triple. If a member consumes that triple, it will stop there.
+/// Otherwise it will keep going until all members are tried.
+///
+/// The schema struct holds the state required by the schema implementation to do the decode. It's
+/// recommended to use one of the existing Schema kinds (`Required`, `Accumulated`, `Key`, `Array`,
+/// `Indexed`) for each member.
+///
+/// # Examples
+///
+/// ```
+/// nvkv_decode! {
+/// struct RequestSchema => Request {
+/// id: Required<u32, 0x0001>,
+/// name: Array<u8, 64, 0x0002>,
+/// }
+/// }
+/// ```
+macro_rules! nvkv_decode {
+ (
+ $(#[$attr:meta])*
+ $vis:vis struct $name:ident => $target:ident {
+ $(
+ $(#[$field_attr:meta])*
+ $field_vis:vis $field:ident : $ty:ty
+ ),* $(,)?
+ }
+ ) => {
+ $(#[$attr])*
+ $vis struct $name {
+ $(
+ $(#[$field_attr])*
+ $field_vis $field: $ty,
+ )*
+ }
+
+ impl $crate::gsp::nvkv::Schema for $name {
+ type Target = $target;
+
+ fn init() -> impl ::kernel::prelude::Init<Self> {
+ ::pin_init::init!(Self {
+ $( $field <- <$ty as $crate::gsp::nvkv::Schema>::init(), )*
+ })
+ }
+
+ fn visit(
+ &mut self,
+ key: $crate::gsp::nvkv::KeyId,
+ index: $crate::gsp::nvkv::Index,
+ value: $crate::gsp::nvkv::DecoderValue<'_>,
+ ) -> ::kernel::error::Result<bool> {
+ Ok(false
+ $( || $crate::gsp::nvkv::Schema::visit(&mut self.$field, key, index, value)? )*)
+ }
+
+ #[inline(always)]
+ fn finish(
+ &mut self,
+ ) -> impl ::kernel::prelude::Init<Self::Target, ::kernel::error::Error> + '_ {
+ let Self { $($field,)* } = self;
+ ::kernel::try_init!(Self::Target {
+ $( $field <- $crate::gsp::nvkv::Schema::finish($field), )*
+ }? ::kernel::error::Error)
+ }
+ }
+
+ impl ::core::default::Default for $name {
+ fn default() -> Self {
+ $crate::gsp::nvkv::assert_schema_size_reasonable::<Self>();
+ Self {
+ $( $field: ::core::default::Default::default(), )*
+ }
+ }
+ }
+ };
+}
+pub(crate) use nvkv_decode;
+
+/// Asserts that a schema built by value is small enough.
+pub(crate) fn assert_schema_size_reasonable<S>() {
+ // Clippy triggers this even if the enclosing function is never called, so skip if clippy is on.
+ const_assert!(
+ cfg!(clippy) || size_of::<S>() <= 1024,
+ "construct large schemas in place with `Schema::init` instead of `Default`"
+ );
+}
+
+impl<T: for<'a> TryFrom<DecoderValue<'a>, Error = Error> + Default, const KEY_ID: KeyId> Schema
+ for Key<T, KEY_ID>
+{
+ type Target = T;
+
+ #[inline(always)]
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool> {
+ if key != KEY_ID {
+ Ok(false)
+ } else if index != Index::new::<0>() {
+ // Single values being set must be at index 0.
+ Err(EINVAL)
+ } else {
+ // Overwrite and take the latest value here.
+ self.0 = value.try_into()?;
+ Ok(true)
+ }
+ }
+
+ #[inline(always)]
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ Ok(core::mem::take(&mut self.0))
+ }
+}
+
+impl<T: for<'a> TryFrom<DecoderValue<'a>, Error = Error>, const KEY_ID: KeyId> Schema
+ for Key<Option<T>, KEY_ID>
+{
+ type Target = Option<T>;
+
+ #[inline(always)]
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool> {
+ if key != KEY_ID {
+ Ok(false)
+ } else if index != Index::new::<0>() {
+ // Single values being set must be at index 0.
+ Err(EINVAL)
+ } else {
+ // Overwrite and take the latest value here.
+ self.0 = Some(value.try_into()?);
+ Ok(true)
+ }
+ }
+
+ #[inline(always)]
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ Ok(self.0.take())
+ }
+}
+
+impl<T: Default + Copy, const N: usize, const KEY_ID: KeyId> Schema for Array<T, N, KEY_ID>
+where
+ for<'a> &'a [T]: TryFrom<DecoderValue<'a>, Error = Error>,
+{
+ type Target = ArrayVec<T, N>;
+
+ fn init() -> impl Init<Self> {
+ init!(Self {
+ vec <- ArrayVec::init_with::<Infallible>(|_| Ok(())),
+ })
+ }
+
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool> {
+ if key != KEY_ID {
+ return Ok(false);
+ }
+ // Require to be at index 0
+ if index != Index::new::<0>() {
+ return Err(EINVAL);
+ }
+ // Reject oversized and take the latest value.
+ self.vec.clear();
+ self.vec.extend_from_slice(value.try_into()?)?;
+ Ok(true)
+ }
+
+ #[inline(always)]
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ ArrayVec::init_with(move |dst| {
+ dst.extend_from_slice(&self.vec)?;
+ self.vec.clear();
+ Ok(())
+ })
+ }
+}
+
+/// A schema field for a key that must be present.
+///
+/// `finish` fails with `EINVAL` if no value arrived for the key.
+#[repr(transparent)]
+pub(crate) struct Required<T, const KEY_ID: KeyId>(Key<Option<T>, KEY_ID>);
+
+impl<T: for<'a> TryFrom<DecoderValue<'a>, Error = Error>, const KEY_ID: KeyId> Schema
+ for Required<T, KEY_ID>
+{
+ type Target = T;
+
+ #[inline(always)]
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool> {
+ self.0.visit(key, index, value)
+ }
+
+ #[inline(always)]
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ (self.0).0.take().ok_or(EINVAL)
+ }
+}
+
+impl<T, const KEY_ID: KeyId> Default for Required<T, KEY_ID> {
+ fn default() -> Self {
+ Self(None.into())
+ }
+}
+
+/// Expects objects specified sequentially with index starting from zero.
+pub(crate) struct Accumulated<S: Schema> {
+ current_index: Index,
+ current: S,
+ current_started: bool,
+ next: S,
+ accumulated: KVVec<S::Target>,
+}
+
+impl<S: Schema + Default> Accumulated<S> {
+ /// Creates an empty accumulator.
+ pub(crate) fn new() -> Self {
+ Self {
+ current_index: Index::new::<0>(),
+ current: S::default(),
+ current_started: false,
+ next: S::default(),
+ accumulated: KVVec::new(),
+ }
+ }
+
+ fn take_vec(&mut self) -> Result<KVVec<S::Target>> {
+ if self.current_started {
+ self.accumulated
+ .try_push_init(self.current.finish(), GFP_KERNEL)?;
+ self.current_started = false;
+ }
+ self.current_index = Index::new::<0>();
+ Ok(core::mem::take(&mut self.accumulated))
+ }
+}
+
+impl<S: Schema + Default> Schema for Accumulated<S> {
+ type Target = KVVec<S::Target>;
+
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool> {
+ if index != self.current_index {
+ if !self.next.visit(key, Index::new::<0>(), value)? {
+ // Unrelated key to us.
+ return Ok(false);
+ }
+
+ // Require that objects at index k have all their keys sent before the k + 1 th object
+ // can be completed. Require that objects are sent contiguously in order from index 0.
+ if !self.current_started || index != self.current_index + 1 {
+ return Err(EINVAL);
+ }
+
+ // The current value must be finished. Push it and swap in `next`.
+ self.accumulated
+ .try_push_init(self.current.finish(), GFP_KERNEL)?;
+ core::mem::swap(&mut self.current, &mut self.next);
+ self.current_started = true;
+ self.current_index = index;
+ Ok(true)
+ } else {
+ let consumed = self.current.visit(key, Index::new::<0>(), value)?;
+ self.current_started |= consumed;
+ Ok(consumed)
+ }
+ }
+
+ #[inline(always)]
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ self.take_vec()
+ }
+}
+
+impl<S: Schema + Default> Default for Accumulated<S> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// A schema field that scatters indexed values into an array of `N` slots.
+#[repr(transparent)]
+pub(crate) struct Indexed<T, const N: usize, const KEY_ID: KeyId, As = T>([T; N], PhantomData<As>);
+
+/// Copies `elems`, converted to `T`, into `slots` at `start`.
+///
+/// Fails with `EINVAL` if the window does not fit in `slots`.
+fn scatter_window<T: From<As>, As: Copy>(slots: &mut [T], start: usize, elems: &[As]) -> Result {
+ let end = start.checked_add(elems.len()).ok_or(EINVAL)?;
+ // Reject indices outside of the declared array size.
+ let dst = slots.get_mut(start..end).ok_or(EINVAL)?;
+ for (d, &e) in dst.iter_mut().zip(elems) {
+ *d = T::from(e);
+ }
+ Ok(())
+}
+
+impl<T, const N: usize, const KEY_ID: KeyId, As> Schema for Indexed<T, N, KEY_ID, As>
+where
+ T: From<As> + Default,
+ As: Copy + for<'a> TryFrom<DecoderValue<'a>, Error = Error>,
+ for<'a> &'a [As]: TryFrom<DecoderValue<'a>, Error = Error>,
+{
+ type Target = [T; N];
+
+ fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool> {
+ if key != KEY_ID {
+ return Ok(false);
+ }
+ let start = index.cast::<usize>().get();
+ // Accept both scalar vs scattered array setting for flexibility.
+ match <&[As]>::try_from(value) {
+ Ok(elems) => scatter_window(&mut self.0, start, elems)?,
+ Err(_) => scatter_window(&mut self.0, start, &[As::try_from(value)?])?,
+ }
+ Ok(true)
+ }
+
+ #[inline(always)]
+ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+ init_array_from_fn(|i| Ok::<_, Error>(core::mem::take(&mut self.0[i])))
+ }
+}
+
+impl<T: Default + Copy, const N: usize, const KEY_ID: KeyId, As> Default
+ for Indexed<T, N, KEY_ID, As>
+{
+ fn default() -> Self {
+ assert_schema_size_reasonable::<Self>();
+ Self([T::default(); N], PhantomData)
+ }
+}
+
/// A decoded NVKV value.
#[derive(Copy, Clone)]
pub(crate) enum DecoderValue<'a> {
@@ -53,12 +393,23 @@ fn try_from(value: DecoderValue<'a>) -> Result<Self> {
pub(crate) trait Schema {
type Target;
+ /// Returns an initializer that creates an empty schema in place.
+ ///
+ /// Useful if the schema is too large to fit on the stack.
+ fn init() -> impl Init<Self>
+ where
+ Self: Sized + Default,
+ {
+ Self::default()
+ }
+
/// Visits one decoded pair. Returns `Ok(true)` if the schema consumed it.
fn visit<'a>(&mut self, key: KeyId, index: Index, value: DecoderValue<'a>) -> Result<bool>;
/// Returns an initializer that makes the decoded `Target`.
///
- /// After the returned initializer runs, the schema should be empty again.
+ /// After the returned initializer runs successfully, the schema should be empty again. If the
+ /// initializer fails, the schema may hold stale state.
fn finish(&mut self) -> impl Init<Self::Target, Error> + '_;
}
@@ -262,4 +613,129 @@ fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
Ok(())
}
+
+ // Tests that decoding via the `nvkv_decode!` macro works correctly.
+ #[test]
+ fn decode_typed_struct() -> Result {
+ const SCALAR32_KEY: KeyId = 0x1234;
+ const SCALAR64_KEY: KeyId = 0x1235;
+ const ARRAY8_KEY: KeyId = 0x1236;
+ const ARRAY32_KEY: KeyId = 0x1237;
+ const ARRAY64_KEY: KeyId = 0x1238;
+ const OPT_PRESENT_KEY: KeyId = 0x1239;
+ const OPT_ABSENT_KEY: KeyId = 0x123a;
+ const X_KEY: KeyId = 0x0100;
+ const Y_KEY: KeyId = 0x0101;
+ const SLOT_KEY: KeyId = 0x0200;
+
+ const SCALAR32_VALUE: u32 = 0x89ab_cdef;
+ const SCALAR64_VALUE: u64 = 0x0123_4567_89ab_cdef;
+ const ARRAY8_VALUE: &[u8] = &[0x12, 0x34, 0x56];
+ const ARRAY32_VALUE: &[u32] = &[0x0123_4567, 0x89ab_cdef];
+ const ARRAY64_VALUE: &[u64] = &[0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210];
+ const OPT_PRESENT_VALUE: u32 = 0x55;
+
+ nvkv_decode! {
+ struct PairSchema => Pair {
+ x: Required<u32, { X_KEY }>,
+ y: Required<u32, { Y_KEY }>,
+ }
+ }
+
+ struct Pair {
+ x: u32,
+ y: u32,
+ }
+
+ nvkv_decode! {
+ struct TestSchema => TestDecodeable {
+ scalar32: Required<u32, { SCALAR32_KEY }>,
+ scalar64: Required<u64, { SCALAR64_KEY }>,
+ array8: Array<u8, 64, { ARRAY8_KEY }>,
+ array32: Array<u32, 64, { ARRAY32_KEY }>,
+ array64: Array<u64, 32, { ARRAY64_KEY }>,
+ opt_present: Key<Option<u32>, { OPT_PRESENT_KEY }>,
+ opt_absent: Key<Option<u32>, { OPT_ABSENT_KEY }>,
+ pairs: Accumulated<PairSchema>,
+ slots: Indexed<u32, 4, { SLOT_KEY }>,
+ }
+ }
+
+ struct TestDecodeable {
+ scalar32: u32,
+ scalar64: u64,
+ array8: ArrayVec<u8, 64>,
+ array32: ArrayVec<u32, 64>,
+ array64: ArrayVec<u64, 32>,
+ opt_present: Option<u32>,
+ opt_absent: Option<u32>,
+ pairs: KVVec<Pair>,
+ slots: [u32; 4],
+ }
+
+ let index0 = Index::new::<0>();
+ let index1 = Index::new::<1>();
+ let mut encoder = Encoder::new();
+ encoder.encode_u32(SCALAR32_KEY, index0, SCALAR32_VALUE)?;
+ encoder.encode_u64(SCALAR64_KEY, index0, SCALAR64_VALUE)?;
+ encoder.encode_array8(ARRAY8_KEY, index0, ARRAY8_VALUE)?;
+ encoder.encode_array32(ARRAY32_KEY, index0, ARRAY32_VALUE)?;
+ encoder.encode_array64(ARRAY64_KEY, index0, ARRAY64_VALUE)?;
+ encoder.encode_u32(OPT_PRESENT_KEY, index0, OPT_PRESENT_VALUE)?;
+ encoder.encode_u32(X_KEY, index0, 1)?;
+ encoder.encode_u32(Y_KEY, index0, 2)?;
+ encoder.encode_u32(SLOT_KEY, index1, 20)?;
+ encoder.encode_u32(X_KEY, index1, 3)?;
+ encoder.encode_u32(Y_KEY, index1, 4)?;
+ encoder.encode_u32(SLOT_KEY, index0, 10)?;
+ let serialized = encoder.finish();
+
+ let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+ let mut schema = TestSchema::default();
+ let decoded = KBox::try_init(decoder.decode(&mut schema)?, GFP_KERNEL)?;
+
+ assert_eq!(decoded.scalar32, SCALAR32_VALUE);
+ assert_eq!(decoded.scalar64, SCALAR64_VALUE);
+ assert_eq!(*decoded.array8, *ARRAY8_VALUE);
+ assert_eq!(*decoded.array32, *ARRAY32_VALUE);
+ assert_eq!(*decoded.array64, *ARRAY64_VALUE);
+ assert_eq!(decoded.opt_present, Some(OPT_PRESENT_VALUE));
+ assert_eq!(decoded.opt_absent, None);
+ assert_eq!(decoded.pairs.len(), 2);
+ assert_eq!(decoded.pairs[0].x, 1);
+ assert_eq!(decoded.pairs[0].y, 2);
+ assert_eq!(decoded.pairs[1].x, 3);
+ assert_eq!(decoded.pairs[1].y, 4);
+ assert_eq!(decoded.slots, [10, 20, 0, 0]);
+
+ Ok(())
+ }
+
+ // Tests that a schema too large for the stack decodes on the heap.
+ #[test]
+ fn decode_large_schema_on_heap() -> Result {
+ const BLOB_KEY: KeyId = 0x1400;
+ const BLOB_VALUE: &[u8] = &[0xab; 100];
+
+ nvkv_decode! {
+ struct BigSchema => BigDecodeable {
+ blob: Array<u8, 2048, { BLOB_KEY }>,
+ }
+ }
+
+ struct BigDecodeable {
+ blob: ArrayVec<u8, 2048>,
+ }
+
+ let mut encoder = Encoder::new();
+ encoder.encode_array8(BLOB_KEY, Index::new::<0>(), BLOB_VALUE)?;
+ let serialized = encoder.finish();
+
+ let mut schema = KBox::init(BigSchema::init(), GFP_KERNEL)?;
+ let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+ let decoded = KBox::try_init(decoder.decode(&mut *schema)?, GFP_KERNEL)?;
+
+ assert_eq!(*decoded.blob, *BLOB_VALUE);
+ Ok(())
+ }
}
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 8/8] gpu: nova-core: add NVKV GSP_INIT schemas
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
` (6 preceding siblings ...)
2026-08-27 14:12 ` [PATCH v2 7/8] gpu: nova-core: add NVKV typed decoding Eliot Courtney
@ 2026-08-27 14:12 ` Eliot Courtney
7 siblings, 0 replies; 9+ messages in thread
From: Eliot Courtney @ 2026-08-27 14:12 UTC (permalink / raw)
To: Danilo Krummrich, Lorenzo Stoakes, Vlastimil Babka,
Liam R. Howlett, Uladzislau Rezki, Miguel Ojeda, Boqun Feng,
Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg,
Alice Ryhl, Trevor Gross, Daniel Almeida, Tamir Duberstein,
Alexandre Courbot, Onur Özkan, David Airlie, Simona Vetter
Cc: John Hubbard, Alistair Popple, Timur Tabi, rust-for-linux,
linux-kernel, nova-gpu, dri-devel, Eliot Courtney
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@nvidia.com>
---
drivers/gpu/nova-core/gsp/fw/commands.rs | 350 +++++++++++++++++++++++++++++++
drivers/gpu/nova-core/gsp/nvkv.rs | 3 -
2 files changed, 350 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/nova-core/gsp/fw/commands.rs b/drivers/gpu/nova-core/gsp/fw/commands.rs
index 6dc31d1bf5ae..25ac1e8e7785 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::*,
@@ -19,6 +21,19 @@
num::IntoSafeCast, //
};
+use crate::gsp::nvkv::{
+ nvkv_decode,
+ nvkv_encode,
+ Accumulated,
+ Array,
+ DecoderValue,
+ Encodable,
+ Encoder,
+ Key,
+ KeyId,
+ Required, //
+};
+
use super::bindings;
/// Payload of the `GspSetSystemInfo` command.
@@ -217,3 +232,338 @@ 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,
+ Index,
+ UnknownKeyPolicy, //
+ };
+
+ use super::*;
+
+ // 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"test_key\0".into(),
+ key_value: 0xdead_beef.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(), 22);
+
+ 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 = FbRegionSchema::default();
+ 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 = Resp::default();
+ 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 = Resp::default();
+ 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 7d58ca91cbc3..e7a9549919fb 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;
use core::ops::{
Deref,
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-08-27 14:23 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-27 14:12 [PATCH v2 0/8] gpu: nova-core: add NVKV codec Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 1/8] rust: alloc: add Vec::try_push_init Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 2/8] rust: alloc: add Vec::push_init Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 3/8] rust: alloc: add ArrayVec Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 4/8] gpu: nova-core: add NVKV encoder Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 5/8] gpu: nova-core: add NVKV decoder Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 6/8] gpu: nova-core: add NVKV typed encoding Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 7/8] gpu: nova-core: add NVKV typed decoding Eliot Courtney
2026-08-27 14:12 ` [PATCH v2 8/8] gpu: nova-core: add NVKV GSP_INIT schemas Eliot Courtney
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox