[PATCH v2 24/31] gpu: nova-core: gsp: send GSP_INIT and decode its reply

John Hubbard <[email protected]>
Newsgroups dev.linux.lists.nova-gpu,org.kernel.vger.linux-kernel
Message-ID <[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
Reviewed-by: Timur Tabi <[email protected]>
Reviewed-by: Zhi Wang <[email protected]>
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 98d749674df1..d0e93e6a84d0 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<'_>,
@@ -1387,9 +1407,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.
@@ -1405,7 +1429,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;
@@ -1437,7 +1461,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,
+            )
         };
 
         self.gsp_mem
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 89482d2648f5..9a44a1409fb4 100644
--- a/drivers/gpu/nova-core/gsp/fw.rs
+++ b/drivers/gpu/nova-core/gsp/fw.rs
@@ -1012,6 +1012,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 c9deee8d23c8..f41b626ad6d1 100644
--- a/drivers/gpu/nova-core/gsp/fw/commands.rs
+++ b/drivers/gpu/nova-core/gsp/fw/commands.rs
@@ -428,7 +428,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>,
@@ -446,7 +446,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,
@@ -454,7 +454,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! {
-- 
2.55.0
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.