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! {