All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v4 0/7] gpu: nova-core: add NVKV codec
@ 2026-10-16  1:53 Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 1/7] rust: alloc: add Vec::try_push_init and Vec::push_init Eliot Courtney
                   ` (6 more replies)
  0 siblings, 7 replies; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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 v4:
- Rebase onto drm-rust-next.
- Fold Vec::push_init into the Vec::try_push_init patch (Alex)
- Rename PushInitError to TryPushInitError and replace FullError with
  kvec::PushError (Alex)
- Add #[inline] to small Vec and ArrayVec methods (Sashiko)
- Drop #[inline] and #[repr(transparent)] (Alex)
- Move IndexedKey to nvkv.rs and make it crate-visible (Alex)
- Make Required work on schemas and add Optional (Alex)
- Add stack asserts for decoded Key and Optional values (Alex)
- Drop the GSP_INIT tests, use literal key ids (Alex)
- Doc, test and type cleanups (Alex)
- Fix expect dead_code for CONFIG_KUNIT=m build
- Drop the clippy exception from the ArrayVec::new() stack assert
  It turns out this isn't just clippy, but also -Os.
- Implement ArrayVec::init_with with Init::chain (simpler)
- Allow a lifetime on nvkv_encode! structs, like on nvkv_decode! structs
- Add a stack assert for encoded Key values
- Support the As type of Key in decoding and use it for FbRegionFlags
- Add TruncatingArray and truncate a long GPU name as OpenRM does,
  instead of failing to decode
- Drop unneeded bounds on Array and Schema::init, make HostArch private
- Link to v3: https://patch.msgid.link/20260928-b4-nvkv-v3-0-f04504c262c2@nvidia.com

Changes in v3:
- Rebase + drop zerocopy prereqs which are in now.
- Define Opcode with bounded_enum! (Alex)
- Use #[inline] instead of #[inline(always)] (Alex)
- Add Co-developed-by for Danilo on the encoder patch (EncodedStream)
- Split Schema into Schema (init/finish) and Visit<'data> (visit) to let
  schemas borrow from the encoded stream
- Support schemas that borrow from the stream in nvkv_decode!, e.g.
  struct NameSchema<'d> => Name<'d>
- Construct schemas only in place with a required Schema::init() and
  drop the Default impls + stack size asserts (Alex)
- Doc improvements (Alex)
- Add a TODO for the O(#fields) visit dispatch in nvkv_decode! (Alex)
- Replace the Key<Option<T>> special case with TryFrom<DecoderValue>
  for Option<T>
- Make Key's field and IndexedKey::new private (Alex)
- Move the nvkv_*! re-exports to GSP_INIT patch (Alex)
- Add decoder tests (Alex)
- Add error path tests (Alex)
- Add IndexedKey tests (Alex)
- Add RegKey tests (Alex)
- Link to v2: https://patch.msgid.link/20260827-b4-nvkv-v2-0-0de9d5c8658c@nvidia.com

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 (7):
      rust: alloc: add Vec::try_push_init and 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 |  157 +++-
 drivers/gpu/nova-core/gsp/nvkv.rs        |  189 +++++
 drivers/gpu/nova-core/gsp/nvkv/decode.rs | 1208 ++++++++++++++++++++++++++++++
 drivers/gpu/nova-core/gsp/nvkv/encode.rs |  477 ++++++++++++
 rust/kernel/alloc.rs                     |    3 +
 rust/kernel/alloc/arrayvec.rs            |  338 +++++++++
 rust/kernel/alloc/kvec.rs                |   86 ++-
 rust/kernel/alloc/kvec/errors.rs         |   32 +-
 9 files changed, 2487 insertions(+), 4 deletions(-)
---
base-commit: 5a1b7121a6c4db6c0239d3ac3fd1c2bb33a40025
change-id: 20260812-b4-nvkv-131af5c2661c

Best regards,
--  
Eliot Courtney <ecourtney@nvidia.com>


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH v4 1/7] rust: alloc: add Vec::try_push_init and Vec::push_init
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 2/7] rust: alloc: add ArrayVec Eliot Courtney
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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 `TryPushInitError<I, E>` that it returns. If
allocation fails, it hands back the original initializer. A `From` impl
for `Error` lets callers decay the `TryPushInitError<I, E>` to a regular
Error if they want.

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        | 85 +++++++++++++++++++++++++++++++++++++++-
 rust/kernel/alloc/kvec/errors.rs | 30 ++++++++++++++
 2 files changed, 113 insertions(+), 2 deletions(-)

diff --git a/rust/kernel/alloc/kvec.rs b/rust/kernel/alloc/kvec.rs
index c7546b9da4fa..c8b312f64657 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,
+    RemoveError,
+    TryPushInitError, //
+};
 
 /// Create a [`KVec`] containing the arguments.
 ///
@@ -359,6 +367,79 @@ 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.
+    ///
+    /// # 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>(())
+    /// ```
+    #[inline]
+    pub fn push_init(&mut self, init: impl Init<T>, flags: Flags) -> Result<(), AllocError> {
+        self.try_push_init(init, flags)
+            .map_err(|TryPushInitError::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 [`TryPushInitError::AllocError`]. If the
+    /// initializer itself fails, its error is returned in [`TryPushInitError::InitError`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// struct Element {
+    ///     value: u8,
+    /// }
+    ///
+    /// impl Element {
+    ///     fn new(value: u32) -> impl Init<Self, Error> {
+    ///         try_init!(Element {
+    ///             value: u8::try_from(value)?,
+    ///         }? Error)
+    ///     }
+    /// }
+    ///
+    /// let mut v: KVec<Element> = KVec::new();
+    /// v.try_push_init(Element::new(16), GFP_KERNEL)?;
+    /// assert_eq!(v[0].value, 16);
+    ///
+    /// // The initializer fails because 256 does not fit in a `u8`.
+    /// assert!(v.try_push_init(Element::new(256), GFP_KERNEL).is_err());
+    /// assert_eq!(v.len(), 1);
+    /// # Ok::<(), Error>(())
+    /// ```
+    pub fn try_push_init<I, E>(
+        &mut self,
+        init: I,
+        flags: Flags,
+    ) -> Result<(), TryPushInitError<I, E>>
+    where
+        I: Init<T, E>,
+    {
+        if self.reserve(1, flags).is_err() {
+            return Err(TryPushInitError::AllocError(init));
+        }
+        // SAFETY: The call to `reserve` was successful, so there is at least one spare slot.
+        let slot = unsafe { self.spare_capacity_mut().get_unchecked_mut(0) }.as_mut_ptr();
+        // SAFETY:
+        // - `slot` points to a spare slot, which is valid, uninitialized memory.
+        // - If `raw_try_init` fails, this returns without increasing the length or touching `slot`.
+        // - `init` implements `Init<T, E>`, which removes the pinning requirement.
+        unsafe { pin_init::raw_try_init(slot, init) }.map_err(TryPushInitError::InitError)?;
+        // SAFETY: The call to `raw_try_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.
diff --git a/rust/kernel/alloc/kvec/errors.rs b/rust/kernel/alloc/kvec/errors.rs
index aaca6446516a..d24dfe22834a 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 TryPushInitError<I, E> {
+    /// The allocation failed. Hand the initializer back.
+    AllocError(I),
+    /// The initializer failed.
+    InitError(E),
+}
+
+impl<I, E> fmt::Debug for TryPushInitError<I, E> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            TryPushInitError::AllocError(_) => write!(f, "Failed to allocate"),
+            TryPushInitError::InitError(_) => write!(f, "Initializer failed"),
+        }
+    }
+}
+
+impl<I, E> From<TryPushInitError<I, E>> for Error
+where
+    Error: From<E>,
+{
+    #[inline]
+    fn from(e: TryPushInitError<I, E>) -> Error {
+        match e {
+            TryPushInitError::AllocError(_) => ENOMEM,
+            TryPushInitError::InitError(e) => Error::from(e),
+        }
+    }
+}
+
 /// Error type for [`Vec::remove`].
 pub struct RemoveError;
 

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v4 2/7] rust: alloc: add ArrayVec
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 1/7] rust: alloc: add Vec::try_push_init and Vec::push_init Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  2026-10-16  2:04   ` sashiko-bot
  2026-10-16  1:53 ` [PATCH v4 3/7] gpu: nova-core: add NVKV encoder Eliot Courtney
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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    | 338 +++++++++++++++++++++++++++++++++++++++
 rust/kernel/alloc/kvec.rs        |   1 +
 rust/kernel/alloc/kvec/errors.rs |   2 +-
 4 files changed, 343 insertions(+), 1 deletion(-)

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..8793adf4d27b
--- /dev/null
+++ b/rust/kernel/alloc/arrayvec.rs
@@ -0,0 +1,338 @@
+// 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,
+}
+
+/// The maximum size in bytes of an [`ArrayVec`] returned by value from [`ArrayVec::new`].
+const MAX_STACK_SIZE: usize = 512;
+
+impl<T, const N: usize> ArrayVec<T, N> {
+    /// Creates an empty [`ArrayVec`].
+    #[inline]
+    pub const fn new() -> Self {
+        const_assert!(
+            size_of::<Self>() <= MAX_STACK_SIZE,
+            "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>(())
+    /// ```
+    #[inline]
+    pub fn init_with<E>(f: impl FnOnce(&mut Self) -> Result<(), E>) -> impl Init<Self, E> {
+        let init_empty = |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 { (&raw mut (*slot).len).write(0) };
+            Ok(())
+        };
+
+        // SAFETY: `init_empty` fully initializes the slot and never fails.
+        unsafe { init_from_closure(init_empty) }.chain(f)
+    }
+
+    /// Appends an element to the back of the [`ArrayVec`].
+    ///
+    /// Fails when the [`ArrayVec`] is full, handing the element back in [`PushError`].
+    #[inline]
+    pub fn push(&mut self, v: T) -> Result<(), PushError<T>> {
+        self.try_push_init(v)
+            .map_err(|TryPushInitError::Full(v)| PushError(v))
+    }
+
+    /// Appends an element to the back of the [`ArrayVec`] by initializing it in place.
+    ///
+    /// Fails when the [`ArrayVec`] is full, handing the initializer back in [`PushError`].
+    #[inline]
+    pub fn push_init<I: Init<T>>(&mut self, init: I) -> Result<(), PushError<I>> {
+        self.try_push_init(init)
+            .map_err(|TryPushInitError::Full(init)| PushError(init))
+    }
+
+    /// 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 [`TryPushInitError::Full`]. If the
+    /// initializer itself fails, its error is returned in [`TryPushInitError::InitError`].
+    pub fn try_push_init<I, E>(&mut self, init: I) -> Result<(), TryPushInitError<I, E>>
+    where
+        I: Init<T, E>,
+    {
+        let Some(slot) = self.spare_capacity_mut().first_mut() else {
+            return Err(TryPushInitError::Full(init));
+        };
+
+        // SAFETY:
+        // - `slot.as_mut_ptr()` points to a spare slot, which is valid, uninitialized memory.
+        // - If `raw_try_init` fails, this returns without increasing `len` or touching `slot`.
+        // - `init` implements `Init<T, E>`, which removes the pinning requirement.
+        unsafe { pin_init::raw_try_init(slot.as_mut_ptr(), init) }
+            .map_err(TryPushInitError::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`].
+    #[inline]
+    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 TryPushInitError<I, E> {
+    /// The [`ArrayVec`] is full. Hand the initializer back.
+    Full(I),
+    /// The initializer failed.
+    InitError(E),
+}
+
+impl<I, E> fmt::Debug for TryPushInitError<I, E> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            TryPushInitError::Full(_) => write!(f, "Not enough capacity"),
+            TryPushInitError::InitError(_) => write!(f, "Initializer failed"),
+        }
+    }
+}
+
+impl<I, E> From<TryPushInitError<I, E>> for Error
+where
+    Error: From<E>,
+{
+    #[inline]
+    fn from(e: TryPushInitError<I, E>) -> Error {
+        match e {
+            TryPushInitError::Full(_) => EINVAL,
+            TryPushInitError::InitError(e) => Error::from(e),
+        }
+    }
+}
+
+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> {
+    #[inline]
+    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> {
+    #[inline]
+    fn borrow(&self) -> &[T] {
+        self.as_slice()
+    }
+}
+
+impl<T, const N: usize> BorrowMut<[T]> for ArrayVec<T, N> {
+    #[inline]
+    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>;
+
+    #[inline]
+    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>;
+
+    #[inline]
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter_mut()
+    }
+}
+
+impl<T: fmt::Debug, const N: usize> fmt::Debug for ArrayVec<T, N> {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Debug::fmt(self.as_slice(), f)
+    }
+}
diff --git a/rust/kernel/alloc/kvec.rs b/rust/kernel/alloc/kvec.rs
index c8b312f64657..cd0c57a9ad90 100644
--- a/rust/kernel/alloc/kvec.rs
+++ b/rust/kernel/alloc/kvec.rs
@@ -1255,6 +1255,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 d24dfe22834a..f5c44fdc7016 100644
--- a/rust/kernel/alloc/kvec/errors.rs
+++ b/rust/kernel/alloc/kvec/errors.rs
@@ -7,7 +7,7 @@
     prelude::*, //
 };
 
-/// Error type for [`Vec::push_within_capacity`].
+/// Error type for a push that fails because there is no spare capacity.
 pub struct PushError<T>(pub T);
 
 impl<T> fmt::Debug for PushError<T> {

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v4 3/7] gpu: nova-core: add NVKV encoder
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 1/7] rust: alloc: add Vec::try_push_init and Vec::push_init Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 2/7] rust: alloc: add ArrayVec Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 4/7] gpu: nova-core: add NVKV decoder Eliot Courtney
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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.

Co-developed-by: Danilo Krummrich <dakr@kernel.org>
Signed-off-by: Danilo Krummrich <dakr@kernel.org>
Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
 drivers/gpu/nova-core/gsp.rs             |   1 +
 drivers/gpu/nova-core/gsp/nvkv.rs        | 122 ++++++++++++++++++
 drivers/gpu/nova-core/gsp/nvkv/encode.rs | 204 +++++++++++++++++++++++++++++++
 3 files changed, 327 insertions(+)

diff --git a/drivers/gpu/nova-core/gsp.rs b/drivers/gpu/nova-core/gsp.rs
index dda58095f40b..a4a6bf7e2df7 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..80936f7c53d0
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -0,0 +1,122 @@
+// 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;
+
+use crate::bounded_enum;
+
+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 => KeyId;
+        27:16 index => Index;
+        31:28 opcode ?=> Opcode;
+        63:32 value => u32;
+    }
+}
+
+bounded_enum! {
+    /// Describes the format of the following NVKV operation.
+    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+    #[repr(u8)]
+    enum Opcode with TryFrom<Bounded<u64, 4>> {
+        /// 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,
+    }
+}
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..4585e4d5bcd5
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+#![cfg_attr(not(CONFIG_KUNIT = "y"), 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
+    }
+
+    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.
+    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.
+    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.
+    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.
+    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.
+    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(1)
+                .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(3)
+                .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(2)
+                .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(2)
+                .into_raw()
+        );
+        assert_eq!(encoded[8], ARRAY64_VALUE[0]);
+        assert_eq!(encoded[9], ARRAY64_VALUE[1]);
+
+        Ok(())
+    }
+}

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v4 4/7] gpu: nova-core: add NVKV decoder
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
                   ` (2 preceding siblings ...)
  2026-10-16  1:53 ` [PATCH v4 3/7] gpu: nova-core: add NVKV encoder Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  2026-10-16  2:00   ` sashiko-bot
  2026-10-16  1:53 ` [PATCH v4 5/7] gpu: nova-core: add NVKV typed encoding Eliot Courtney
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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` and `Visit` trait. The
`Visit` 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 | 503 +++++++++++++++++++++++++++++++
 2 files changed, 506 insertions(+)

diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index 80936f7c53d0..ac4b399fb05c 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -29,6 +29,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..6496fab1c0c6
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
@@ -0,0 +1,503 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+#![cfg_attr(not(CONFIG_KUNIT = "y"), expect(dead_code))]
+
+use kernel::prelude::*;
+
+use crate::{
+    gsp::nvkv::{
+        Index,
+        KeyId,
+        Op,
+        Opcode, //
+    },
+    num::IntoSafeCast, //
+};
+
+/// A decoded NVKV value.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+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);
+
+/// The state of one NVKV decode operation which produces a target value [`Target`](Schema::Target).
+///
+/// The decoded values are passed to the state through [`Visit`], which is a separate trait so
+/// that only [`Visit`] has the lifetime of the stream as a parameter. Structs whose [`Schema`]
+/// fields don't borrow from the stream then don't need a lifetime parameter.
+pub(crate) trait Schema {
+    type Target;
+
+    /// Returns an initializer that creates an empty schema in place.
+    ///
+    /// Use [`KBox::init`] for the heap or [`stack_pin_init!`](pin_init::stack_pin_init) for the
+    /// stack (if sure that the value is small enough to fit).
+    fn init() -> impl Init<Self>;
+
+    /// Returns an initializer that makes the decoded [`Target`](Schema::Target).
+    ///
+    /// After the returned initializer runs successfully, the schema must be empty again. Taking
+    /// `self` instead of `&mut self` would avoid this contract, but it forces a copy of the schema
+    /// onto the stack.
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_;
+}
+
+/// Trait for types that visit the decoded key, index, and value tuple of an NVKV stream.
+///
+/// A schema that doesn't need to borrow data from the stream can implement this for all `'data`
+/// lifetimes, avoiding having to carry the lifetime parameter. A schema that borrows from the
+/// stream directly should implement it for its own lifetime only.
+pub(crate) trait Visit<'data> {
+    /// Visits one decoded tuple. Returns `Ok(true)` if the schema consumed it.
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> Result<bool>;
+}
+
+/// A read position in an NVKV stream.
+struct Cursor<'a> {
+    data: &'a [u64],
+}
+
+impl<'a> Cursor<'a> {
+    /// Creates a cursor at the start of `data`.
+    fn new(data: &'a [u64]) -> Self {
+        Self { data }
+    }
+
+    /// Returns `true` if no `u64` values remain.
+    fn is_empty(&self) -> bool {
+        self.data.is_empty()
+    }
+
+    /// Takes the next `u64`.
+    fn take_u64(&mut self) -> Result<u64> {
+        // PANIC: `take_u64s(1)` returns exactly one element on success.
+        Ok(self.take_u64s(1)?[0])
+    }
+
+    /// Takes `count` bytes. If `count` is not a multiple of 8 (`u64` size), bytes are discarded up
+    /// to the next multiple.
+    fn take_u8s(&mut self, count: usize) -> Result<&'a [u8]> {
+        let values = self.take_u64s(count.div_ceil(size_of::<u64>()))?;
+        values.as_bytes().get(..count).ok_or(EINVAL)
+    }
+
+    /// Takes `count` 32-bit values. If `count` is not a multiple of 2 (`u32` values per `u64`),
+    /// bytes are discarded up to the next multiple.
+    fn take_u32s(&mut self, count: usize) -> Result<&'a [u32]> {
+        let values = self.take_u64s(count.div_ceil(size_of::<u64>() / size_of::<u32>()))?;
+        <[u32]>::ref_from_prefix_with_elems(values.as_bytes(), count)
+            .map(|(elems, _)| elems)
+            .map_err(|_| EINVAL)
+    }
+
+    /// Takes `count` `u64` values, or fails with [`EINVAL`] if fewer remain.
+    fn take_u64s(&mut self, count: usize) -> Result<&'a [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 }
+    }
+
+    /// Passes a decoded value to `schema` and applies the [`UnknownKeyPolicy`] if not consumed.
+    fn visit<S: Visit<'a>>(
+        &self,
+        schema: &mut S,
+        key: KeyId,
+        index: Index,
+        value: DecoderValue<'a>,
+    ) -> Result {
+        let consumed = schema.visit(key, index, value)?;
+        if !consumed && self.policy == UnknownKeyPolicy::Error {
+            Err(EINVAL)
+        } else {
+            Ok(())
+        }
+    }
+
+    /// Computes the key to use with a `SEQ` operation, or errors if it doesn't
+    /// make a valid key (overflow).
+    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 + Visit<'a>>(
+        &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();
+            let index = op.index();
+            let op_value = op.value();
+            match op.opcode()? {
+                Opcode::Imm32 => {
+                    self.visit(schema, key, index, DecoderValue::Scalar32(op_value))?;
+                }
+                Opcode::Seq32 => {
+                    let values = cursor.take_u32s(op_value.into_safe_cast())?;
+                    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(op_value.into_safe_cast())?;
+                    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(op_value.into_safe_cast())?;
+                    self.visit(schema, key, index, DecoderValue::Array8(value))?;
+                }
+                Opcode::Array32 => {
+                    let value = cursor.take_u32s(op_value.into_safe_cast())?;
+                    self.visit(schema, key, index, DecoderValue::Array32(value))?;
+                }
+                Opcode::Array64 => {
+                    let value = cursor.take_u64s(op_value.into_safe_cast())?;
+                    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 {
+    /// Decoding skips values that the schema does not consume.
+    Ignore,
+    /// Decoding fails with [`EINVAL`] on the first value that the schema does not consume.
+    Error,
+}
+
+#[kunit_tests(nova_core_nvkv_decode)]
+mod tests {
+    use super::*;
+
+    use crate::gsp::nvkv::Encoder;
+
+    /// 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 RawSchema {
+        const SCALAR32_KEY: KeyId = 0x1001;
+        const SCALAR64_KEY: KeyId = 0x1002;
+    }
+
+    impl Schema for RawSchema {
+        type Target = Self;
+
+        fn init() -> impl Init<Self> {
+            Self::default()
+        }
+
+        fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+            Ok(core::mem::take(self))
+        }
+    }
+
+    impl<'d> Visit<'d> for RawSchema {
+        fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'d>) -> Result<bool> {
+            if index != Index::new::<0>() {
+                return Err(EINVAL);
+            }
+            match key {
+                Self::SCALAR32_KEY => self.scalar32 = value.try_into()?,
+                Self::SCALAR64_KEY => self.scalar64 = value.try_into()?,
+                _ => return Ok(false),
+            }
+            Ok(true)
+        }
+    }
+
+    // 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_VALUE: u32 = 0x1111_2222;
+        const SCALAR64_VALUE: u64 = 0x3333_4444_5555_6666;
+
+        let mut encoder = Encoder::new();
+        encoder.encode_u32(RawSchema::SCALAR32_KEY, Index::new::<0>(), SCALAR32_VALUE)?;
+        encoder.encode_u64(RawSchema::SCALAR64_KEY, Index::new::<0>(), SCALAR64_VALUE)?;
+        let serialized = encoder.finish();
+
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(RawSchema::init(), GFP_KERNEL)?;
+        let decoded = KBox::try_init(decoder.decode(&mut *schema)?, GFP_KERNEL)?;
+
+        assert_eq!(decoded.scalar32, SCALAR32_VALUE);
+        assert_eq!(decoded.scalar64, SCALAR64_VALUE);
+
+        Ok(())
+    }
+
+    // Tests that an unknown key fails the decode under `UnknownKeyPolicy::Error` and is skipped
+    // under `UnknownKeyPolicy::Ignore`.
+    #[test]
+    fn decode_unknown_key() -> Result {
+        const UNKNOWN_KEY: KeyId = 0x2001;
+
+        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);
+        let mut schema = KBox::init(RawSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Ignore);
+        let mut schema = KBox::init(RawSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_ok());
+
+        Ok(())
+    }
+
+    /// Records each visit as `(key, index, value)`, for tests on hand-built streams.
+    #[derive(Default)]
+    struct Recorder<'d> {
+        visits: KVVec<(KeyId, u64, DecoderValue<'d>)>,
+    }
+
+    impl<'d> Schema for Recorder<'d> {
+        type Target = KVVec<(KeyId, u64, DecoderValue<'d>)>;
+
+        fn init() -> impl Init<Self> {
+            Self::default()
+        }
+
+        fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+            Ok(core::mem::take(&mut self.visits))
+        }
+    }
+
+    impl<'d> Visit<'d> for Recorder<'d> {
+        fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'d>) -> Result<bool> {
+            self.visits.push((key, index.get(), value), GFP_KERNEL)?;
+            Ok(true)
+        }
+    }
+
+    // Tests the decoder on hand-built `u64` values that the encoder does not produce: SEQ32,
+    // multi-value SEQ64, zero counts, a non-zero index and padded arrays.
+    #[test]
+    fn decode_raw_u64s() -> Result {
+        const SEQ32_KEY: KeyId = 0x2000;
+        const SEQ64_KEY: KeyId = 0x2010;
+        const EMPTY_SEQ64_KEY: KeyId = 0x2020;
+        const EMPTY_SEQ32_KEY: KeyId = 0x2021;
+        const EMPTY_ARRAY8_KEY: KeyId = 0x2030;
+        const EMPTY_ARRAY32_KEY: KeyId = 0x2031;
+        const EMPTY_ARRAY64_KEY: KeyId = 0x2032;
+        const ARRAY8_KEY: KeyId = 0x2040;
+        const ARRAY32_KEY: KeyId = 0x2041;
+
+        let data = [
+            // SEQ32 with three values for three consecutive keys, packed two per `u64`.
+            Op::zeroed()
+                .with_key(SEQ32_KEY)
+                .with_opcode(Opcode::Seq32)
+                .with_value(3)
+                .into_raw(),
+            0x0000_0002_0000_0001,
+            0x0000_0000_0000_0003,
+            // SEQ64 with two values at a non-zero index.
+            Op::zeroed()
+                .with_key(SEQ64_KEY)
+                .with_index(Index::new::<3>())
+                .with_opcode(Opcode::Seq64)
+                .with_value(2)
+                .into_raw(),
+            0x1111_1111_1111_1111,
+            0x2222_2222_2222_2222,
+            // A zero-count sequence has no keys and is skipped, as in NVIDIA's decoder.
+            Op::zeroed()
+                .with_key(EMPTY_SEQ64_KEY)
+                .with_opcode(Opcode::Seq64)
+                .with_value(0)
+                .into_raw(),
+            Op::zeroed()
+                .with_key(EMPTY_SEQ32_KEY)
+                .with_opcode(Opcode::Seq32)
+                .with_value(0)
+                .into_raw(),
+            // A zero-length array is visited with an empty slice.
+            Op::zeroed()
+                .with_key(EMPTY_ARRAY8_KEY)
+                .with_opcode(Opcode::Array8)
+                .with_value(0)
+                .into_raw(),
+            Op::zeroed()
+                .with_key(EMPTY_ARRAY32_KEY)
+                .with_opcode(Opcode::Array32)
+                .with_value(0)
+                .into_raw(),
+            Op::zeroed()
+                .with_key(EMPTY_ARRAY64_KEY)
+                .with_opcode(Opcode::Array64)
+                .with_value(0)
+                .into_raw(),
+            // Three bytes padded to one `u64`, then three 32-bit values padded to two `u64` values.
+            Op::zeroed()
+                .with_key(ARRAY8_KEY)
+                .with_opcode(Opcode::Array8)
+                .with_value(3)
+                .into_raw(),
+            0x0000_0000_00cc_bbaa,
+            Op::zeroed()
+                .with_key(ARRAY32_KEY)
+                .with_opcode(Opcode::Array32)
+                .with_value(3)
+                .into_raw(),
+            0x0000_0002_0000_0001,
+            0x0000_0000_0000_0003,
+        ];
+
+        let decoder = Decoder::new(&data, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(Recorder::init(), GFP_KERNEL)?;
+        let visits = KBox::try_init(decoder.decode(&mut *schema)?, GFP_KERNEL)?;
+
+        assert_eq!(
+            visits.as_slice(),
+            &[
+                (SEQ32_KEY, 0, DecoderValue::Scalar32(1)),
+                (SEQ32_KEY + 1, 0, DecoderValue::Scalar32(2)),
+                (SEQ32_KEY + 2, 0, DecoderValue::Scalar32(3)),
+                (SEQ64_KEY, 3, DecoderValue::Scalar64(0x1111_1111_1111_1111)),
+                (
+                    SEQ64_KEY + 1,
+                    3,
+                    DecoderValue::Scalar64(0x2222_2222_2222_2222)
+                ),
+                (EMPTY_ARRAY8_KEY, 0, DecoderValue::Array8(&[])),
+                (EMPTY_ARRAY32_KEY, 0, DecoderValue::Array32(&[])),
+                (EMPTY_ARRAY64_KEY, 0, DecoderValue::Array64(&[])),
+                (ARRAY8_KEY, 0, DecoderValue::Array8(&[0xaa, 0xbb, 0xcc])),
+                (ARRAY32_KEY, 0, DecoderValue::Array32(&[1, 2, 3])),
+            ]
+        );
+
+        Ok(())
+    }
+
+    // Tests that decoding a malformed stream fails instead of reading past the payload.
+    #[test]
+    fn decode_raw_words_malformed() -> Result {
+        const KEY: KeyId = 0x2100;
+
+        // An `Op` with the reserved opcode 6.
+        let bad_opcode = Op::zeroed().with_key(KEY).into_raw() | (6u64 << Op::OPCODE_SHIFT);
+        let streams: [&[u64]; 6] = [
+            // 100 bytes need 13 `u64` values, only one follows.
+            &[
+                Op::zeroed()
+                    .with_key(KEY)
+                    .with_opcode(Opcode::Array8)
+                    .with_value(100)
+                    .into_raw(),
+                0,
+            ],
+            // Two 64-bit values, only one follows.
+            &[
+                Op::zeroed()
+                    .with_key(KEY)
+                    .with_opcode(Opcode::Seq64)
+                    .with_value(2)
+                    .into_raw(),
+                0,
+            ],
+            // Three 32-bit values need two `u64` values, only one follows.
+            &[
+                Op::zeroed()
+                    .with_key(KEY)
+                    .with_opcode(Opcode::Array32)
+                    .with_value(3)
+                    .into_raw(),
+                0,
+            ],
+            // A value count with no payload.
+            &[Op::zeroed()
+                .with_key(KEY)
+                .with_opcode(Opcode::Seq32)
+                .with_value(1)
+                .into_raw()],
+            &[bad_opcode],
+            // Consecutive keys that overflow `KeyId`.
+            &[
+                Op::zeroed()
+                    .with_key(KeyId::MAX)
+                    .with_opcode(Opcode::Seq32)
+                    .with_value(2)
+                    .into_raw(),
+                0,
+            ],
+        ];
+
+        for stream in streams {
+            let decoder = Decoder::new(stream, UnknownKeyPolicy::Ignore);
+            let mut schema = KBox::init(Recorder::init(), GFP_KERNEL)?;
+            assert!(decoder.decode(&mut *schema).is_err());
+        }
+
+        Ok(())
+    }
+}

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v4 5/7] gpu: nova-core: add NVKV typed encoding
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
                   ` (3 preceding siblings ...)
  2026-10-16  1:53 ` [PATCH v4 4/7] gpu: nova-core: add NVKV decoder Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 6/7] gpu: nova-core: add NVKV typed decoding Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 7/7] gpu: nova-core: add NVKV GSP_INIT schemas Eliot Courtney
  6 siblings, 0 replies; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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 `Encodable` 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 an NVKV key with some value, to avoid having to make a lot of
newtypes and implement `Encodable` on them.

Signed-off-by: Eliot Courtney <ecourtney@nvidia.com>
---
 drivers/gpu/nova-core/gsp/nvkv.rs        |  62 ++++++-
 drivers/gpu/nova-core/gsp/nvkv/encode.rs | 273 +++++++++++++++++++++++++++++++
 2 files changed, 334 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index ac4b399fb05c..153aeb8d08d0 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -11,7 +11,13 @@
 
 #![expect(unused_imports)]
 
-use core::ops::Deref;
+use core::{
+    marker::PhantomData,
+    ops::{
+        Deref,
+        DerefMut, //
+    }, //
+};
 
 use kernel::{
     alloc::{
@@ -94,6 +100,60 @@ 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.
+pub(crate) struct Key<T, const KEY_ID: KeyId, As = T>(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)
+    }
+}
+
+/// A value with a specific index that encodes under the NVKV key `KEY_ID`.
+pub(crate) 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 }
+    }
+}
+
 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 4585e4d5bcd5..64b4ea46d59c 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/encode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -8,11 +8,137 @@
 use super::{
     EncodedStream,
     Index,
+    IndexedKey,
+    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>`.
+///
+/// Use a lifetime on the struct if its fields borrow data, for example, `Key<&'a [u8], KEY>`.
+///
+/// # Examples
+///
+/// ```
+/// nvkv_encode! {
+///     struct Request {
+///         id: Key<u32, 0x0001>,
+///         name: Key<&'static [u8], 0x0002>,
+///     }
+/// }
+/// ```
+#[cfg_attr(not(CONFIG_KUNIT = "y"), expect(unused_macros))]
+macro_rules! nvkv_encode {
+    (
+        $(#[$attr:meta])*
+        $vis:vis struct $name:ident $(<$lt:lifetime>)? {
+            $(
+                $(#[$field_attr:meta])*
+                $field_vis:vis $field:ident : $ty:ty
+            ),* $(,)?
+        }
+    ) => {
+        $(#[$attr])*
+        $vis struct $name $(<$lt>)? {
+            $(
+                $(#[$field_attr])*
+                $field_vis $field: $ty,
+            )*
+        }
+
+        impl $(<$lt>)? $crate::gsp::nvkv::Encodable for $name $(<$lt>)? {
+            fn encode(&self, encoder: &mut $crate::gsp::nvkv::Encoder) -> ::kernel::error::Result {
+                $( $crate::gsp::nvkv::Encodable::encode(&self.$field, encoder)?; )*
+                Ok(())
+            }
+        }
+    };
+}
+
+impl<const KEY_ID: KeyId> Encodable for IndexedKey<u32, KEY_ID> {
+    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> {
+    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> {
+    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> {
+    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> {
+    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> {
+    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> {
+    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> {
+    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,
+{
+    fn encode(&self, encoder: &mut Encoder) -> Result {
+        const_assert!(
+            size_of::<T>() <= 32 && size_of::<As>() <= 32,
+            "`Key` here would materialize too large a value on the stack"
+        );
+
+        IndexedKey::new(Index::new::<0>(), As::from(self.0)).encode(encoder)
+    }
+}
+
+impl<T: Encodable> Encodable for Option<T> {
+    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,
@@ -201,4 +327,151 @@ 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;
+        const INDEXED_KEY: KeyId = 0x0006;
+        const CONVERTED_KEY: KeyId = 0x0007;
+        const INNER_KEY: KeyId = 0x0008;
+
+        // A value that is encoded as the `u32` it converts to.
+        #[derive(Clone, Copy)]
+        enum TestEnum {
+            Value = 3,
+        }
+
+        impl From<TestEnum> for u32 {
+            fn from(value: TestEnum) -> Self {
+                value as u32
+            }
+        }
+
+        nvkv_encode! {
+            struct Inner {
+                x: Key<u32, INNER_KEY>,
+            }
+        }
+
+        nvkv_encode! {
+            struct TypedRequest<'a> {
+                a: Key<u32, U32_KEY>,
+                b: Key<u64, U64_KEY>,
+                name: Key<&'a [u8], NAME_KEY>,
+                fixed: Key<[u8; 4], FIXED_KEY>,
+                opt: Option<Key<u32, OPT_KEY>>,
+                indexed: IndexedKey<u32, INDEXED_KEY>,
+                converted: Key<TestEnum, CONVERTED_KEY, u32>,
+                nested: Option<Inner>,
+            }
+        }
+
+        const U32_VALUE: u32 = 0x89ab_cdef;
+        const U64_VALUE: u64 = 0x0123_4567_89ab_cdef;
+        const INDEXED_VALUE: u32 = 0x1234_5678;
+        const INNER_VALUE: u32 = 0x0bad_f00d;
+
+        let index5 = Index::new::<5>();
+        let request = TypedRequest {
+            a: U32_VALUE.into(),
+            b: U64_VALUE.into(),
+            name: b"name\0".into(),
+            fixed: [1u8, 2, 3, 4].into(),
+            opt: None,
+            indexed: IndexedKey::new(index5, INDEXED_VALUE),
+            converted: TestEnum::Value.into(),
+            nested: Some(Inner {
+                x: INNER_VALUE.into(),
+            }),
+        };
+
+        let mut encoder = Encoder::new();
+        request.encode(&mut encoder)?;
+        let encoded = encoder.finish();
+
+        // `opt` is `None` and is not encoded.
+        assert_eq!(encoded.len(), 10);
+
+        let index0 = Index::new::<0>();
+        // `a`: IMM32 with the value in the `Op`.
+        assert_eq!(
+            encoded[0],
+            Op::zeroed()
+                .with_key(U32_KEY)
+                .with_index(index0)
+                .with_opcode(Opcode::Imm32)
+                .with_value(U32_VALUE)
+                .into_raw()
+        );
+        // `b`: a single-key SEQ64 followed by the value.
+        assert_eq!(
+            encoded[1],
+            Op::zeroed()
+                .with_key(U64_KEY)
+                .with_index(index0)
+                .with_opcode(Opcode::Seq64)
+                .with_value(1)
+                .into_raw()
+        );
+        assert_eq!(encoded[2], U64_VALUE);
+        // `name`: ARRAY8 of 5 bytes, zero-padded to one `u64`.
+        assert_eq!(
+            encoded[3],
+            Op::zeroed()
+                .with_key(NAME_KEY)
+                .with_index(index0)
+                .with_opcode(Opcode::Array8)
+                .with_value(5)
+                .into_raw()
+        );
+        assert_eq!(encoded[4], u64::from_le_bytes(*b"name\0\0\0\0"));
+        // `fixed`: ARRAY8 of 4 bytes, zero-padded to one `u64`.
+        assert_eq!(
+            encoded[5],
+            Op::zeroed()
+                .with_key(FIXED_KEY)
+                .with_index(index0)
+                .with_opcode(Opcode::Array8)
+                .with_value(4)
+                .into_raw()
+        );
+        assert_eq!(encoded[6], u64::from_le_bytes([1, 2, 3, 4, 0, 0, 0, 0]));
+        // `indexed`: IMM32 with a non-zero index in the `Op`.
+        assert_eq!(
+            encoded[7],
+            Op::zeroed()
+                .with_key(INDEXED_KEY)
+                .with_index(index5)
+                .with_opcode(Opcode::Imm32)
+                .with_value(INDEXED_VALUE)
+                .into_raw()
+        );
+        // `converted`: IMM32 with the `u32` that `TestEnum::Value` converts to.
+        assert_eq!(
+            encoded[8],
+            Op::zeroed()
+                .with_key(CONVERTED_KEY)
+                .with_index(index0)
+                .with_opcode(Opcode::Imm32)
+                .with_value(3)
+                .into_raw()
+        );
+        // `nested`: IMM32 with the single `u32` it contains.
+        assert_eq!(
+            encoded[9],
+            Op::zeroed()
+                .with_key(INNER_KEY)
+                .with_index(index0)
+                .with_opcode(Opcode::Imm32)
+                .with_value(INNER_VALUE)
+                .into_raw()
+        );
+
+        Ok(())
+    }
 }

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v4 6/7] gpu: nova-core: add NVKV typed decoding
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
                   ` (4 preceding siblings ...)
  2026-10-16  1:53 ` [PATCH v4 5/7] gpu: nova-core: add NVKV typed encoding Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  2026-10-16  1:53 ` [PATCH v4 7/7] gpu: nova-core: add NVKV GSP_INIT schemas Eliot Courtney
  6 siblings, 0 replies; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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        |  10 +-
 drivers/gpu/nova-core/gsp/nvkv/decode.rs | 708 ++++++++++++++++++++++++++++++-
 2 files changed, 715 insertions(+), 3 deletions(-)

diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index 153aeb8d08d0..9a4f3e40f811 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -9,7 +9,7 @@
 //! 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)]
+#![cfg_attr(not(CONFIG_KUNIT = "y"), expect(unused_imports))]
 
 use core::{
     marker::PhantomData,
@@ -22,7 +22,8 @@
 use kernel::{
     alloc::{
         allocator::KVmalloc,
-        Allocator, //
+        Allocator,
+        ArrayVec, //
     },
     bitfield,
     num::Bounded,
@@ -154,6 +155,11 @@ pub(crate) fn new(index: Index, value: T) -> Self {
     }
 }
 
+/// A schema field for an array value under the NVKV key `KEY_ID`.
+pub(crate) struct Array<T, 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 6496fab1c0c6..43693cb4d31c 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/decode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
@@ -3,11 +3,25 @@
 
 #![cfg_attr(not(CONFIG_KUNIT = "y"), expect(dead_code))]
 
-use kernel::prelude::*;
+use core::{
+    convert::Infallible,
+    marker::PhantomData, //
+};
+
+use kernel::{
+    alloc::ArrayVec,
+    prelude::*, //
+};
+use pin_init::{
+    init_array_from_fn,
+    stack_try_pin_init, //
+};
 
 use crate::{
     gsp::nvkv::{
+        Array,
         Index,
+        Key,
         KeyId,
         Op,
         Opcode, //
@@ -15,6 +29,445 @@
     num::IntoSafeCast, //
 };
 
+/// Defines a schema struct together with its [`Schema`] and [`Visit`] implementations that decode
+/// into `$target`.
+///
+/// Each member of the struct should implement [`Schema`] and [`Visit`]. For every
+/// `(key, index, value)` triple decoded from the NVKV stream, the generated parent [`Visit`]
+/// 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`], [`Optional`],
+/// [`Accumulated`], [`Key`], [`Array`], [`TruncatingArray`], [`Indexed`]) for each member.
+///
+/// Use a lifetime on the schema struct if you want to borrow data directly from the encoded stream,
+/// for example, `Key<&'d [u8], KEY>`.
+///
+/// # Examples
+///
+/// ```
+/// nvkv_decode! {
+///     struct RequestSchema => Request {
+///         id: Required<Key<u32, 0x0001>>,
+///         name: Array<u8, 64, 0x0002>,
+///     }
+/// }
+///
+/// nvkv_decode! {
+///     struct NameSchema<'d> => Name<'d> {
+///         name: Key<&'d [u8], 0x0002>,
+///     }
+/// }
+/// ```
+#[cfg_attr(not(CONFIG_KUNIT = "y"), expect(unused_macros))]
+macro_rules! nvkv_decode {
+    // A schema which doesn't borrow from the stream. The macro uses a generic lifetime for the
+    // [`Visit`] implementation but not for the struct itself. This allows omitting the unused
+    // lifetime on the struct.
+    (
+        $(#[$attr:meta])*
+        $vis:vis struct $name:ident => $target:ty { $($fields:tt)* }
+    ) => {
+        $crate::gsp::nvkv::nvkv_decode!(
+            @impl ['data] [] $(#[$attr])* $vis struct $name => $target { $($fields)* }
+        );
+    };
+    // A schema which borrows from the stream for lifetime `$datalt`. The macro connects the
+    // [`Visit`] lifetime with the struct lifetime.
+    (
+        $(#[$attr:meta])*
+        $vis:vis struct $name:ident<$datalt:lifetime> => $target:ty { $($fields:tt)* }
+    ) => {
+        $crate::gsp::nvkv::nvkv_decode!(
+            @impl [$datalt] [<$datalt>] $(#[$attr])* $vis struct $name => $target { $($fields)* }
+        );
+    };
+    (@impl [$datalt:lifetime] [$($generics:tt)*]
+        $(#[$attr:meta])*
+        $vis:vis struct $name:ident => $target:ty {
+            $(
+                $(#[$field_attr:meta])*
+                $field_vis:vis $field:ident : $ty:ty
+            ),* $(,)?
+        }
+    ) => {
+        $(#[$attr])*
+        $vis struct $name $($generics)* {
+            $(
+                $(#[$field_attr])*
+                $field_vis $field: $ty,
+            )*
+        }
+
+        impl $($generics)* $crate::gsp::nvkv::Schema for $name $($generics)* {
+            type Target = $target;
+
+            fn init() -> impl ::kernel::prelude::Init<Self> {
+                ::pin_init::init!(Self {
+                    $( $field <- <$ty as $crate::gsp::nvkv::Schema>::init(), )*
+                })
+            }
+
+            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<$datalt> $crate::gsp::nvkv::Visit<$datalt> for $name $($generics)* {
+            fn visit(
+                &mut self,
+                key: $crate::gsp::nvkv::KeyId,
+                index: $crate::gsp::nvkv::Index,
+                value: $crate::gsp::nvkv::DecoderValue<$datalt>,
+            ) -> ::kernel::error::Result<bool> {
+                // TODO: This performs worst-case O(#fields) visit calls. Consider optimising this
+                // if it becomes a problem.
+                Ok(false
+                    $( || $crate::gsp::nvkv::Visit::visit(&mut self.$field, key, index, value)? )*)
+            }
+        }
+    };
+}
+pub(crate) use nvkv_decode;
+
+impl<T: Default, const KEY_ID: KeyId, As> Schema for Key<T, KEY_ID, As> {
+    type Target = T;
+
+    fn init() -> impl Init<Self> {
+        Self::default()
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        const_assert!(
+            size_of::<T>() <= 32,
+            "`Key` here would materialize too large a value on the stack"
+        );
+
+        Ok(core::mem::take(&mut self.0))
+    }
+}
+
+impl<'data, T, const KEY_ID: KeyId, As> Visit<'data> for Key<T, KEY_ID, As>
+where
+    As: TryFrom<DecoderValue<'data>, Error = Error>,
+    T: TryFrom<As>,
+    Error: From<<T as TryFrom<As>>::Error>,
+{
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> 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 = T::try_from(As::try_from(value)?)?;
+            Ok(true)
+        }
+    }
+}
+
+impl<T: Copy, const N: usize, const KEY_ID: KeyId> Schema for Array<T, N, KEY_ID> {
+    type Target = ArrayVec<T, N>;
+
+    fn init() -> impl Init<Self> {
+        init!(Self {
+            vec <- ArrayVec::init_with::<Infallible>(|_| Ok(())),
+        })
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        ArrayVec::init_with(move |dst| {
+            dst.extend_from_slice(&self.vec)?;
+            self.vec.clear();
+            Ok(())
+        })
+    }
+}
+
+impl<'data, T: Copy + 'data, const N: usize, const KEY_ID: KeyId> Visit<'data>
+    for Array<T, N, KEY_ID>
+where
+    &'data [T]: TryFrom<DecoderValue<'data>, Error = Error>,
+{
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> 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)
+    }
+}
+
+/// A schema field for an array value under the NVKV key `KEY_ID` that keeps at most `N` elements.
+///
+/// Unlike [`Array`], a longer value is truncated to its first `N` elements instead of failing the
+/// decode.
+pub(crate) struct TruncatingArray<T, const N: usize, const KEY_ID: KeyId> {
+    vec: ArrayVec<T, N>,
+}
+
+impl<T: Copy, const N: usize, const KEY_ID: KeyId> Schema for TruncatingArray<T, N, KEY_ID> {
+    type Target = ArrayVec<T, N>;
+
+    fn init() -> impl Init<Self> {
+        init!(Self {
+            vec <- ArrayVec::init_with::<Infallible>(|_| Ok(())),
+        })
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        ArrayVec::init_with(move |dst| {
+            dst.extend_from_slice(&self.vec)?;
+            self.vec.clear();
+            Ok(())
+        })
+    }
+}
+
+impl<'data, T: Copy + 'data, const N: usize, const KEY_ID: KeyId> Visit<'data>
+    for TruncatingArray<T, N, KEY_ID>
+where
+    &'data [T]: TryFrom<DecoderValue<'data>, Error = Error>,
+{
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> Result<bool> {
+        if key != KEY_ID {
+            return Ok(false);
+        }
+        // Arrays must be at index 0.
+        if index != Index::new::<0>() {
+            return Err(EINVAL);
+        }
+        let elems: &[T] = value.try_into()?;
+        // Take the latest value, keeping its first `N` elements.
+        self.vec.clear();
+        self.vec
+            .extend_from_slice(elems.get(..N).unwrap_or(elems))?;
+        Ok(true)
+    }
+}
+
+/// A schema field that marks a child [`Schema`] as required.
+///
+/// `finish` fails with [`EINVAL`] if the child consumed no value.
+pub(crate) struct Required<S> {
+    inner: S,
+    parsed: bool,
+}
+
+impl<S: Schema> Schema for Required<S> {
+    type Target = S::Target;
+
+    fn init() -> impl Init<Self> {
+        init!(Self {
+            inner <- S::init(),
+            parsed: false,
+        })
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        let parsed = core::mem::take(&mut self.parsed);
+        self.inner
+            .finish()
+            .chain(move |_| if parsed { Ok(()) } else { Err(EINVAL) })
+    }
+}
+
+impl<'data, S: Visit<'data>> Visit<'data> for Required<S> {
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> Result<bool> {
+        let consumed = self.inner.visit(key, index, value)?;
+        self.parsed |= consumed;
+        Ok(consumed)
+    }
+}
+
+/// A schema field which marks a child [`Schema`] as optional.
+pub(crate) struct Optional<S> {
+    inner: S,
+    parsed: bool,
+}
+
+impl<S: Schema> Schema for Optional<S>
+where
+    S::Target: Default + Unpin,
+{
+    type Target = Option<S::Target>;
+
+    fn init() -> impl Init<Self> {
+        init!(Self {
+            inner <- S::init(),
+            parsed: false,
+        })
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        const_assert!(
+            size_of::<Self::Target>() <= 512,
+            "`Optional` here would materialize too large a value on the stack"
+        );
+
+        if !core::mem::take(&mut self.parsed) {
+            return Ok(None);
+        }
+        stack_try_pin_init!(let value: S::Target =? self.inner.finish());
+        Ok(Some(core::mem::take(value.get_mut())))
+    }
+}
+
+impl<'data, S: Visit<'data>> Visit<'data> for Optional<S> {
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> Result<bool> {
+        let consumed = self.inner.visit(key, index, value)?;
+        self.parsed |= consumed;
+        Ok(consumed)
+    }
+}
+
+/// Expects objects specified sequentially with index starting from zero.
+///
+/// This stores two [`Schema`]s which are initially empty. A [`Schema`] is expected to be complete
+/// when we get the next index.
+pub(crate) struct Accumulated<S: Schema> {
+    // Tracks the current index (starting from 0). When this advances, the schema `current` must be
+    // finished.
+    current_index: Index,
+
+    // The current `Schema` being constructed now.
+    current: S,
+
+    // Tracks empty schemas (one that has never consumed a key via `visit`), so we don't call
+    // `finish` on them.
+    current_started: bool,
+
+    // The next `Schema` to be constructed. This is required because we must consume a key intended
+    // for this Schema from the next index to know we can try calling `finish` on `current`.
+    next: S,
+
+    // The set of results generated by the completed schemas so far.
+    accumulated: KVVec<S::Target>,
+}
+
+impl<S: Schema> Schema for Accumulated<S> {
+    type Target = KVVec<S::Target>;
+
+    fn init() -> impl Init<Self> {
+        init!(Self {
+            current_index: Index::new::<0>(),
+            current <- S::init(),
+            current_started: false,
+            next <- S::init(),
+            accumulated: KVVec::new(),
+        })
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        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<'data, S: Schema + Visit<'data>> Visit<'data> for Accumulated<S> {
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> 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)
+        }
+    }
+}
+
+/// A schema field that scatters indexed values into an array of `N` slots.
+///
+/// Values are decoded from the encoded stream using the type `As`, but then converted into `T` with
+/// [`From`], like the `As` parameter of [`Key`].
+pub(crate) struct Indexed<T, const N: usize, const KEY_ID: KeyId, As = T> {
+    slots: [T; N],
+    _as: 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: Default, const N: usize, const KEY_ID: KeyId, As> Schema for Indexed<T, N, KEY_ID, As> {
+    type Target = [T; N];
+
+    fn init() -> impl Init<Self> {
+        init!(Self {
+            slots <- init_array_from_fn(|_| T::default()),
+            _as: PhantomData,
+        })
+    }
+
+    fn finish(&mut self) -> impl Init<Self::Target, Error> + '_ {
+        init_array_from_fn(|i| Ok::<_, Error>(core::mem::take(&mut self.slots[i])))
+    }
+}
+
+impl<'data, T, const N: usize, const KEY_ID: KeyId, As> Visit<'data> for Indexed<T, N, KEY_ID, As>
+where
+    T: From<As>,
+    As: Copy + TryFrom<DecoderValue<'data>, Error = Error> + 'data,
+    &'data [As]: TryFrom<DecoderValue<'data>, Error = Error>,
+{
+    fn visit(&mut self, key: KeyId, index: Index, value: DecoderValue<'data>) -> 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.slots, start, elems)?,
+            Err(_) => scatter_window(&mut self.slots, start, &[As::try_from(value)?])?,
+        }
+        Ok(true)
+    }
+}
+
 /// A decoded NVKV value.
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub(crate) enum DecoderValue<'a> {
@@ -312,6 +765,133 @@ fn decode_unknown_key() -> Result {
         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 OPT_ARRAY_KEY: KeyId = 0x123b;
+        const TRUNCATED_KEY: KeyId = 0x123c;
+        const CONVERTED_KEY: KeyId = 0x123d;
+        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;
+        const OPT_ARRAY_VALUE: &[u8] = &[0x77, 0x88];
+        const TRUNCATED_VALUE: &[u8] = &[0x9a, 0xbc, 0xde];
+        const CONVERTED_VALUE: u32 = 0x66;
+
+        // A value that is decoded from the `u32` it converts from.
+        #[derive(Default)]
+        struct Converted(u32);
+
+        impl From<u32> for Converted {
+            fn from(value: u32) -> Self {
+                Self(value)
+            }
+        }
+
+        nvkv_decode! {
+            struct PairSchema => Pair {
+                x: Required<Key<u32, X_KEY>>,
+                y: Required<Key<u32, Y_KEY>>,
+            }
+        }
+
+        struct Pair {
+            x: u32,
+            y: u32,
+        }
+
+        nvkv_decode! {
+            struct TestSchema => TestDecodable {
+                scalar32: Required<Key<u32, SCALAR32_KEY>>,
+                scalar64: Required<Key<u64, SCALAR64_KEY>>,
+                array8: Array<u8, 64, ARRAY8_KEY>,
+                array32: Array<u32, 64, ARRAY32_KEY>,
+                array64: Array<u64, 32, ARRAY64_KEY>,
+                opt_present: Optional<Key<u32, OPT_PRESENT_KEY>>,
+                opt_absent: Optional<Key<u32, OPT_ABSENT_KEY>>,
+                opt_array: Optional<Array<u8, 4, OPT_ARRAY_KEY>>,
+                truncated: TruncatingArray<u8, 2, TRUNCATED_KEY>,
+                converted: Key<Converted, CONVERTED_KEY, u32>,
+                pairs: Accumulated<PairSchema>,
+                slots: Indexed<u32, 4, SLOT_KEY>,
+            }
+        }
+
+        struct TestDecodable {
+            scalar32: u32,
+            scalar64: u64,
+            array8: ArrayVec<u8, 64>,
+            array32: ArrayVec<u32, 64>,
+            array64: ArrayVec<u64, 32>,
+            opt_present: Option<u32>,
+            opt_absent: Option<u32>,
+            opt_array: Option<ArrayVec<u8, 4>>,
+            truncated: ArrayVec<u8, 2>,
+            converted: Converted,
+            pairs: KVVec<Pair>,
+            slots: [u32; 4],
+        }
+
+        let index0 = Index::new::<0>();
+        let index1 = Index::new::<1>();
+        let index2 = Index::new::<2>();
+        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_array8(OPT_ARRAY_KEY, index0, OPT_ARRAY_VALUE)?;
+        encoder.encode_array8(TRUNCATED_KEY, index0, TRUNCATED_VALUE)?;
+        encoder.encode_u32(CONVERTED_KEY, index0, CONVERTED_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)?;
+        encoder.encode_array32(SLOT_KEY, index2, &[30, 40])?;
+        let serialized = encoder.finish();
+
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(TestSchema::init(), GFP_KERNEL)?;
+        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.opt_array.as_deref(), Some(OPT_ARRAY_VALUE));
+        assert_eq!(*decoded.truncated, TRUNCATED_VALUE[..2]);
+        assert_eq!(decoded.converted.0, CONVERTED_VALUE);
+        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, 30, 40]);
+
+        Ok(())
+    }
+
     /// Records each visit as `(key, index, value)`, for tests on hand-built streams.
     #[derive(Default)]
     struct Recorder<'d> {
@@ -500,4 +1080,130 @@ fn decode_raw_words_malformed() -> Result {
 
         Ok(())
     }
+
+    // Tests the error paths of the schema kinds.
+    #[test]
+    fn decode_typed_struct_errors() -> Result {
+        const VALUE_KEY: KeyId = 0x2200;
+        const SLOT_KEY: KeyId = 0x2201;
+        const BLOB_KEY: KeyId = 0x2202;
+        const X_KEY: KeyId = 0x2203;
+        const Y_KEY: KeyId = 0x2204;
+
+        let index0 = Index::new::<0>();
+        let index1 = Index::new::<1>();
+        let index2 = Index::new::<2>();
+
+        nvkv_decode! {
+            struct ValueSchema => Value {
+                value: Key<u32, VALUE_KEY>,
+            }
+        }
+
+        struct Value {
+            value: u32,
+        }
+
+        // A single value at a non-zero index.
+        let mut encoder = Encoder::new();
+        encoder.encode_u32(VALUE_KEY, index1, 1)?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(ValueSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        // A 64-bit value for a 32-bit key.
+        let mut encoder = Encoder::new();
+        encoder.encode_u64(VALUE_KEY, index0, 1)?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(ValueSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        nvkv_decode! {
+            struct SlotsSchema => Slots {
+                slots: Indexed<u32, 2, SLOT_KEY>,
+            }
+        }
+
+        struct Slots {
+            slots: [u32; 2],
+        }
+
+        // An index past the declared slots.
+        let mut encoder = Encoder::new();
+        encoder.encode_u32(SLOT_KEY, index2, 1)?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(SlotsSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        nvkv_decode! {
+            struct BlobSchema => Blob {
+                blob: Array<u8, 4, BLOB_KEY>,
+            }
+        }
+
+        struct Blob {
+            blob: ArrayVec<u8, 4>,
+        }
+
+        // An array longer than the declared capacity.
+        let mut encoder = Encoder::new();
+        encoder.encode_array8(BLOB_KEY, index0, &[0; 5])?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(BlobSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        nvkv_decode! {
+            struct PairSchema => Pair {
+                x: Required<Key<u32, X_KEY>>,
+                y: Required<Key<u32, Y_KEY>>,
+            }
+        }
+
+        struct Pair {
+            x: u32,
+            y: u32,
+        }
+
+        // Required keys that are missing are an error.
+        let mut encoder = Encoder::new();
+        encoder.encode_u32(X_KEY, index0, 1)?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(PairSchema::init(), GFP_KERNEL)?;
+        let init = decoder.decode(&mut *schema)?;
+        assert!(KBox::try_init(init, GFP_KERNEL).is_err());
+
+        nvkv_decode! {
+            struct PairsSchema => Pairs {
+                pairs: Accumulated<PairSchema>,
+            }
+        }
+
+        struct Pairs {
+            pairs: KVVec<Pair>,
+        }
+
+        // Accumulated objects must start at index 0.
+        let mut encoder = Encoder::new();
+        encoder.encode_u32(X_KEY, index1, 1)?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(PairsSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        // Accumulated objects must be complete before the next one starts.
+        let mut encoder = Encoder::new();
+        encoder.encode_u32(X_KEY, index0, 1)?;
+        encoder.encode_u32(X_KEY, index1, 2)?;
+        let serialized = encoder.finish();
+        let decoder = Decoder::new(&serialized, UnknownKeyPolicy::Error);
+        let mut schema = KBox::init(PairsSchema::init(), GFP_KERNEL)?;
+        assert!(decoder.decode(&mut *schema).is_err());
+
+        Ok(())
+    }
 }

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v4 7/7] gpu: nova-core: add NVKV GSP_INIT schemas
  2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
                   ` (5 preceding siblings ...)
  2026-10-16  1:53 ` [PATCH v4 6/7] gpu: nova-core: add NVKV typed decoding Eliot Courtney
@ 2026-10-16  1:53 ` Eliot Courtney
  6 siblings, 0 replies; 11+ messages in thread
From: Eliot Courtney @ 2026-10-16  1:53 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. 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 | 157 ++++++++++++++++++++++++++++++-
 drivers/gpu/nova-core/gsp/nvkv.rs        |   2 -
 drivers/gpu/nova-core/gsp/nvkv/decode.rs |   1 -
 drivers/gpu/nova-core/gsp/nvkv/encode.rs |   2 +-
 4 files changed, 157 insertions(+), 5 deletions(-)

diff --git a/drivers/gpu/nova-core/gsp/fw/commands.rs b/drivers/gpu/nova-core/gsp/fw/commands.rs
index 567c56fbbfe3..88111aec3ddb 100644
--- a/drivers/gpu/nova-core/gsp/fw/commands.rs
+++ b/drivers/gpu/nova-core/gsp/fw/commands.rs
@@ -4,6 +4,8 @@
 use core::ops::Range;
 
 use kernel::{
+    alloc::ArrayVec,
+    bitfield,
     device,
     pci,
     prelude::*,
@@ -15,7 +17,19 @@
 
 use crate::{
     gpu::Chipset,
-    gsp::GSP_PAGE_SIZE,
+    gsp::{
+        nvkv::{
+            nvkv_decode,
+            nvkv_encode,
+            Accumulated,
+            Encodable,
+            Encoder,
+            Key,
+            Required,
+            TruncatingArray, //
+        },
+        GSP_PAGE_SIZE, //
+    },
     num::IntoSafeCast, //
 };
 
@@ -245,3 +259,144 @@ 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)]
+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], 0x3070>,
+        key_value: Key<u32, 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, 0x0080>,
+        first_vf_offset: Key<u32, 0x0081>,
+        flags: Key<u64, 0x1003>,
+        first_bar0_address: Key<u64, 0x1050>,
+        first_bar1_address: Key<u64, 0x1051>,
+        first_bar2_address: Key<u64, 0x1052>,
+    }
+}
+
+nvkv_encode! {
+    /// Payload of the `GSP_INIT` command.
+    // TODO: expect() doesn't work here before rustc 1.97.0
+    // https://github.com/rust-lang/rust/pull/154377
+    #[allow(dead_code)]
+    struct GspInitRequest {
+        pci_device_id: Key<u32, 0x0001>,
+        pci_sub_device_id: Key<u32, 0x0002>,
+        pci_revision_id: Key<u32, 0x0003>,
+        pci_config_mirror_base: Key<u32, 0x0010>,
+        pci_config_mirror_size: Key<u32, 0x0011>,
+        host_arch: Key<HostArch, 0x0070, u32>,
+        bus_device_func: Key<u64, 0x1020>,
+        regkeys: KVVec<RegKey>,
+        vf_info: Option<VfInfo>,
+    }
+}
+
+// Decode:
+
+// Should decode with UnknownKeyPolicy::Ignore.
+nvkv_decode! {
+    /// Schema for the `GSP_INIT` response.
+    // TODO: expect() doesn't work here before rustc 1.97.0
+    // https://github.com/rust-lang/rust/pull/154377
+    #[allow(dead_code)]
+    struct GspInitResponseSchema => GspInitResponse {
+        gpu_name: TruncatingArray<u8, { GspInitResponse::MAX_GPU_NAME_LEN }, 0x2000>,
+        fb_regions: Accumulated<FbRegionSchema>,
+        bar1_pde_base: Required<Key<u64, 0x1020>>,
+        vmmu_segment_size: Key<u64, 0x1050>,
+    }
+}
+
+/// Payload of the `GSP_INIT` response.
+// TODO: expect() doesn't work here before rustc 1.97.0
+// https://github.com/rust-lang/rust/pull/154377
+#[allow(dead_code)]
+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<Key<u64, 0x1011>>,
+        limit: Required<Key<u64, 0x1012>>,
+        flags: Required<Key<FbRegionFlags, 0x0012, u32>>,
+        tag: Required<Key<u32, 0x0013>>,
+    }
+}
+
+bitfield! {
+    /// FB region attribute flags.
+    #[derive(Default)]
+    struct FbRegionFlags(u32) {
+        0:0 support_compressed => bool;
+        1:1 support_iso => bool;
+        2:2 protected => bool;
+    }
+}
+
+/// One FB memory region.
+struct FbRegion {
+    base: u64,
+    limit: u64,
+    flags: FbRegionFlags,
+    tag: u32,
+}
diff --git a/drivers/gpu/nova-core/gsp/nvkv.rs b/drivers/gpu/nova-core/gsp/nvkv.rs
index 9a4f3e40f811..8b10de9b48c8 100644
--- a/drivers/gpu/nova-core/gsp/nvkv.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv.rs
@@ -9,8 +9,6 @@
 //! 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.
 
-#![cfg_attr(not(CONFIG_KUNIT = "y"), expect(unused_imports))]
-
 use core::{
     marker::PhantomData,
     ops::{
diff --git a/drivers/gpu/nova-core/gsp/nvkv/decode.rs b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
index 43693cb4d31c..7637be89ea31 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/decode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
@@ -60,7 +60,6 @@
 ///     }
 /// }
 /// ```
-#[cfg_attr(not(CONFIG_KUNIT = "y"), expect(unused_macros))]
 macro_rules! nvkv_decode {
     // A schema which doesn't borrow from the stream. The macro uses a generic lifetime for the
     // [`Visit`] implementation but not for the struct itself. This allows omitting the unused
diff --git a/drivers/gpu/nova-core/gsp/nvkv/encode.rs b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
index 64b4ea46d59c..b40aff41196e 100644
--- a/drivers/gpu/nova-core/gsp/nvkv/encode.rs
+++ b/drivers/gpu/nova-core/gsp/nvkv/encode.rs
@@ -38,7 +38,6 @@ pub(crate) trait Encodable {
 ///     }
 /// }
 /// ```
-#[cfg_attr(not(CONFIG_KUNIT = "y"), expect(unused_macros))]
 macro_rules! nvkv_encode {
     (
         $(#[$attr:meta])*
@@ -65,6 +64,7 @@ fn encode(&self, encoder: &mut $crate::gsp::nvkv::Encoder) -> ::kernel::error::R
         }
     };
 }
+pub(crate) use nvkv_encode;
 
 impl<const KEY_ID: KeyId> Encodable for IndexedKey<u32, KEY_ID> {
     fn encode(&self, encoder: &mut Encoder) -> Result {

-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH v4 4/7] gpu: nova-core: add NVKV decoder
  2026-10-16  1:53 ` [PATCH v4 4/7] gpu: nova-core: add NVKV decoder Eliot Courtney
@ 2026-10-16  2:00   ` sashiko-bot
  2026-10-16  2:05     ` John Hubbard
  0 siblings, 1 reply; 11+ messages in thread
From: sashiko-bot @ 2026-10-16  2:00 UTC (permalink / raw)
  To: Eliot Courtney, 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, nova-gpu, dri-devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] gpu: nova-core: endianness bug when extracting u8/u32 arrays from native &[u64] stream
--

commit 60056513188e14b734dcdb09e45f7f46bb4688b6
Author: Eliot Courtney <ecourtney@nvidia.com>

gpu: nova-core: add NVKV decoder

This commit adds an NVKV decoder for parsing messages from GSP for GMCAPI
calls. It reads an encoded stream of (key, index, value) tuples and invokes
a Schema/Visit trait interface to consume the values.

> 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 0000000000000..6496fab1c0c62
> --- /dev/null
> +++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
[ ... ]
> +    /// Takes `count` bytes. If `count` is not a multiple of 8 (`u64` size), bytes are discarded up
> +    /// to the next multiple.
> +    fn take_u8s(&mut self, count: usize) -> Result<&'a [u8]> {
> +        let values = self.take_u64s(count.div_ceil(size_of::<u64>()))?;
> +        values.as_bytes().get(..count).ok_or(EINVAL)
> +    }

[Severity: High]
Does casting a native u64 slice to bytes extract the wrong payload on
big-endian architectures?

Because Cursor::take_u64s() reads host-native 64-bit integers, using
.as_bytes() reverses the byte layout on big-endian hosts compared to
little-endian.

For example, if a 3-byte payload is stored inside a 64-bit word, a big-endian
host will have the payload at the end of the word and zero-padding at the
beginning, causing .get(..count) to extract the zeros instead of the actual
data payload.

> +
> +    /// Takes `count` 32-bit values. If `count` is not a multiple of 2 (`u32` values per `u64`),
> +    /// bytes are discarded up to the next multiple.
> +    fn take_u32s(&mut self, count: usize) -> Result<&'a [u32]> {
> +        let values = self.take_u64s(count.div_ceil(size_of::<u64>() / size_of::<u32>()))?;
> +        <[u32]>::ref_from_prefix_with_elems(values.as_bytes(), count)
> +            .map(|(elems, _)| elems)
> +            .map_err(|_| EINVAL)
> +    }

[Severity: High]
Can this result in swapped 32-bit words on big-endian systems?

Similar to the byte array extraction above, passing the native memory
representation through .as_bytes() to recreate a [u32] slice will swap
the order of the 32-bit halves on big-endian architectures compared to
little-endian ones.

[ ... ]
> +    /// Decodes every pair into `schema` and returns the result of [`Schema::finish`].
> +    pub(crate) fn decode<'s, S: Schema + Visit<'a>>(
> +        &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();

[Severity: High]
Does this misalign the operation bitfields on big-endian hosts?

The 64-bit operation word is read as a host-native integer via take_u64().
If the NVKV stream comes from a little-endian GSP, parsing it directly as a
native u64 on a big-endian host will misalign the key, index, and value
fields inside Op.

Should this explicitly handle endianness (e.g., using little-endian read
functions) instead of relying on native host integers?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261016-b4-nvkv-v4-0-e8b92b91aa5b@nvidia.com?part=4

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v4 2/7] rust: alloc: add ArrayVec
  2026-10-16  1:53 ` [PATCH v4 2/7] rust: alloc: add ArrayVec Eliot Courtney
@ 2026-10-16  2:04   ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-16  2:04 UTC (permalink / raw)
  To: Eliot Courtney, 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, nova-gpu, dri-devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] rust: missing #[inline] on try_push_init and extend_from_slice
--

commit 83efda44f14b36e4aab4040f689c08677c6c23f1
Author: Eliot Courtney <ecourtney@nvidia.com>

rust: alloc: add ArrayVec

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.

> diff --git a/rust/kernel/alloc/arrayvec.rs b/rust/kernel/alloc/arrayvec.rs
> new file mode 100644
> index 0000000000000..8793adf4d27b7
> --- /dev/null
> +++ b/rust/kernel/alloc/arrayvec.rs

[ ... ]

> +    /// 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 [`TryPushInitError::Full`]. If the
> +    /// initializer itself fails, its error is returned in [`TryPushInitError::InitError`].
> +    pub fn try_push_init<I, E>(&mut self, init: I) -> Result<(), TryPushInitError<I, E>>
> +    where
> +        I: Init<T, E>,
> +    {
> +        let Some(slot) = self.spare_capacity_mut().first_mut() else {

[Severity: Low]
This isn't a bug, but is try_push_init() missing an #[inline] attribute?
The Rust subsystem coding guidelines state that small functions in abstractions
should be annotated with #[inline].

[ ... ]

> +    /// 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 {

[Severity: Low]
This isn't a bug, but is extend_from_slice() also missing an #[inline]
attribute? The Rust subsystem coding guidelines state that small functions in
abstractions should be annotated with #[inline].

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261016-b4-nvkv-v4-0-e8b92b91aa5b@nvidia.com?part=2

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v4 4/7] gpu: nova-core: add NVKV decoder
  2026-10-16  2:00   ` sashiko-bot
@ 2026-10-16  2:05     ` John Hubbard
  0 siblings, 0 replies; 11+ messages in thread
From: John Hubbard @ 2026-10-16  2:05 UTC (permalink / raw)
  To: sashiko-reviews, Eliot Courtney, 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: Alistair Popple, Timur Tabi, nova-gpu, dri-devel, Roman Gushchin

On 10/15/26 7:00 PM, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] gpu: nova-core: endianness bug when extracting u8/u32 arrays from native &[u64] stream
...
>> +++ b/drivers/gpu/nova-core/gsp/nvkv/decode.rs
> [ ... ]
>> +    /// Takes `count` bytes. If `count` is not a multiple of 8 (`u64` size), bytes are discarded up
>> +    /// to the next multiple.
>> +    fn take_u8s(&mut self, count: usize) -> Result<&'a [u8]> {
>> +        let values = self.take_u64s(count.div_ceil(size_of::<u64>()))?;
>> +        values.as_bytes().get(..count).ok_or(EINVAL)
>> +    }
> 
> [Severity: High]
> Does casting a native u64 slice to bytes extract the wrong payload on
> big-endian architectures?

Roman, or anyone who knows,

How do we update sashiko to make it stop reporting on this issue, which
we have decided is a non-issue (Nova is actually configured to only ever
be little-endian)?


thanks,
John Hubbard

> 
> Because Cursor::take_u64s() reads host-native 64-bit integers, using
> .as_bytes() reverses the byte layout on big-endian hosts compared to
> little-endian.
> 
> For example, if a 3-byte payload is stored inside a 64-bit word, a big-endian
> host will have the payload at the end of the word and zero-padding at the
> beginning, causing .get(..count) to extract the zeros instead of the actual
> data payload.
> 
>> +
>> +    /// Takes `count` 32-bit values. If `count` is not a multiple of 2 (`u32` values per `u64`),
>> +    /// bytes are discarded up to the next multiple.
>> +    fn take_u32s(&mut self, count: usize) -> Result<&'a [u32]> {
>> +        let values = self.take_u64s(count.div_ceil(size_of::<u64>() / size_of::<u32>()))?;
>> +        <[u32]>::ref_from_prefix_with_elems(values.as_bytes(), count)
>> +            .map(|(elems, _)| elems)
>> +            .map_err(|_| EINVAL)
>> +    }
> 
> [Severity: High]
> Can this result in swapped 32-bit words on big-endian systems?
> 
> Similar to the byte array extraction above, passing the native memory
> representation through .as_bytes() to recreate a [u32] slice will swap
> the order of the 32-bit halves on big-endian architectures compared to
> little-endian ones.
> 
> [ ... ]
>> +    /// Decodes every pair into `schema` and returns the result of [`Schema::finish`].
>> +    pub(crate) fn decode<'s, S: Schema + Visit<'a>>(
>> +        &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();
> 
> [Severity: High]
> Does this misalign the operation bitfields on big-endian hosts?
> 
> The 64-bit operation word is read as a host-native integer via take_u64().
> If the NVKV stream comes from a little-endian GSP, parsing it directly as a
> native u64 on a big-endian host will misalign the key, index, and value
> fields inside Op.
> 
> Should this explicitly handle endianness (e.g., using little-endian read
> functions) instead of relying on native host integers?
> 



^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-10-16  2:06 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16  1:53 [PATCH v4 0/7] gpu: nova-core: add NVKV codec Eliot Courtney
2026-10-16  1:53 ` [PATCH v4 1/7] rust: alloc: add Vec::try_push_init and Vec::push_init Eliot Courtney
2026-10-16  1:53 ` [PATCH v4 2/7] rust: alloc: add ArrayVec Eliot Courtney
2026-10-16  2:04   ` sashiko-bot
2026-10-16  1:53 ` [PATCH v4 3/7] gpu: nova-core: add NVKV encoder Eliot Courtney
2026-10-16  1:53 ` [PATCH v4 4/7] gpu: nova-core: add NVKV decoder Eliot Courtney
2026-10-16  2:00   ` sashiko-bot
2026-10-16  2:05     ` John Hubbard
2026-10-16  1:53 ` [PATCH v4 5/7] gpu: nova-core: add NVKV typed encoding Eliot Courtney
2026-10-16  1:53 ` [PATCH v4 6/7] gpu: nova-core: add NVKV typed decoding Eliot Courtney
2026-10-16  1:53 ` [PATCH v4 7/7] gpu: nova-core: add NVKV GSP_INIT schemas Eliot Courtney

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.