Re: [PATCH 23/27] gpu: nova-core: gsp: send GSP_INIT and decode its reply

Zhi Wang <[email protected]>
Newsgroups dev.linux.lists.nova-gpu,org.kernel.vger.linux-kernel
Message-ID <20260820213642.483b9cce@inno-dell>
On Tue, 18 Aug 2026 20:52:16 -0700
John Hubbard <[email protected]> wrote:

Looks good to me.

Reviewed-by: Zhi Wang <[email protected]>

> GSP-RM answers a GSP_INIT request with the static GPU configuration,
> and that reply is also what signals it has finished starting. It
> raises load-and-execute events in the meantime, so a caller must
> handle them rather than wait through them.
> 
> Nova-core can build the request but cannot send it: the GMC sender is
> private, and the receive path drops the field carrying the reply
> status.
> 
> Add the sender and the receive-side status field it needs, and decode
> the reply into the static-info type the RPC path already produces.
> 
> Assisted-by: Cursor:claude-opus-5
> Signed-off-by: John Hubbard <[email protected]>
> ---
>  drivers/gpu/nova-core/gsp/cmdq.rs        |  49 ++++++--
>  drivers/gpu/nova-core/gsp/commands.rs    | 140
> ++++++++++++++++++++++- drivers/gpu/nova-core/gsp/fw.rs          |
> 4 + drivers/gpu/nova-core/gsp/fw/commands.rs |  35 +++++-
>  4 files changed, 213 insertions(+), 15 deletions(-)
> 
> diff --git a/drivers/gpu/nova-core/gsp/cmdq.rs
> b/drivers/gpu/nova-core/gsp/cmdq.rs index 7da61bc9ad92..82ff46620911
> 100644 --- a/drivers/gpu/nova-core/gsp/cmdq.rs
> +++ b/drivers/gpu/nova-core/gsp/cmdq.rs
> @@ -790,22 +790,43 @@ fn receive_msg<M: MessageFromGsp>(&self,
> timeout: Delta) -> Result<M> self.inner.lock().receive_msg(timeout,
> None) }
>  
> -    /// Receives one GMC event from the GSP and passes its command
> id and raw payload slices to
> -    /// `handler`.
> +    /// Receives one GMC element from the GSP and passes its command
> id, the `max_resp_or_status`
> +    /// field, and the raw payload slices to `handler`.
>      ///
>      /// This method may sleep while waiting. The [`CmdqInner`] mutex
> stays locked across the wait /// and across the `handler` call, so
> `handler` must not call back into this [`Cmdq`]. ///
>      /// See [`CmdqInner::receive_gmc_and_dispatch`] for return
> values, queue state, and errors.
> -    #[expect(dead_code)]
>      pub(crate) fn receive_gmc_and_dispatch<R>(
>          &self,
>          timeout: Delta,
> -        handler: impl FnOnce(u32, &[u8], &[u8]) -> Option<R>,
> +        handler: impl FnOnce(u32, u32, &[u8], &[u8]) -> Option<R>,
>      ) -> Result<Option<R>> {
>          self.inner.lock().receive_gmc_and_dispatch(timeout, handler)
>      }
>  
> +    /// Sends a GMC API command to the GSP without waiting for its
> response.
> +    ///
> +    /// A caller that expects a response reads it with
> [`Self::receive_gmc_and_dispatch`], which
> +    /// lets it handle the events GSP-RM interleaves before the
> response arrives.
> +    ///
> +    /// # Errors
> +    ///
> +    /// - `EMSGSIZE` if the command exceeds the maximum queue
> element size.
> +    /// - `ETIMEDOUT` if space does not become available within the
> timeout.
> +    /// - `EIO` if the command header is not properly aligned.
> +    pub(crate) fn send_gmc_no_wait(
> +        &self,
> +        bar: Bar0<'_>,
> +        command_id: u32,
> +        payload: &[u8],
> +        max_response_size: u32,
> +    ) -> Result {
> +        self.inner
> +            .lock()
> +            .send_gmc(bar, command_id, payload, max_response_size)
> +    }
> +
>      /// Waits for an unsolicited GSP event of type `M`, dispatching
> any other event that arrives /// first.
>      ///
> @@ -1005,7 +1026,6 @@ fn send_command<M>(&mut self, bar: Bar0<'_>,
> command: M) -> Result<u32> /// - `EMSGSIZE` if the command exceeds
> the maximum queue element size. /// - `ETIMEDOUT` if space does not
> become available within the timeout. /// - `EIO` if the command
> header is not properly aligned.
> -    #[expect(dead_code)]
>      fn send_gmc(
>          &mut self,
>          bar: Bar0<'_>,
> @@ -1383,9 +1403,13 @@ fn wait_for_gmc_msg(&self, timeout: Delta) ->
> Result<GmcMessage<'_>> { 
>      /// Receive the next GMC event from the GSP and dispatch it
> through a handler. ///
> -    /// The handler receives the GMC command id and the raw payload
> slices that follow the
> -    /// [`super::fw::GmcApiHeader`] (two slices because the circular
> buffer may wrap). It returns
> -    /// `None` for an event it does not handle.
> +    /// The handler receives the GMC command id, the header's
> `max_resp_or_status` field, and the
> +    /// raw payload slices that follow the
> [`super::fw::GmcApiHeader`] (two slices because the
> +    /// circular buffer may wrap). It returns `None` for an element
> it does not handle.
> +    ///
> +    /// `max_resp_or_status` is a union: GSP-RM writes an
> `NV_STATUS` there when the element is a
> +    /// response, and the maximum response size when it is a
> request. Only a handler that knows
> +    /// which one it asked for can read it.
>      ///
>      /// Where [`Self::receive_msg`] keys on [`MsgFunction`], this
> keys on the GMC command id, /// which is the form the r000 firmware
> uses for boot events. @@ -1401,7 +1425,7 @@ fn
> wait_for_gmc_msg(&self, timeout: Delta) -> Result<GmcMessage<'_>> {
> fn receive_gmc_and_dispatch<R>( &mut self,
>          timeout: Delta,
> -        handler: impl FnOnce(u32, &[u8], &[u8]) -> Option<R>,
> +        handler: impl FnOnce(u32, u32, &[u8], &[u8]) -> Option<R>,
>      ) -> Result<Option<R>> {
>          let message = self.wait_for_gmc_msg(timeout)?;
>          let header = message.header;
> @@ -1420,7 +1444,12 @@ fn receive_gmc_and_dispatch<R>(
>                  length,
>              );
>  
> -            handler(command_id, message.contents.0,
> message.contents.1)
> +            handler(
> +                command_id,
> +                header.gmc.max_resp_or_status,
> +                message.contents.0,
> +                message.contents.1,
> +            )
>          } else {
>              dev_warn!(&self.dev, "GSP GMC: dropping non-GMC queue
> element\n"); None
> diff --git a/drivers/gpu/nova-core/gsp/commands.rs
> b/drivers/gpu/nova-core/gsp/commands.rs index
> 0c3832ccf726..d55faf1a4e04 100644 ---
> a/drivers/gpu/nova-core/gsp/commands.rs +++
> b/drivers/gpu/nova-core/gsp/commands.rs @@ -20,6 +20,7 @@
>  };
>  
>  use crate::{
> +    driver::Bar0,
>      gpu::Chipset,
>      gsp::{
>          cmdq::{
> @@ -32,13 +33,18 @@
>              self,
>              commands::{
>                  GspInitRequest,
> +                GspInitResponse,
> +                GspInitResponseSchema,
>                  RegKey, //
>              },
> -            MsgFunction, //
> +            MsgFunction,
> +            GMCAPI_CMD_GSP_INIT, //
>          },
>          nvkv::{
> +            Decoder,
>              Encodeable,
> -            Encoder, //
> +            Encoder,
> +            UnknownKeyPolicy, //
>          },
>      },
>      sbuffer::SBufferIter,
> @@ -311,6 +317,136 @@ pub(crate) fn build_gsp_init_payload(
>      Ok(encoder.finish())
>  }
>  
> +/// Size of the buffer GSP-RM may fill with static configuration,
> matching the allocation Open RM +/// makes in `kgspSendInitRpcs`.
> +const GSP_INIT_MAX_RESPONSE_SIZE: u32 = 48 * 1024;
> +
> +/// Sends `GSP_INIT` and returns the static configuration its reply
> carries. +///
> +/// GSP-RM interleaves load-and-execute events between the request
> and the reply, and those events +/// drive the falcon loads that let
> it finish starting, so each one is passed to `on_boot_event` +///
> rather than skipped. The reply arrives only once GSP-RM is up, which
> is what makes it the +/// signal that boot is complete. +///
> +/// `payload` is the blob from [`build_gsp_init_payload`].
> +///
> +/// # Errors
> +///
> +/// - `EIO` if GSP-RM reports a failure status, or if the reply is
> not a whole number of NVKV +///   words.
> +/// - `ETIMEDOUT` if neither the reply nor another element arrives
> within +///   [`Cmdq::RECEIVE_TIMEOUT`].
> +///
> +/// Errors from `on_boot_event` and from decoding the reply are
> propagated as-is. +#[expect(dead_code)]
> +pub(crate) fn gsp_init(
> +    cmdq: &Cmdq,
> +    bar: Bar0<'_>,
> +    payload: &[u64],
> +    mut on_boot_event: impl FnMut(u32, &[u8]) -> Result,
> +) -> Result<GetGspStaticInfoReply> {
> +    // Qualified because `zerocopy::IntoBytes` also gives `[T]` an
> `as_bytes`.
> +    let payload = AsBytes::as_bytes(payload);
> +
> +    cmdq.send_gmc_no_wait(
> +        bar,
> +        GMCAPI_CMD_GSP_INIT,
> +        payload,
> +        GSP_INIT_MAX_RESPONSE_SIZE,
> +    )?;
> +
> +    loop {
> +        let reply = cmdq.receive_gmc_and_dispatch(
> +            Cmdq::RECEIVE_TIMEOUT,
> +            |command_id, max_resp_or_status, payload_0, payload_1| {
> +                if command_id == GMCAPI_CMD_GSP_INIT {
> +                    Some(decode_gsp_init_reply(
> +                        max_resp_or_status,
> +                        payload_0,
> +                        payload_1,
> +                    ))
> +                } else {
> +                    // A boot event. Keep waiting for the reply
> unless handling it failed.
> +                    match on_boot_event(command_id, payload_0) {
> +                        Ok(()) => None,
> +                        Err(e) => Some(Err(e)),
> +                    }
> +                }
> +            },
> +        )?;
> +
> +        if let Some(reply) = reply {
> +            return reply;
> +        }
> +    }
> +}
> +
> +/// Decodes the `GSP_INIT` reply, whose `max_resp_or_status` field
> carries an `NV_STATUS`. +fn decode_gsp_init_reply(
> +    status: u32,
> +    payload_0: &[u8],
> +    payload_1: &[u8],
> +) -> Result<GetGspStaticInfoReply> {
> +    if status != 0 {
> +        return Err(EIO);
> +    }
> +
> +    decode_gsp_info(&nvkv_words(payload_0, payload_1)?)
> +}
> +
> +/// Joins the two halves of a wrapped payload into the `u64` words
> an NVKV stream is made of. +///
> +/// # Errors
> +///
> +/// - `EIO` if the combined length is not a whole number of words.
> +/// - `ENOMEM` if the buffer cannot be allocated.
> +fn nvkv_words(payload_0: &[u8], payload_1: &[u8]) ->
> Result<KVVec<u64>> {
> +    let bytes = SBufferIter::new_reader([payload_0,
> payload_1]).flush_into_kvec(GFP_KERNEL)?;
> +    let words = bytes.chunks_exact(size_of::<u64>());
> +    if !words.remainder().is_empty() {
> +        return Err(EIO);
> +    }
> +
> +    let mut out = KVVec::with_capacity(bytes.len() /
> size_of::<u64>(), GFP_KERNEL)?;
> +    for word in words {
> +        let word: [u8; size_of::<u64>()] =
> word.try_into().map_err(|_| EIO)?;
> +        out.push(u64::from_le_bytes(word), GFP_KERNEL)?;
> +    }
> +
> +    Ok(out)
> +}
> +
> +/// Decodes the static GPU configuration from an NVKV stream.
> +///
> +/// # Errors
> +///
> +/// - `EINVAL` if the stream is malformed or omits a required key.
> +/// - `ENOMEM` if the decoded regions cannot be allocated.
> +fn decode_gsp_info(words: &[u64]) -> Result<GetGspStaticInfoReply> {
> +    let decoder = Decoder::new(words, UnknownKeyPolicy::Ignore);
> +    let decoded = KBox::try_init(
> +        decoder.decode(GspInitResponseSchema::default())?,
> +        GFP_KERNEL,
> +    )?;
> +
> +    let mut gpu_name = [0u8; GspInitResponse::MAX_GPU_NAME_LEN];
> +    let name = decoded.gpu_name();
> +    gpu_name
> +        .get_mut(..name.len())
> +        .ok_or(EINVAL)?
> +        .copy_from_slice(name);
> +
> +    let mut usable_fb_regions = KVec::new();
> +    for region in decoded.usable_fb_regions() {
> +        usable_fb_regions.push(region, GFP_KERNEL)?;
> +    }
> +
> +    Ok(GetGspStaticInfoReply {
> +        gpu_name,
> +        usable_fb_regions,
> +    })
> +}
> +
>  pub(crate) use fw::commands::PowerStateLevel;
>  
>  /// The `UnloadingGuestDriver` command, used to shut down the GSP.
> diff --git a/drivers/gpu/nova-core/gsp/fw.rs
> b/drivers/gpu/nova-core/gsp/fw.rs index 4772af362117..6958ee3a2e4e
> 100644 --- a/drivers/gpu/nova-core/gsp/fw.rs
> +++ b/drivers/gpu/nova-core/gsp/fw.rs
> @@ -979,6 +979,10 @@ pub(crate) struct GmcApiHeader {
>  /// `GMCAPI_HEADER_COMMAND_ID_MASK`. The remaining byte carries
> flags. const GMCAPI_COMMAND_ID_MASK: u32 = 0x00ff_ffff;
>  
> +/// GMC command that hands GSP-RM its system information and
> registry keys and returns the static +/// GPU configuration. Its
> reply is also what signals that GSP-RM has finished starting.
> +pub(crate) const GMCAPI_CMD_GSP_INIT: u32 =
> r000_00::GMCAPI_COMMANDS_GMCAPI_CMD_GSP_INIT; + /// GMC command
> asking the driver to run the generic falcon bootloader against a
> descriptor the /// GSP supplies. pub(crate) const
> GMCAPI_CMD_EXEC_GENERIC_BOOTLOADER: u32 = diff --git
> a/drivers/gpu/nova-core/gsp/fw/commands.rs
> b/drivers/gpu/nova-core/gsp/fw/commands.rs index
> bfd756813c64..00e40a435053 100644 ---
> a/drivers/gpu/nova-core/gsp/fw/commands.rs +++
> b/drivers/gpu/nova-core/gsp/fw/commands.rs @@ -402,7 +402,7 @@
> pub(crate) fn new( /// Schema for the `GSP_INIT` response.
> #[cfg_attr(not(CONFIG_KUNIT), allow(dead_code))] #[derive(Default)]
> -    struct GspInitResponseSchema => GspInitResponse {
> +    pub(crate) struct GspInitResponseSchema => GspInitResponse {
>          gpu_name:
>              Array<u8, { GspInitResponse::MAX_GPU_NAME_LEN }, {
> Self::GPU_NAME_STRING_KEY }>, fb_regions: Accumulated<FbRegionSchema>,
> @@ -420,7 +420,7 @@ impl GspInitResponseSchema {
>  
>  /// Payload of the `GSP_INIT` response.
>  #[cfg_attr(not(CONFIG_KUNIT), allow(dead_code))]
> -struct GspInitResponse {
> +pub(crate) struct GspInitResponse {
>      gpu_name: ArrayVec<u8, { Self::MAX_GPU_NAME_LEN }>,
>      fb_regions: KVVec<FbRegion>,
>      bar1_pde_base: u64,
> @@ -428,7 +428,36 @@ struct GspInitResponse {
>  }
>  
>  impl GspInitResponse {
> -    const MAX_GPU_NAME_LEN: usize = 64;
> +    pub(crate) const MAX_GPU_NAME_LEN: usize = 64;
> +
> +    /// A region with no tag is general-purpose memory. A tagged
> region is reserved for a
> +    /// firmware-internal use that the tag identifies.
> +    const FB_REGION_TAG_NONE: u32 = 0;
> +
> +    /// Returns the GPU name, which GSP-RM sends with its NULL
> terminator.
> +    pub(crate) fn gpu_name(&self) -> &[u8] {
> +        self.gpu_name.as_slice()
> +    }
> +
> +    /// Iterates over the FB regions the driver may allocate from.
> +    ///
> +    /// A region qualifies when it is untagged, unprotected, and
> supports both compression and
> +    /// isochronous access, which is the same set the RPC path
> selects from
> +    /// [`GspStaticConfigInfo::usable_fb_regions`].
> +    pub(crate) fn usable_fb_regions(&self) -> impl Iterator<Item =
> Range<u64>> + '_ {
> +        self.fb_regions.iter().filter_map(|region| {
> +            if region.limit >= region.base
> +                && region.tag == Self::FB_REGION_TAG_NONE
> +                && !region.flags.protected()
> +                && region.flags.support_compressed()
> +                && region.flags.support_iso()
> +            {
> +                region.limit.checked_add(1).map(|end|
> region.base..end)
> +            } else {
> +                None
> +            }
> +        })
> +    }
>  }
>  
>  nvkv_decode! {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.