* [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support
@ 2026-07-28 18:39 Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 1/7] drm/tyr: add resources to RegistrationData Deborah Brouwer
` (6 more replies)
0 siblings, 7 replies; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
This series adds firmware loading and MCU boot support to the Tyr DRM
driver. It includes:
- A parser for the Mali CSF firmware binary format
- A kernel-managed BO type (KernelBo) for internal driver allocations
- GPU virtual memory (VM) integration using drm_gpuvm
- An MMU module and a generic slot manager
- Loading firmware, VM activation, and MCU boot at probe()
It is based on drm-rust-next and also depends on:
[PATCH v3] rust: iommu: add device lifetime to IoPageTable
https://lore.kernel.org/rust-for-linux/20260703-pgtable_lt_b4-v3-1-e738e1f513a4@collabora.com
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
Changes in v10:
- rename 'bound to 'drm when not used for bus device private data
- check that mapping/unmapping parameters are page aligned
- Link to v9: https://lore.kernel.org/r/20260722-fw-boot-b4-v9-0-8669d2a02590@collabora.com
Changes in v9:
- reuse SlotData when reactivating a slot
- add fn seat to SlotOperations
- don't allocate gpuva for full unmap_range
- add small sleeps instead of busy-wait for hw
- move sparse address space test to constructor
- use infallible pin_init! for registration data
- add fw stop() to disable MCU and call it from drop()
- check page alignment in parser
- validate firmware header and entry bounds
- allow firmware sections with no VM mapping footprint
- clear stale MCU interrupts before boot
- handle various errors and clean up
- remove various unreachable code
- remove trailing newlines from dev_ prints
- Link to v8: https://lore.kernel.org/r/20260721-fw-boot-b4-v8-0-b28f744581c9@collabora.com
Changes in v8:
- kill the vm if firmware does not initialize properly
- write to JOB_IRQ_CLEAR after MCU boots
- check that a mapping request does not run past the end of the GEM buffer
- check that the SG table gave us enough backing memory for our VA
- make the KernelBo gem object field (bo) private and add an accessor
- return unused gpuva from remap()
- always provide three gpuva to unmap() instead of two
- be stricter about interpreting MAIR bits for device and non-cacheable memory
- preserve idle slot data in case subsequent activation fails
- clear evicted slot after eviction in case a subsequent activation fails
- wait for flush and lock commands to complete before proceeding
- rewrite SectionFlags with the register macro
- use the InaBits conversion provided by the register macro
- rename the slot data argument to vm_as_data for clarity
- add various checks for size, shifting, truncating, sparse values, overflow etc.
- return errors instead of panicking from unwrap()
- use dev_* macros for all logging
- Link to v7: https://lore.kernel.org/r/20260713-fw-boot-b4-v7-0-afd8dc1494dd@collabora.com
Changes in v7:
- slot.rs: don't print an error when all slots are busy
- change all allow(dead_code) to expect(dead_code)
- use LockedSeat type alias in VmAsData
- shift the lockaddr_base so it fits properly in LOCKADDR
- replace core::fmt with kernel::fmt
- make SAFETY comments clearer for map/unmap pages
- fix arm 32-bit build failures
- release gpuvm_unique lock before calling deferred_cleanup
- Link to v6: https://lore.kernel.org/r/20260709-fw-boot-b4-v6-0-ca391e1a4108@collabora.com
Changes in v6:
- Move gpu_info from drm::Driver::Data to RegistrationData
and remove drm::Driver::Data entirely.
- Remove the extra drm::Device ARef in TyrPlatformDriverData
since drm::Registration already owns an ARef to the drm::Device.
- Stop storing platform::Device ARef in firmware since it is unused.
- Pass a generic kernel::Device instead of the platform::Device through
firmware to Vm and pagetables since this is all that they need.
- Link to v5: https://lore.kernel.org/r/20260708-fw-boot-b4-v5-0-7792ab68e359@collabora.com
Changes in v5:
- Reduce the scope of this series back to just MCU booting.
- Drop the CSF global interface, job IRQ, wait, and arch timer patches
since they will be sent as subsequent series.
- Add lifetimes to resources and store them in TyrRegistrationData.
- Link to v4: https://lore.kernel.org/r/20260424-b4-fw-boot-v4-v4-0-a5d91050789d@collabora.com
Changes in v4:
New commits:
- drm/tyr: program CSF global interface
- rust: time: add arch_timer_get_rate wrapper
- drm/tyr: add CSF firmware interface support
- drm/tyr: validate presence of CSF shared section
- drm/tyr: wait for global interface readiness
- drm/tyr: add Job IRQ handling
- drm/tyr: add Wait type for GPU events
The existing commits from v3 remain unchanged.
- Link to v3: https://lore.kernel.org/r/20260413-b4-fw-boot-v3-v3-0-b422f3c03885@collabora.com
Changes in v3:
New commits:
- drm/tyr: remove unused device from platform data
- drm/tyr: use shmem GEM object type in TyrDrmDriver
drm/tyr: select required dependencies in Kconfig
- Rename commit since the dependencies are not limited to DRM.
- Select new RUST_DRM_GEM_SHMEM_HELPER instead of DRM_GEM_SHMEM_HELPER.
drm/tyr: set DMA mask using GPU physical address
- Use register macro to read pa_bits instead of separate helper function.
drm/tyr: add MMU module
- Switch MMU code to typed register APIs (TRANSCFG, MEMATTR, STATUS, LOCKADDR, etc.).
- Use MmuCommand enum for MMU commands instead of raw constants.
- Minor cleanups and renaming (MAX_AS, AS_PRESENT handling).
drm/tyr: add GPU virtual memory module
- Extract VA/PA bits via typed MMU_FEATURES register.
- Update the VM code to match the new GPUVM v6 and shmem GEM v10 APIs.
drm/tyr: add a kernel buffer object
- Reject zero-sized KernelBo allocations up front.
drm/tyr: add firmware loading and MCU boot support
- Use typed GPU control registers.
- Pass iomem by Arc into Firmware::new() since we store it eventually.
- Link to v2: https://lore.kernel.org/rust-for-linux/20260302232500.244489-1-deborah.brouwer@collabora.com/
Changes in v2:
- The whole series is rebased on drm-rust-next including v7.0-rc1.
- Each patch has its own changelog.
Link to v1: https://lore.kernel.org/rust-for-linux/20260212013713.304343-1-deborah.brouwer@collabora.com/
---
Boris Brezillon (3):
drm/tyr: add a generic slot manager
drm/tyr: add Memory Management Unit (MMU) support
drm/tyr: add GPU virtual memory (VM) support
Daniel Almeida (1):
drm/tyr: add parser for firmware binary
Deborah Brouwer (3):
drm/tyr: add resources to RegistrationData
drm/tyr: add a kernel buffer object
drm/tyr: add Microcontroller Unit (MCU) booting
drivers/gpu/drm/tyr/Kconfig | 5 +
drivers/gpu/drm/tyr/driver.rs | 68 ++-
drivers/gpu/drm/tyr/file.rs | 11 +-
drivers/gpu/drm/tyr/fw.rs | 321 +++++++++++
drivers/gpu/drm/tyr/fw/parser.rs | 588 +++++++++++++++++++
drivers/gpu/drm/tyr/gem.rs | 134 ++++-
drivers/gpu/drm/tyr/mmu.rs | 120 ++++
drivers/gpu/drm/tyr/mmu/address_space.rs | 511 +++++++++++++++++
drivers/gpu/drm/tyr/regs.rs | 135 ++++-
drivers/gpu/drm/tyr/slot.rs | 404 +++++++++++++
drivers/gpu/drm/tyr/tyr.rs | 4 +
drivers/gpu/drm/tyr/vm.rs | 949 +++++++++++++++++++++++++++++++
12 files changed, 3215 insertions(+), 35 deletions(-)
---
base-commit: 951aa1f4952cbb4e30ace86c99f248f968d56705
change-id: 20260708-fw-boot-b4-f07706a265c7
Best regards,
--
Deborah Brouwer <deborah.brouwer@collabora.com>
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v10 1/7] drm/tyr: add resources to RegistrationData
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
2026-07-28 18:55 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 2/7] drm/tyr: add a generic slot manager Deborah Brouwer
` (5 subsequent siblings)
6 siblings, 1 reply; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
Currently Tyr is not storing any resources in its drm::Driver
RegistrationData.
Move Tyr's device-private resources and gpu information from
drm::Driver::Data to drm::Driver::RegistrationData. This allows Tyr to
access this data safely within the lifetime of its binding to its parent
platform device and while registered with userspace.
Reviewed-by: Daniel Almeida <daniel.almeida@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/driver.rs | 42 +++++++++++++++++++++---------------------
drivers/gpu/drm/tyr/file.rs | 11 ++++++-----
2 files changed, 27 insertions(+), 26 deletions(-)
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index 8348c6cd3929..728a8388d591 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -6,6 +6,7 @@
OptionalClk, //
},
device::{
+ Bound,
Core,
Device,
DeviceContext, //
@@ -27,10 +28,7 @@
regulator,
regulator::Regulator,
sizes::SZ_2M,
- sync::{
- aref::ARef,
- Mutex, //
- },
+ sync::Mutex,
time, //
};
@@ -53,13 +51,17 @@
#[pin_data(PinnedDrop)]
pub(crate) struct TyrPlatformDriverData<'bound> {
- _device: ARef<TyrDrmDevice>,
_reg: drm::Registration<'bound, TyrDrmDriver>,
}
+/// Data owned by the DRM [`Registration`].
+///
+/// This data can have references tied to the parent platform device binding scope
+/// and is accessible only while the DRM device is registered with userspace.
#[pin_data]
-pub(crate) struct TyrDrmDeviceData {
- pub(crate) pdev: ARef<platform::Device>,
+pub(crate) struct TyrDrmRegistrationData<'drm> {
+ /// Parent platform device.
+ pub(crate) pdev: &'drm platform::Device<Bound>,
#[pin]
clks: Mutex<Clocks>,
@@ -67,9 +69,10 @@ pub(crate) struct TyrDrmDeviceData {
#[pin]
regulators: Mutex<Regulators>,
- /// Some information on the GPU.
- ///
- /// This is mainly queried by userspace, i.e.: Mesa.
+ /// GPU MMIO register mapping.
+ pub(crate) iomem: IoMem<'drm>,
+
+ /// GPU information read from hardware during probe.
pub(crate) gpu_info: GpuInfo,
}
@@ -134,10 +137,10 @@ fn probe<'bound>(
// other threads of execution.
unsafe { pdev.dma_set_mask_and_coherent(DmaMask::try_new(pa_bits)?)? };
- let platform: ARef<platform::Device> = pdev.into();
+ let unreg_dev = drm::UnregisteredDevice::<TyrDrmDriver>::new(pdev, Ok(()))?;
- let data = try_pin_init!(TyrDrmDeviceData {
- pdev: platform.clone(),
+ let reg_data = try_pin_init!(TyrDrmRegistrationData {
+ pdev,
clks <- new_mutex!(Clocks {
core: core_clk,
stacks: stacks_clk,
@@ -147,18 +150,15 @@ fn probe<'bound>(
_mali: mali_regulator,
_sram: sram_regulator,
}),
+ iomem,
gpu_info,
});
- let tdev = drm::UnregisteredDevice::<TyrDrmDriver>::new(pdev, data)?;
// SAFETY: `reg` is stored in `TyrPlatformDriverData` and dropped when the driver is
// unbound; it is never forgotten.
- let reg = unsafe { drm::Registration::new(pdev.as_ref(), tdev, (), 0)? };
+ let reg = unsafe { drm::Registration::new(pdev.as_ref(), unreg_dev, reg_data, 0)? };
- let driver = TyrPlatformDriverData {
- _device: reg.device().into(),
- _reg: reg,
- };
+ let driver = TyrPlatformDriverData { _reg: reg };
// We need this to be dev_info!() because dev_dbg!() does not work at
// all in Rust for now, and we need to see whether probe succeeded.
@@ -184,8 +184,8 @@ fn drop(self: Pin<&mut Self>) {}
#[vtable]
impl drm::Driver for TyrDrmDriver {
- type Data = TyrDrmDeviceData;
- type RegistrationData<'a> = ();
+ type Data = ();
+ type RegistrationData<'drm> = TyrDrmRegistrationData<'drm>;
type File = TyrDrmFileData;
type Object = drm::gem::shmem::Object<BoData>;
type ParentDevice<Ctx: DeviceContext> = platform::Device<Ctx>;
diff --git a/drivers/gpu/drm/tyr/file.rs b/drivers/gpu/drm/tyr/file.rs
index b686041d5d6b..9f60a90d4948 100644
--- a/drivers/gpu/drm/tyr/file.rs
+++ b/drivers/gpu/drm/tyr/file.rs
@@ -12,7 +12,8 @@
use crate::driver::{
TyrDrmDevice,
- TyrDrmDriver, //
+ TyrDrmDriver,
+ TyrDrmRegistrationData, //
};
#[pin_data]
@@ -31,15 +32,15 @@ fn open(_dev: &drm::Device<Self::Driver>) -> Result<Pin<KBox<Self>>> {
impl TyrDrmFileData {
pub(crate) fn dev_query(
- ddev: &TyrDrmDevice<Registered>,
- _reg_data: &(),
+ _ddev: &TyrDrmDevice<Registered>,
+ reg_data: &TyrDrmRegistrationData<'_>,
devquery: &mut uapi::drm_panthor_dev_query,
_file: &TyrDrmFile,
) -> Result<u32> {
if devquery.pointer == 0 {
match devquery.type_ {
uapi::drm_panthor_dev_query_type_DRM_PANTHOR_DEV_QUERY_GPU_INFO => {
- devquery.size = core::mem::size_of_val(&ddev.gpu_info) as u32;
+ devquery.size = core::mem::size_of_val(®_data.gpu_info) as u32;
Ok(0)
}
_ => Err(EINVAL),
@@ -53,7 +54,7 @@ pub(crate) fn dev_query(
)
.writer();
- writer.write(&ddev.gpu_info)?;
+ writer.write(®_data.gpu_info)?;
Ok(0)
}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v10 2/7] drm/tyr: add a generic slot manager
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 1/7] drm/tyr: add resources to RegistrationData Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support Deborah Brouwer
` (4 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
From: Boris Brezillon <boris.brezillon@collabora.com>
Introduce a generic slot manager to dynamically allocate limited hardware
slots to software "seats". It can be used for both address space (AS) and
command stream group (CSG) slots.
The slot manager initially assigns seats to its free slots. It will
continue to reuse the same slot for a seat, as long as another seat does
not start to use the slot in the interim.
When contention arises because all of the slots are allocated, the slot
manager will lazily evict and reuse slots that have become idle (if any).
The seat state is protected using the LockedBy pattern with the same lock
that guards the SlotManager. This ensures the seat state stays consistent
across slot operations.
Hardware specific behaviour is controlled through the SlotManager's
specific manager type that implements the `SlotOperations` trait.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/slot.rs | 405 ++++++++++++++++++++++++++++++++++++++++++++
drivers/gpu/drm/tyr/tyr.rs | 1 +
2 files changed, 406 insertions(+)
diff --git a/drivers/gpu/drm/tyr/slot.rs b/drivers/gpu/drm/tyr/slot.rs
new file mode 100644
index 000000000000..845846e07ee5
--- /dev/null
+++ b/drivers/gpu/drm/tyr/slot.rs
@@ -0,0 +1,405 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Slot management abstraction for limited hardware resources.
+//!
+//! This module provides a generic [`SlotManager`] that assigns limited hardware
+//! slots to logical "seats". A seat represents an entity (such as a virtual memory
+//! (VM) address space) that needs access to a hardware slot.
+//!
+//! The [`SlotManager`] tracks slot allocation using sequence numbers (seqno) to detect
+//! when a seat's binding has been invalidated. When a seat requests activation,
+//! the manager will either reuse the seat's existing slot (if still valid),
+//! allocate a free slot (if any are available), or evict the oldest idle slot if any
+//! slots are idle.
+//!
+//! Hardware-specific behavior is customized by implementing the [`SlotOperations`]
+//! trait, which allows callbacks when slots are activated or evicted.
+//!
+//! This is currently used for managing address space slots in the GPU, and it will
+//! also be used to manage Command Stream Group (CSG) interface slots in the future.
+//!
+//! [SlotOperations]: crate::slot::SlotOperations
+//! [SlotManager]: crate::slot::SlotManager
+#![expect(dead_code)]
+
+use core::{
+ mem,
+ ops::{
+ Deref,
+ DerefMut, //
+ }, //
+};
+
+use kernel::{
+ prelude::*,
+ sync::LockedBy, //
+};
+
+/// Seat information.
+///
+/// This can't be accessed directly by the element embedding a `Seat`,
+/// but is used by the generic slot manager logic to control residency
+/// of a certain object on a hardware slot.
+pub(crate) struct SeatInfo {
+ /// Slot used by this seat.
+ ///
+ /// This index is only valid if the slot pointed to by this index
+ /// has its `SlotInfo::seqno` match `SeatInfo::seqno`. Otherwise,
+ /// it means the object has been evicted from the hardware slot,
+ /// and a new slot needs to be acquired to make this object
+ /// resident again.
+ slot: u8,
+
+ /// Sequence number encoding the last time this seat was active.
+ /// We also use it to check if a slot is still bound to a seat.
+ seqno: u64,
+}
+
+/// Seat state.
+///
+/// This is meant to be embedded in the object that wants to acquire
+/// hardware slots. It also starts in the `Seat::NoSeat` state, and
+/// the slot manager will change the object value when an active/evict
+/// request is issued.
+#[derive(Default)]
+pub(crate) enum Seat {
+ #[expect(clippy::enum_variant_names)]
+ /// Resource is not resident.
+ ///
+ /// All objects start with a seat in the `Seat::NoSeat` state. The seat also
+ /// gets back to that state if the user requests eviction. It
+ /// can also end up in that state next time an operation is done
+ /// on a `Seat::Idle` seat and the slot manager finds out this
+ /// object has been evicted from the slot.
+ #[default]
+ NoSeat,
+
+ /// Resource is actively used and resident.
+ ///
+ /// When a seat is in the `Seat::Active` state, it can't be evicted, and the
+ /// slot pointed to by `SeatInfo::slot` is guaranteed to be reserved
+ /// for this object as long as the seat stays active.
+ Active(SeatInfo),
+
+ /// Resource is idle and might or might not be resident.
+ ///
+ /// When a seat is in the`Seat::Idle` state, we can't know for sure if the
+ /// object is resident or evicted until the next request we issue
+ /// to the slot manager. This tells the slot manager it can
+ /// reclaim the underlying slot if needed.
+ /// In order for the hardware to use this object again, the seat
+ /// needs to be turned into an `Seat::Active` state again
+ /// with a `SlotManager::activate()` call.
+ Idle(SeatInfo),
+}
+
+impl Seat {
+ /// Get the slot index this seat is pointing to.
+ ///
+ /// If the seat is not `Seat::Active` we can't trust the
+ /// `SeatInfo`. In that case `None` is returned, otherwise
+ /// `Some(SeatInfo::slot)` is returned.
+ pub(crate) fn slot(&self) -> Option<u8> {
+ match self {
+ Self::Active(info) => Some(info.slot),
+ _ => None,
+ }
+ }
+}
+
+/// Information related to a slot.
+struct SlotInfo<D> {
+ /// Type specific data attached to a slot.
+ slot_data: D,
+
+ /// Sequence number from when this slot was last activated.
+ seqno: u64,
+}
+
+/// Slot state.
+#[derive(Default)]
+enum Slot<D> {
+ /// Slot is free.
+ #[default]
+ Free,
+
+ /// Slot is active.
+ Active(SlotInfo<D>),
+
+ /// Slot is idle.
+ Idle(SlotInfo<D>),
+}
+
+pub(crate) type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
+
+/// Trait describing the slot-related operations.
+pub(crate) trait SlotOperations<const MAX_SLOTS: usize>: Sized {
+ /// Implementation-specific data associated with each slot.
+ type SlotData;
+
+ /// Returns the seat belonging to this slot data.
+ fn seat(slot_data: &Self::SlotData) -> &LockedSeat<Self, MAX_SLOTS>;
+
+ /// Called when a slot is being activated for a seat.
+ fn activate(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
+ Ok(())
+ }
+
+ /// Called when a slot is being evicted and freed.
+ fn evict(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
+ Ok(())
+ }
+}
+
+/// A generic slot manager that provides access to a limited number of hardware slots.
+pub(crate) struct SlotManager<T: SlotOperations<MAX_SLOTS>, const MAX_SLOTS: usize> {
+ /// A specific implementation of the generic slot manager.
+ manager: T,
+
+ /// Number of slots actually available.
+ slot_count: usize,
+
+ /// Slot array used to track the state of each slot.
+ slots: [Slot<T::SlotData>; MAX_SLOTS],
+
+ /// Sequence number incremented each time a Seat is successfully activated
+ use_seqno: u64,
+}
+
+impl<T: SlotOperations<MAX_SLOTS>, const MAX_SLOTS: usize> SlotManager<T, MAX_SLOTS> {
+ /// Creates a specific instance of a slot manager.
+ pub(crate) fn new(manager: T, slot_count: usize) -> Result<Self> {
+ if slot_count == 0 {
+ return Err(EINVAL);
+ }
+ if slot_count > MAX_SLOTS {
+ return Err(EINVAL);
+ }
+ // Since the slot index is stored in SeatInfo as a u8, the maximum number of slots is 256.
+ if slot_count > u8::MAX as usize + 1 {
+ return Err(EINVAL);
+ }
+
+ Ok(Self {
+ manager,
+ slot_count,
+ slots: [const { Slot::Free }; MAX_SLOTS],
+ use_seqno: 1,
+ })
+ }
+
+ /// Records a newly activated slot for the given seat.
+ /// The slot manager takes ownership of the hardware-specific slot data.
+ fn record_active_slot(&mut self, slot_idx: usize, slot_data: T::SlotData) {
+ let cur_seqno = self.use_seqno;
+
+ *T::seat(&slot_data).access_mut(self) = Seat::Active(SeatInfo {
+ slot: slot_idx as u8,
+ seqno: cur_seqno,
+ });
+
+ self.slots[slot_idx] = Slot::Active(SlotInfo {
+ slot_data,
+ seqno: cur_seqno,
+ });
+
+ self.use_seqno += 1;
+ }
+
+ /// Reactivates an active/idle slot for a given seat without reprogramming the hardware.
+ /// The SlotManager reuses the existing slot_data. This ensures that the hardware-specific
+ /// information is not changed between subsequent uses. It also ensures that resources
+ /// owned by the existing slot_data remain alive while the hardware is configured to use them.
+ fn reactivate_slot(&mut self, slot_idx: usize, slot_data: &T::SlotData) -> Result {
+ let cur_seqno = self.use_seqno;
+
+ let mut slot_info = match mem::take(&mut self.slots[slot_idx]) {
+ Slot::Active(slot_info) | Slot::Idle(slot_info) => slot_info,
+ Slot::Free => {
+ *T::seat(slot_data).access_mut(self) = Seat::NoSeat;
+ return Err(EINVAL);
+ }
+ };
+
+ *T::seat(slot_data).access_mut(self) = Seat::Active(SeatInfo {
+ slot: slot_idx as u8,
+ seqno: cur_seqno,
+ });
+
+ slot_info.seqno = cur_seqno;
+ self.slots[slot_idx] = Slot::Active(slot_info);
+
+ self.use_seqno += 1;
+
+ Ok(())
+ }
+
+ /// Activates a slot for the given seat.
+ fn activate_slot(&mut self, slot_idx: usize, slot_data: T::SlotData) -> Result {
+ self.manager.activate(slot_idx, &slot_data)?;
+ self.record_active_slot(slot_idx, slot_data);
+ Ok(())
+ }
+
+ /// Finds a slot for the given seat. A free slot is preferred, but if none
+ /// are available, the oldest idle slot is evicted and reused. Otherwise, if
+ /// there are no free or idle slots, return [`EBUSY`].
+ fn allocate_slot(&mut self, slot_data: T::SlotData) -> Result {
+ let slots = &self.slots[..self.slot_count];
+
+ let mut idle_slot_idx = None;
+ let mut idle_slot_seqno: u64 = 0;
+
+ for (slot_idx, slot) in slots.iter().enumerate() {
+ match slot {
+ Slot::Free => {
+ return self.activate_slot(slot_idx, slot_data);
+ }
+ Slot::Idle(slot_info) => {
+ if idle_slot_idx.is_none() || slot_info.seqno < idle_slot_seqno {
+ idle_slot_idx = Some(slot_idx);
+ idle_slot_seqno = slot_info.seqno;
+ }
+ }
+ Slot::Active(_) => (),
+ }
+ }
+
+ match idle_slot_idx {
+ Some(slot_idx) => {
+ // Lazily evict idle slot just before it is reused.
+ if let Slot::Idle(slot_info) = &self.slots[slot_idx] {
+ self.manager.evict(slot_idx, &slot_info.slot_data)?;
+ mem::take(&mut self.slots[slot_idx]);
+ }
+ self.activate_slot(slot_idx, slot_data)
+ }
+ None => Err(EBUSY),
+ }
+ }
+
+ /// Converts an active slot and its seat to idle state.
+ fn idle_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ let slot = mem::take(&mut self.slots[slot_idx]);
+
+ self.slots[slot_idx] = match slot {
+ // If the slot was active, make it idle.
+ Slot::Active(slot_info) => Slot::Idle(slot_info),
+
+ // Preserve an already-idle slot.
+ Slot::Idle(slot_info) => Slot::Idle(slot_info),
+
+ // A free slot remains free.
+ Slot::Free => Slot::Free,
+ };
+
+ // If the seat was active, make it idle, or keep it idle if it was already idle.
+ *locked_seat.access_mut(self) = match locked_seat.access(self) {
+ Seat::Active(seat_info) | Seat::Idle(seat_info) => Seat::Idle(SeatInfo {
+ slot: seat_info.slot,
+ seqno: seat_info.seqno,
+ }),
+ Seat::NoSeat => Seat::NoSeat,
+ };
+ Ok(())
+ }
+
+ /// Evicts an active or idle slot: calls the eviction callback and marks the slot as free
+ /// and the seat as NoSeat.
+ fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ match &self.slots[slot_idx] {
+ Slot::Active(slot_info) | Slot::Idle(slot_info) => {
+ // If hardware eviction fails (e.g. times out), the slot retains
+ // its SlotData so that any resources still referenced by the hardware
+ // will remain alive. This prevents use-after-free errors.
+ self.manager.evict(slot_idx, &slot_info.slot_data)?;
+ mem::take(&mut self.slots[slot_idx]);
+ }
+ _ => (),
+ }
+
+ *locked_seat.access_mut(self) = Seat::NoSeat;
+ Ok(())
+ }
+
+ /// Checks that the seat state matches the slot's state.
+ /// If they don't match, the seat is stale and is reset to `NoSeat`.
+ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
+ let (slot_idx, seat_seqno, is_active) = match locked_seat.access(self) {
+ Seat::Active(seat_info) => (seat_info.slot as usize, seat_info.seqno, true),
+ Seat::Idle(seat_info) => (seat_info.slot as usize, seat_info.seqno, false),
+ _ => return,
+ };
+
+ let valid = if is_active {
+ !kernel::warn_on!(!matches!(
+ &self.slots[slot_idx],
+ Slot::Active(slot_info) if slot_info.seqno == seat_seqno
+ ))
+ } else {
+ matches!(
+ &self.slots[slot_idx],
+ Slot::Idle(slot_info) if slot_info.seqno == seat_seqno
+ )
+ };
+
+ if !valid {
+ *locked_seat.access_mut(self) = Seat::NoSeat;
+ }
+ }
+
+ /// Activates a resource on any available/reclaimable slot.
+ pub(crate) fn activate(&mut self, slot_data: T::SlotData) -> Result {
+ self.check_seat(T::seat(&slot_data));
+
+ // Copy out only the slot index so the borrow of slot_data ends here.
+ let slot_idx = match T::seat(&slot_data).access(self) {
+ Seat::Active(seat_info) | Seat::Idle(seat_info) => Some(seat_info.slot as usize),
+ Seat::NoSeat => None,
+ };
+
+ match slot_idx {
+ Some(slot_idx) => self.reactivate_slot(slot_idx, &slot_data),
+ None => self.allocate_slot(slot_data),
+ }
+ }
+
+ /// Flag a resource as idle. This method will be used for user VM support.
+ #[expect(dead_code)]
+ pub(crate) fn idle(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ self.check_seat(locked_seat);
+ if let Seat::Active(seat_info) = locked_seat.access(self) {
+ self.idle_slot(seat_info.slot as usize, locked_seat)?;
+ }
+ Ok(())
+ }
+
+ /// Evict a resource from its slot.
+ pub(crate) fn evict(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ self.check_seat(locked_seat);
+
+ match locked_seat.access(self) {
+ Seat::Active(seat_info) | Seat::Idle(seat_info) => {
+ let slot_idx = seat_info.slot as usize;
+ self.evict_slot(slot_idx, locked_seat)?;
+ }
+ _ => (),
+ }
+
+ Ok(())
+ }
+}
+
+impl<T: SlotOperations<MAX_SLOTS>, const MAX_SLOTS: usize> Deref for SlotManager<T, MAX_SLOTS> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.manager
+ }
+}
+
+impl<T: SlotOperations<MAX_SLOTS>, const MAX_SLOTS: usize> DerefMut for SlotManager<T, MAX_SLOTS> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.manager
+ }
+}
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index 95cda7b0962f..7c9a8063b3b9 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -12,6 +12,7 @@
mod gem;
mod gpu;
mod regs;
+mod slot;
kernel::module_platform_driver! {
type: TyrPlatformDriver,
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 1/7] drm/tyr: add resources to RegistrationData Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 2/7] drm/tyr: add a generic slot manager Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
2026-07-28 18:52 ` sashiko-bot
2026-07-28 19:31 ` Danilo Krummrich
2026-07-28 18:39 ` [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support Deborah Brouwer
` (3 subsequent siblings)
6 siblings, 2 replies; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
From: Boris Brezillon <boris.brezillon@collabora.com>
Add Memory Management Unit (MMU) support in Tyr. The MMU module wraps a
SlotManager instance to allocate MMU address-space slots for use by
virtual memory (VM) address spaces. The MMU's SlotManager uses an
AddressSpaceManager to handle the hardware-specific callbacks. For
example, the AddressSpaceManager activates and evicts VMs from slots by
writing commands to the MMU registers.
Add an implementation block for the MMU's MEMATTR register to provide
a method for translating the Memory Attribute Indirection Register (MAIR)
format from the pagetable configuration to a format understood by the MMU.
Create an mmu instance during probe, it will be used by subsequent patches
in this series.
Wrap the iomem stored in TyrDrmRegistrationData in an Arc. The iomem
is stored in the mmu through its AddressSpaceManager. In anticipation
of the iomem also being stored in the firmware object, set up shared
ownership of the iomem now.
Update Kconfig to add the new MMU and IOMMU dependencies required
by this MMU module.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/Kconfig | 3 +
drivers/gpu/drm/tyr/driver.rs | 13 +-
drivers/gpu/drm/tyr/mmu.rs | 121 ++++++++
drivers/gpu/drm/tyr/mmu/address_space.rs | 511 +++++++++++++++++++++++++++++++
drivers/gpu/drm/tyr/regs.rs | 135 +++++++-
drivers/gpu/drm/tyr/slot.rs | 1 -
drivers/gpu/drm/tyr/tyr.rs | 1 +
7 files changed, 780 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/tyr/Kconfig b/drivers/gpu/drm/tyr/Kconfig
index 51a68ef8212c..61a2fd6f961a 100644
--- a/drivers/gpu/drm/tyr/Kconfig
+++ b/drivers/gpu/drm/tyr/Kconfig
@@ -5,9 +5,12 @@ config DRM_TYR
depends on DRM=y
depends on RUST
depends on ARM || ARM64 || COMPILE_TEST
+ depends on MMU
depends on !GENERIC_ATOMIC64 # for IOMMU_IO_PGTABLE_LPAE
depends on COMMON_CLK
+ depends on IOMMU_SUPPORT
default n
+ select IOMMU_IO_PGTABLE_LPAE
select RUST_DRM_GEM_SHMEM_HELPER
help
Rust DRM driver for ARM Mali CSF-based GPUs.
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index 728a8388d591..92c9d98e3aab 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -28,7 +28,10 @@
regulator,
regulator::Regulator,
sizes::SZ_2M,
- sync::Mutex,
+ sync::{
+ Arc,
+ Mutex, //
+ },
time, //
};
@@ -37,6 +40,7 @@
gem::BoData,
gpu,
gpu::GpuInfo,
+ mmu::Mmu,
regs::gpu_control::*, //
};
@@ -70,7 +74,7 @@ pub(crate) struct TyrDrmRegistrationData<'drm> {
regulators: Mutex<Regulators>,
/// GPU MMIO register mapping.
- pub(crate) iomem: IoMem<'drm>,
+ pub(crate) iomem: Arc<IoMem<'drm>>,
/// GPU information read from hardware during probe.
pub(crate) gpu_info: GpuInfo,
@@ -121,7 +125,8 @@ fn probe<'bound>(
let sram_regulator = Regulator::<regulator::Enabled>::get(pdev.as_ref(), c"sram")?;
let request = pdev.io_request_by_index(0).ok_or(ENODEV)?;
- let iomem = request.iomap_sized::<SZ_2M>()?;
+
+ let iomem = Arc::new(request.iomap_sized::<SZ_2M>()?, GFP_KERNEL)?;
issue_soft_reset(pdev.as_ref(), &iomem)?;
gpu::l2_power_on(pdev.as_ref(), &iomem)?;
@@ -139,6 +144,8 @@ fn probe<'bound>(
let unreg_dev = drm::UnregisteredDevice::<TyrDrmDriver>::new(pdev, Ok(()))?;
+ let _mmu = Mmu::new(pdev.as_ref(), iomem.as_arc_borrow(), &gpu_info)?;
+
let reg_data = try_pin_init!(TyrDrmRegistrationData {
pdev,
clks <- new_mutex!(Clocks {
diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
new file mode 100644
index 000000000000..83e022f9168b
--- /dev/null
+++ b/drivers/gpu/drm/tyr/mmu.rs
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Memory Management Unit (MMU) module.
+//!
+//! The GPU MMU provides a limited number of memory address spaces for use by command streams.
+//! The MMU translates virtual addresses to physical addresses and manages memory configuration
+//! and access permissions.
+//!
+//! This MMU module is essentially a locked wrapper around a [`SlotManager`] instance.
+//! The [`SlotManager`] manages the assignment of virtual address spaces to hardware address-space
+//! (AS) slots. MMU commands such as updates and flushes are carried out by the
+//! [`AddressSpaceManager`] which actually writes to the MMU registers.
+#![expect(dead_code)]
+
+use core::ops::Range;
+
+use kernel::{
+ device::{
+ Bound,
+ Device, //
+ },
+ new_mutex,
+ prelude::*,
+ sync::{
+ Arc,
+ ArcBorrow,
+ Mutex, //
+ }, //
+};
+
+use crate::{
+ driver::IoMem,
+ gpu::GpuInfo,
+ mmu::address_space::{
+ AddressSpaceManager,
+ VmAsData, //
+ },
+ regs::{
+ gpu_control::AS_PRESENT,
+ MAX_AS, //
+ },
+ slot::SlotManager, //
+};
+
+pub(crate) mod address_space;
+
+pub(crate) type AsSlotManager<'drm> = SlotManager<AddressSpaceManager<'drm>, MAX_AS>;
+
+/// Locked wrapper for carrying out virtual memory (VM) operations on the MMU.
+#[pin_data]
+pub(crate) struct Mmu<'drm> {
+ /// Slot Manager instance used to allocate hardware slots and write to MMU registers.
+ #[pin]
+ pub(crate) as_manager: Mutex<AsSlotManager<'drm>>,
+}
+
+impl<'drm> Mmu<'drm> {
+ /// Create an MMU component for this device.
+ pub(crate) fn new(
+ dev: &'drm Device<Bound>,
+ iomem: ArcBorrow<'_, IoMem<'drm>>,
+ gpu_info: &GpuInfo,
+ ) -> Result<Arc<Mmu<'drm>>> {
+ let present = AS_PRESENT::from_raw(gpu_info.as_present).present().get();
+ let slot_count = present.count_ones().try_into()?;
+
+ let address_space_manager = AddressSpaceManager::new(dev, iomem.into(), present)?;
+ let as_slot_manager =
+ SlotManager::new(address_space_manager, slot_count).inspect_err(|e| {
+ dev_err!(
+ dev,
+ "Failed to initialize MMU slot manager with {} slots: {:?}",
+ slot_count,
+ e
+ );
+ })?;
+ let mmu_init = try_pin_init!(Self{
+ as_manager <- new_mutex!(as_slot_manager),
+ });
+ Arc::pin_init(mmu_init, GFP_KERNEL)
+ }
+
+ /// Assign a VM to an AS slot, provide a translation table,
+ /// and update the MMU to make the VM resident.
+ pub(crate) fn activate_vm(&self, vm_as_data: ArcBorrow<'_, VmAsData<'drm>>) -> Result {
+ self.as_manager.lock().activate_vm(vm_as_data)
+ }
+
+ /// Evict a VM from its AS slot and flush the MMU.
+ pub(crate) fn deactivate_vm(&self, vm_as_data: &VmAsData<'drm>) -> Result {
+ self.as_manager.lock().deactivate_vm(vm_as_data)
+ }
+
+ /// Flush MMU translation caches after a VM update.
+ pub(crate) fn flush_vm(&self, vm_as_data: &VmAsData<'drm>) -> Result {
+ self.as_manager.lock().flush_vm(vm_as_data)
+ }
+
+ /// Flags the start of a VM update.
+ ///
+ /// If the VM is resident, any GPU access on the memory range being
+ /// updated will be blocked until `Mmu::end_vm_update()` is called.
+ /// This guarantees the atomicity of a VM update.
+ /// If the VM is not resident, this is a NOP.
+ pub(crate) fn start_vm_update(
+ &self,
+ vm_as_data: &VmAsData<'drm>,
+ region: &Range<u64>,
+ ) -> Result {
+ self.as_manager.lock().start_vm_update(vm_as_data, region)
+ }
+
+ /// Flags the end of a VM update.
+ ///
+ /// If the VM is resident, this will let GPU accesses on the updated
+ /// range go through, in case any of them were blocked.
+ /// If the VM is not resident, this is a NOP.
+ pub(crate) fn end_vm_update(&self, vm_as_data: &VmAsData<'drm>) -> Result {
+ self.as_manager.lock().end_vm_update(vm_as_data)
+ }
+}
diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
new file mode 100644
index 000000000000..76e3d2df6afd
--- /dev/null
+++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
@@ -0,0 +1,511 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Address space module.
+//!
+//! This module handles the hardware interaction for MMU operations through
+//! MMIO register access.
+//!
+
+use core::ops::Range;
+
+use kernel::{
+ device::{
+ Bound,
+ Device, //
+ }, //
+ error::Result,
+ io::{
+ poll,
+ register::Array,
+ Io, //
+ },
+ iommu::pgtable::{
+ Config,
+ IoPageTable,
+ ARM64LPAES1, //
+ },
+ num::Bounded,
+ prelude::*,
+ sizes::{
+ SZ_2M,
+ SZ_4K, //
+ },
+ sync::{
+ Arc,
+ ArcBorrow,
+ LockedBy, //
+ },
+ time::Delta, //
+};
+
+use crate::{
+ driver::IoMem,
+ mmu::{
+ AsSlotManager,
+ Mmu, //
+ },
+ regs::{
+ mmu_control::mmu_as_control,
+ mmu_control::mmu_as_control::*,
+ MAX_AS, //
+ },
+ slot::{
+ LockedSeat,
+ Seat,
+ SlotOperations, //
+ }, //
+};
+
+/// Address space configuration values to be written to MMU registers.
+#[derive(Clone, Copy)]
+struct AddressSpaceConfig {
+ /// Translation configuration. Configures how the MMU walks the page table for this
+ /// address space.
+ transcfg: u64,
+
+ /// Translation table base address. The address of the page table.
+ transtab: u64,
+
+ /// Memory attributes such as cacheability.
+ memattr: u64,
+}
+
+/// Virtual memory (VM) address space data for use in MMU operations.
+#[pin_data]
+pub(crate) struct VmAsData<'drm> {
+ /// This address-space seat tracks this VM's binding to a hardware address space slot.
+ /// It can only be accessed when holding the `Mmu::as_manager` lock.
+ as_seat: LockedSeat<AddressSpaceManager<'drm>, MAX_AS>,
+
+ /// Virtual address bits for this address space.
+ va_bits: u8,
+
+ /// The page table which maps GPU virtual addresses to physical addresses for this VM.
+ #[pin]
+ pub(crate) page_table: IoPageTable<'drm, ARM64LPAES1>,
+}
+
+impl<'drm> VmAsData<'drm> {
+ /// Creates VM address space data by initializing all of its fields.
+ pub(crate) fn new<'a>(
+ mmu: &'a Mmu<'drm>,
+ dev: &'drm Device<Bound>,
+ va_bits: u32,
+ pa_bits: u32,
+ ) -> impl pin_init::PinInit<VmAsData<'drm>, Error> + 'a {
+ let pt_config = Config {
+ quirks: 0,
+ pgsize_bitmap: SZ_4K | SZ_2M,
+ ias: va_bits,
+ oas: pa_bits,
+ coherent_walk: false,
+ };
+
+ let page_table_init = IoPageTable::new(dev, pt_config);
+
+ try_pin_init!(Self {
+ as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
+ va_bits: va_bits as u8,
+ page_table <- page_table_init,
+ }? Error)
+ }
+
+ /// Computes the hardware configuration for this address space.
+ fn as_config(&self) -> Result<AddressSpaceConfig> {
+ let pt = &self.page_table;
+ // The hardware computes the valid input address range as:
+ // INA_BITS_VALID = min(HW_INA_BITS, 55 - INA_BITS)
+ // To configure our desired va_bits, we solve for INA_BITS:
+ // INA_BITS = 55 - va_bits
+ // This assumes HW_INA_BITS (hardware capability) >= va_bits.
+ let field = 55u64.checked_sub(self.va_bits.into()).ok_or(EINVAL)?;
+ let ina_bits =
+ match mmu_as_control::InaBits::try_from(Bounded::try_new(field).ok_or(EINVAL)?)? {
+ mmu_as_control::InaBits::Reset => return Err(EINVAL),
+ bits => bits,
+ };
+
+ let transcfg = mmu_as_control::TRANSCFG::zeroed()
+ .with_ptw_memattr(mmu_as_control::PtwMemattr::WriteBack)
+ .with_r_allocate(true)
+ .with_mode(mmu_as_control::AddressSpaceMode::Aarch64_4K)
+ .with_ina_bits(ina_bits)
+ .into_raw();
+
+ Ok(AddressSpaceConfig {
+ transcfg,
+ // SAFETY: The SlotManager holds an `Arc<VmAsData>` as SlotData while this
+ // TTBR is programmed and stores that Arc in the active slot before
+ // returning. Eviction flushes and disables the slot before releasing
+ // the Arc; if eviction fails, the slot retains it. Therefore the page
+ // table cannot be dropped while the GPU is using it.
+ transtab: unsafe { pt.ttbr() },
+ memattr: MEMATTR::from_mair(pt.mair()).into_raw(),
+ })
+ }
+}
+
+/// Coordinates all hardware-level address space operations through MMIO register
+/// operations including enabling, disabling, flushing, and updating address spaces.
+pub(crate) struct AddressSpaceManager<'drm> {
+ /// Parent device used for logging.
+ dev: &'drm Device<Bound>,
+
+ /// Memory-mapped I/O region for GPU register access.
+ iomem: Arc<IoMem<'drm>>,
+
+ /// Bitmask of present address space slots from GPU_AS_PRESENT register.
+ as_present: u32,
+}
+
+impl<'drm> AddressSpaceManager<'drm> {
+ /// Creates a new address space manager.
+ ///
+ /// Initializes the manager with references to the platform device and
+ /// I/O memory region, along with the bitmask of available AS slots.
+ pub(super) fn new(
+ dev: &'drm Device<Bound>,
+ iomem: Arc<IoMem<'drm>>,
+ as_present: u32,
+ ) -> Result<AddressSpaceManager<'drm>> {
+ if as_present.trailing_ones() != as_present.count_ones() {
+ dev_err!(
+ dev,
+ "Sparse AS_PRESENT mask is unsupported: {:#x}",
+ as_present
+ );
+ return Err(EINVAL);
+ }
+ Ok(Self {
+ dev,
+ iomem,
+ as_present,
+ })
+ }
+
+ /// Validates that an AS slot number is within range and present in hardware.
+ ///
+ /// Checks that the slot index is less than [`MAX_AS`] and that
+ /// the corresponding bit is set in the `as_present` mask read from the GPU.
+ ///
+ /// Returns [`EINVAL`] if the slot is out of range or not present in hardware.
+ fn validate_as_slot(&self, as_nr: usize) -> Result {
+ if as_nr >= MAX_AS {
+ dev_err!(
+ self.dev,
+ "AS slot {} out of valid range (max {})",
+ as_nr,
+ MAX_AS
+ );
+ return Err(EINVAL);
+ }
+
+ if (self.as_present & (1 << as_nr)) == 0 {
+ dev_err!(
+ self.dev,
+ "AS slot {} not present in hardware (AS_PRESENT={:#x})",
+ as_nr,
+ self.as_present
+ );
+ return Err(EINVAL);
+ }
+ Ok(())
+ }
+
+ /// Waits for an AS slot to become ready (not active).
+ ///
+ /// Returns an error if polling times out after 10ms or if register access fails.
+ fn as_wait_ready(&self, as_nr: usize) -> Result {
+ let io = &*self.iomem;
+ let op = || {
+ let status_reg = STATUS::try_at(as_nr).ok_or(EINVAL)?;
+ Ok(io.read(status_reg))
+ };
+ let cond = |status: &STATUS| -> bool { !status.active_ext() };
+ poll::read_poll_timeout(op, cond, Delta::from_micros(50), Delta::from_millis(10))?;
+
+ Ok(())
+ }
+
+ /// Sends a command to an AS slot.
+ ///
+ /// Returns an error if waiting for ready times out or if register write fails.
+ fn as_send_cmd(&mut self, as_nr: usize, cmd: MmuCommand) -> Result {
+ self.as_wait_ready(as_nr)?;
+ let io = &*self.iomem;
+ let command_reg = COMMAND::try_at(as_nr).ok_or(EINVAL)?;
+ io.write(command_reg, COMMAND::zeroed().with_command(cmd));
+ Ok(())
+ }
+
+ /// Sends a command to an AS slot and waits for completion.
+ ///
+ /// Returns an error if sending the command fails or if waiting for completion times out.
+ fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: MmuCommand) -> Result {
+ self.as_send_cmd(as_nr, cmd)?;
+ self.as_wait_ready(as_nr)?;
+ Ok(())
+ }
+
+ /// Enables an AS slot with the provided configuration.
+ ///
+ /// Returns an error if the slot is invalid or if register writes/commands fail.
+ fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig) -> Result {
+ self.validate_as_slot(as_nr)?;
+
+ let io = &*self.iomem;
+
+ let transtab = as_config.transtab;
+ io.write(
+ TRANSTAB_LO::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSTAB_LO::from_raw(transtab as u32),
+ );
+ io.write(
+ TRANSTAB_HI::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSTAB_HI::from_raw((transtab >> 32) as u32),
+ );
+
+ let transcfg = as_config.transcfg;
+ io.write(
+ TRANSCFG_LO::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSCFG_LO::from_raw(transcfg as u32),
+ );
+ io.write(
+ TRANSCFG_HI::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSCFG_HI::from_raw((transcfg >> 32) as u32),
+ );
+
+ let memattr = as_config.memattr;
+ io.write(
+ MEMATTR_LO::try_at(as_nr).ok_or(EINVAL)?,
+ MEMATTR_LO::from_raw(memattr as u32),
+ );
+ io.write(
+ MEMATTR_HI::try_at(as_nr).ok_or(EINVAL)?,
+ MEMATTR_HI::from_raw((memattr >> 32) as u32),
+ );
+
+ self.as_send_cmd_and_wait(as_nr, MmuCommand::Update)?;
+
+ Ok(())
+ }
+
+ /// Disables an AS slot and clears its configuration.
+ ///
+ /// Returns an error if the slot is invalid or if register writes/commands fail.
+ fn as_disable(&mut self, as_nr: usize) -> Result {
+ self.validate_as_slot(as_nr)?;
+
+ // Flush AS before disabling
+ self.as_send_cmd_and_wait(as_nr, MmuCommand::FlushMem)?;
+
+ let io = &*self.iomem;
+
+ io.write(
+ TRANSTAB_LO::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSTAB_LO::from_raw(0),
+ );
+ io.write(
+ TRANSTAB_HI::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSTAB_HI::from_raw(0),
+ );
+
+ io.write(
+ MEMATTR_LO::try_at(as_nr).ok_or(EINVAL)?,
+ MEMATTR_LO::from_raw(0),
+ );
+ io.write(
+ MEMATTR_HI::try_at(as_nr).ok_or(EINVAL)?,
+ MEMATTR_HI::from_raw(0),
+ );
+
+ let transcfg = TRANSCFG::zeroed()
+ .with_mode(AddressSpaceMode::Unmapped)
+ .into_raw();
+
+ io.write(
+ TRANSCFG_LO::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSCFG_LO::from_raw(transcfg as u32),
+ );
+ io.write(
+ TRANSCFG_HI::try_at(as_nr).ok_or(EINVAL)?,
+ TRANSCFG_HI::from_raw((transcfg >> 32) as u32),
+ );
+
+ self.as_send_cmd_and_wait(as_nr, MmuCommand::Update)?;
+
+ Ok(())
+ }
+
+ /// Locks a region of the translation tables for an atomic update.
+ ///
+ /// Programs the MMU [`LOCKADDR`] register for the given address space and issues
+ /// the lock command. The hardware rounds the requested range up to a
+ /// power-of-two region aligned to its size.
+ ///
+ /// Returns an error if the slot is invalid or if register writes/commands fail.
+ fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
+ self.validate_as_slot(as_nr)?;
+
+ // Avoid both an empty range and an inverted range.
+ if region.start >= region.end {
+ return Err(EINVAL);
+ }
+
+ // The lock operates on full 64-byte cache lines of translation table entries.
+ // Since each translation table entry (TTE) is 8 bytes, a cache line has 8 TTEs.
+ // Since each TTE maps one page, the minimum locked region size will be 8 pages.
+ //
+ // With 4KiB pages (Aarch64_4K mode), the minimum locked region is 32KiB.
+ let lock_region_min_size: u64 = 4096 * 8;
+
+ // Count the number of trailing zero bits (zeros at the right/least-significant
+ // end of the binary representation). For a power-of-two value, this equals the
+ // base-2 exponent (e.g., 32 KiB = 2^15 → 15).
+ let lock_region_min_size_log2 = lock_region_min_size.trailing_zeros() as u8;
+
+ // XOR the first and last addresses to identify which bits differ between them.
+ // The highest set bit in the result determines the exponent of the smallest
+ // power-of-two region that can contain both addresses.
+ //
+ // Example:
+ // addr_xor = 0x1000 ^ 0x2FFF = 0x3FFF
+ // highest set bit in 0x3FFF is bit 13
+ // minimum region size = 2^(13 + 1) = 16 KiB
+ let addr_xor = region.start ^ (region.end - 1);
+ let region_size_log2 = 64 - addr_xor.leading_zeros() as u8;
+
+ let lock_region_log2 = core::cmp::max(region_size_log2, lock_region_min_size_log2);
+
+ let lock_region_size = 1u64.checked_shl(lock_region_log2.into()).ok_or(EINVAL)?;
+ // Align the LOCKADDR base address down to the lock region size (1 << lock_region_log2).
+ //
+ // The MMU ignores the low lock_region_log2 bits of LOCKADDR base, so ensure
+ // they are cleared in software to avoid ambiguity.
+ //
+ // Example:
+ // lock_region_log2 = 14 (16 KiB)
+ // region.start = 0x1000
+ // lockaddr_base = 0x1000 & ~(0x3FFF) = 0x0000
+ let lockaddr_base = region.start & !(lock_region_size - 1);
+
+ // The LOCKADDR size field encodes the lock region size as log2(size) - 1,
+ // per the hardware definition. For example, a 32 KiB region is encoded as 14
+ // because log2(32 KiB) = 15.
+ let lockaddr_size = lock_region_log2 - 1;
+
+ let io = &*self.iomem;
+
+ // The LOCKADDR base field stores address bits 63:12, so remove the low 12 bits
+ // before passing this value to the register macro helper.
+ // These bits are guaranteed to be zero anyway because of the minimum
+ // size of the locked region.
+ let lockaddr_base_field = lockaddr_base >> 12;
+ let lockaddr_val = LOCKADDR::zeroed()
+ .try_with_size(lockaddr_size)?
+ .try_with_base(lockaddr_base_field)?
+ .into_raw();
+
+ io.write(
+ LOCKADDR_LO::try_at(as_nr).ok_or(EINVAL)?,
+ LOCKADDR_LO::from_raw(lockaddr_val as u32),
+ );
+ io.write(
+ LOCKADDR_HI::try_at(as_nr).ok_or(EINVAL)?,
+ LOCKADDR_HI::from_raw((lockaddr_val >> 32) as u32),
+ );
+
+ self.as_send_cmd_and_wait(as_nr, MmuCommand::Lock)
+ }
+
+ /// Completes an atomic translation table update.
+ ///
+ /// Returns an error if the slot is invalid or if the flush command fails.
+ fn as_end_update(&mut self, as_nr: usize) -> Result {
+ self.validate_as_slot(as_nr)?;
+ self.as_send_cmd_and_wait(as_nr, MmuCommand::FlushPt)?;
+ Ok(())
+ }
+
+ /// Flushes the translation table cache for an AS slot.
+ ///
+ /// Returns an error if the slot is invalid or if the flush command fails.
+ fn as_flush(&mut self, as_nr: usize) -> Result {
+ self.validate_as_slot(as_nr)?;
+ self.as_send_cmd_and_wait(as_nr, MmuCommand::FlushPt)
+ }
+}
+
+impl<'drm> SlotOperations<MAX_AS> for AddressSpaceManager<'drm> {
+ /// VM address space data associated with a hardware slot.
+ type SlotData = Arc<VmAsData<'drm>>;
+
+ fn seat(slot_data: &Self::SlotData) -> &LockedSeat<Self, MAX_AS> {
+ &slot_data.as_seat
+ }
+
+ /// Activates a VM in a hardware slot.
+ fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) -> Result {
+ let as_config = slot_data.as_config()?;
+ self.as_enable(slot_idx, &as_config)
+ }
+
+ /// Evicts a VM from a hardware slot.
+ fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
+ self.as_flush(slot_idx)?;
+ self.as_disable(slot_idx)?;
+ Ok(())
+ }
+}
+
+impl<'drm> AsSlotManager<'drm> {
+ /// Locks a region for translation table updates if the VM has an active slot.
+ pub(super) fn start_vm_update(
+ &mut self,
+ vm_as_data: &VmAsData<'drm>,
+ region: &Range<u64>,
+ ) -> Result {
+ let seat = vm_as_data.as_seat.access(self);
+ match seat.slot() {
+ Some(slot) => {
+ let as_nr = slot as usize;
+ self.as_start_update(as_nr, region)
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Completes translation table updates and unlocks the region.
+ pub(super) fn end_vm_update(&mut self, vm_as_data: &VmAsData<'drm>) -> Result {
+ let seat = vm_as_data.as_seat.access(self);
+ match seat.slot() {
+ Some(slot) => {
+ let as_nr = slot as usize;
+ self.as_end_update(as_nr)
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Flushes the translation table cache if the VM has an active slot.
+ pub(super) fn flush_vm(&mut self, vm_as_data: &VmAsData<'drm>) -> Result {
+ let seat = vm_as_data.as_seat.access(self);
+ match seat.slot() {
+ Some(slot) => {
+ let as_nr = slot as usize;
+ self.as_flush(as_nr)
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Activates a VM by assigning it to a hardware slot.
+ pub(super) fn activate_vm(&mut self, vm_as_data: ArcBorrow<'_, VmAsData<'drm>>) -> Result {
+ self.activate(vm_as_data.into())
+ }
+
+ /// Deactivates a VM by evicting it from its hardware slot.
+ pub(super) fn deactivate_vm(&mut self, vm_as_data: &VmAsData<'drm>) -> Result {
+ self.evict(&vm_as_data.as_seat)
+ }
+}
diff --git a/drivers/gpu/drm/tyr/regs.rs b/drivers/gpu/drm/tyr/regs.rs
index 831357a8ef87..a62724378ced 100644
--- a/drivers/gpu/drm/tyr/regs.rs
+++ b/drivers/gpu/drm/tyr/regs.rs
@@ -25,7 +25,7 @@
//
// Nevertheless, it is useful to have most of them defined, like the C driver
// does.
-#![allow(dead_code)]
+#![expect(dead_code)]
/// Combine two 32-bit values into a single 64-bit value.
pub(crate) fn join_u64(lo: u32, hi: u32) -> u64 {
@@ -45,6 +45,8 @@ pub(crate) fn read_u64_no_tearing(lo_read: impl Fn() -> u32, hi_read: impl Fn()
}
}
+pub(crate) use mmu_control::mmu_as_control::MAX_AS;
+
/// These registers correspond to the GPU_CONTROL register page.
/// They are involved in GPU configuration and control.
pub(crate) mod gpu_control {
@@ -965,6 +967,8 @@ pub(crate) mod mmu_as_control {
register, //
};
+ use pin_init::Zeroable;
+
/// Maximum number of hardware address space slots.
/// The actual number of slots available is usually lower.
pub(crate) const MAX_AS: usize = 16;
@@ -1158,7 +1162,136 @@ fn from(val: MMU_MEMATTR_STAGE1) -> Self {
pub(crate) MEMATTR_HI(u32)[MAX_AS, stride = STRIDE] @ 0x240c {
31:0 value;
}
+ }
+
+ impl MEMATTR {
+ /// Outer cache-policy nibble indicating device memory.
+ const ARM_MAIR_DEVICE_MEMORY: u8 = 0x0;
+
+ /// In the ARM Architecture Reference Manual, the MAIR encoding for Normal memory
+ /// uses the format `0bxxRW` where:
+ /// - `W` (bit 0) = Write-Allocate policy
+ /// - `R` (bit 1) = Read-Allocate policy
+ /// E.g., `0b0011` would allow both read and write allocation on a cache miss.
+ ///
+ /// ARM MAIR Write-Allocate bit (bit 0 of a cache policy nibble).
+ const ARM_MAIR_WRITE_ALLOCATE: u8 = 0x1;
+ /// ARM MAIR Read-Allocate bit (bit 1 of a cache policy nibble).
+ const ARM_MAIR_READ_ALLOCATE: u8 = 0x2;
+
+ /// Write-back policy bit. For cacheable encodings, it is necessary but not
+ /// sufficient to set bit 2 of the cache policy nibble. Bit 2 does not
+ /// definitively determine write back because bit 2 is also set in `0b0100`
+ /// which encodes Normal non-cacheable memory.
+ const ARM_MAIR_WRITE_BACK_BIT: u8 = 0x4;
+
+ /// Complete cache-policy nibble encoding for Normal Non-cacheable memory.
+ const ARM_MAIR_NON_CACHEABLE: u8 = 0x4;
+
+ /// Mask for the inner cache policy nibble in MAIR attribute bytes.
+ const ARM_MAIR_INNER_MASK: u8 = 0x0f;
+
+ /// Check if a MAIR attribute byte represents device memory.
+ ///
+ /// Device memory (memory-mapped I/O, registers) cannot be cached because
+ /// reading and writing to this memory may have side effects.
+ fn is_device_memory(mair_attr: u8) -> bool {
+ // In AArch64 MAIR, outer nibble only is 0 for device memory.
+ (mair_attr >> 4) == Self::ARM_MAIR_DEVICE_MEMORY
+ }
+
+ /// Check if normal memory is fully write-back cacheable.
+ ///
+ /// ARM MAIR has two cache policy levels (outer [7:4] and inner [3:0]).
+ /// For memory to be truly write-back, BOTH levels must have the write-back bit set.
+ /// If only one level is write-back, treat it as non-cacheable for GPU purposes.
+ fn is_writeback_cacheable(mair_attr: u8) -> bool {
+ let outer = mair_attr >> 4;
+ let inner = mair_attr & Self::ARM_MAIR_INNER_MASK;
+
+ outer != Self::ARM_MAIR_NON_CACHEABLE
+ && inner != Self::ARM_MAIR_NON_CACHEABLE
+ && (outer & Self::ARM_MAIR_WRITE_BACK_BIT) != 0
+ && (inner & Self::ARM_MAIR_WRITE_BACK_BIT) != 0
+ }
+
+ // Helper to encode a MEMATTR attribute from its individual fields.
+ fn encode_attribute(
+ alloc_w: bool,
+ alloc_r: bool,
+ alloc_sel: AllocPolicySelect,
+ coherency: Coherency,
+ memory_type: MemoryType,
+ ) -> MMU_MEMATTR_STAGE1 {
+ MMU_MEMATTR_STAGE1::zeroed()
+ .with_alloc_w(alloc_w)
+ .with_alloc_r(alloc_r)
+ .with_alloc_sel(alloc_sel)
+ .with_coherency(coherency)
+ .with_memory_type(memory_type)
+ }
+
+ /// Convert one MAIR attribute byte into a MEMATTR attribute.
+ // TODO: Add a `coherent` parameter like panthor's mair_to_memattr().
+ // For now, assume a non-coherent system and always encode write-back
+ // memory with MidgardInnerDomain coherency.
+ fn attribute_from_mair(mair_attr: u8) -> MMU_MEMATTR_STAGE1 {
+ // Device memory or non-write-back normal memory
+ if Self::is_device_memory(mair_attr) || !Self::is_writeback_cacheable(mair_attr) {
+ return Self::encode_attribute(
+ false,
+ false,
+ AllocPolicySelect::Alloc,
+ Coherency::MidgardInnerDomain,
+ MemoryType::NonCacheable,
+ );
+ }
+
+ // Write-back cacheable normal memory
+ let inner: u8 = mair_attr & Self::ARM_MAIR_INNER_MASK;
+ Self::encode_attribute(
+ (inner & Self::ARM_MAIR_WRITE_ALLOCATE) != 0,
+ (inner & Self::ARM_MAIR_READ_ALLOCATE) != 0,
+ AllocPolicySelect::Alloc,
+ Coherency::MidgardInnerDomain,
+ MemoryType::WriteBack,
+ )
+ }
+
+ /// Write one converted MAIR attribute into a corresponding MEMATTR slot.
+ fn with_encoded_attribute(self, index: usize, attr: MMU_MEMATTR_STAGE1) -> Self {
+ debug_assert!(index < 8);
+
+ let shift = index * 8;
+ let mask = !(0xffu64 << shift);
+ let raw = (self.into_raw() & mask) | ((u64::from(attr.into_raw())) << shift);
+
+ Self::from_raw(raw)
+ }
+
+ /// Convert an AArch64 MAIR value into the GPU MEMATTR register encoding.
+ ///
+ /// Both MAIR and MEMATTR are 64-bit values with eight 8-bit memory
+ /// attribute entries, but the bits do not map directly. The GPU MEMATTR encoding
+ /// is less detailed than the MAIR encoding, so MAIR is converted to MEMATTR
+ /// conservatively as follows:
+ ///
+ /// 1. Device memory, or Normal Memory that is not write-back cacheable, is encoded
+ /// as GPU `NonCacheable`
+ ///
+ /// 2. Normal memory that is write-back cacheable is encoded as GPU `WriteBack`,
+ /// and the inner allocation hints are preserved.
+ pub(crate) fn from_mair(mair: u64) -> Self {
+ mair.to_le_bytes()
+ .into_iter()
+ .enumerate()
+ .fold(Self::zeroed(), |acc, (i, attr)| {
+ acc.with_encoded_attribute(i, Self::attribute_from_mair(attr))
+ })
+ }
+ }
+ register! {
/// Lock region address for each address space.
pub(crate) LOCKADDR(u64)[MAX_AS, stride = STRIDE] @ 0x2410 {
/// Lock region size.
diff --git a/drivers/gpu/drm/tyr/slot.rs b/drivers/gpu/drm/tyr/slot.rs
index 845846e07ee5..d194ead53f71 100644
--- a/drivers/gpu/drm/tyr/slot.rs
+++ b/drivers/gpu/drm/tyr/slot.rs
@@ -20,7 +20,6 @@
//!
//! [SlotOperations]: crate::slot::SlotOperations
//! [SlotManager]: crate::slot::SlotManager
-#![expect(dead_code)]
use core::{
mem,
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index 7c9a8063b3b9..79045d0135a8 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -11,6 +11,7 @@
mod file;
mod gem;
mod gpu;
+mod mmu;
mod regs;
mod slot;
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (2 preceding siblings ...)
2026-07-28 18:39 ` [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
2026-07-28 18:54 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 5/7] drm/tyr: add a kernel buffer object Deborah Brouwer
` (2 subsequent siblings)
6 siblings, 1 reply; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
From: Boris Brezillon <boris.brezillon@collabora.com>
Add GPU virtual address space management using the DRM GPUVM framework.
Each virtual memory (VM) space is backed by ARM64 LPAE Stage 1 page tables
and can be mapped into hardware address space (AS) slots for GPU execution.
The implementation provides memory isolation and virtual address
allocation. VMs support mapping GEM buffer objects with configurable
protection flags (readonly, noexec, uncached) and handle both 4KB and 2MB
page sizes. A new_dummy_object() helper is provided to create a dummy GEM
object for use as a GPUVM root.
The vm module integrates with the MMU for address space activation and
provides map/unmap/remap operations with page table synchronization.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Daniel Almeida <daniel.almeida@collabora.com>
Signed-off-by: Daniel Almeida <daniel.almeida@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/Kconfig | 1 +
drivers/gpu/drm/tyr/driver.rs | 4 +-
drivers/gpu/drm/tyr/gem.rs | 26 +-
drivers/gpu/drm/tyr/mmu.rs | 1 -
drivers/gpu/drm/tyr/tyr.rs | 1 +
drivers/gpu/drm/tyr/vm.rs | 950 ++++++++++++++++++++++++++++++++++++++++++
6 files changed, 978 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/tyr/Kconfig b/drivers/gpu/drm/tyr/Kconfig
index 61a2fd6f961a..79ea4bb214de 100644
--- a/drivers/gpu/drm/tyr/Kconfig
+++ b/drivers/gpu/drm/tyr/Kconfig
@@ -12,6 +12,7 @@ config DRM_TYR
default n
select IOMMU_IO_PGTABLE_LPAE
select RUST_DRM_GEM_SHMEM_HELPER
+ select RUST_DRM_GPUVM
help
Rust DRM driver for ARM Mali CSF-based GPUs.
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index 92c9d98e3aab..f487b3996293 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -37,7 +37,7 @@
use crate::{
file::TyrDrmFileData,
- gem::BoData,
+ gem::Bo,
gpu,
gpu::GpuInfo,
mmu::Mmu,
@@ -194,7 +194,7 @@ impl drm::Driver for TyrDrmDriver {
type Data = ();
type RegistrationData<'drm> = TyrDrmRegistrationData<'drm>;
type File = TyrDrmFileData;
- type Object = drm::gem::shmem::Object<BoData>;
+ type Object = Bo;
type ParentDevice<Ctx: DeviceContext> = platform::Device<Ctx>;
const INFO: drm::DriverInfo = INFO;
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index 1640a161754b..c28be61a01bb 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -5,8 +5,12 @@
//! DRM's GEM subsystem with shmem backing.
use kernel::{
- drm::gem,
- prelude::*, //
+ drm::gem::{
+ self,
+ shmem, //
+ },
+ prelude::*,
+ sync::aref::ARef, //
};
use crate::driver::{
@@ -34,3 +38,21 @@ fn new(_dev: &TyrDrmDevice, _size: usize, args: BoCreateArgs) -> impl PinInit<Se
try_pin_init!(Self { flags: args.flags })
}
}
+
+/// Type alias for Tyr GEM buffer objects.
+pub(crate) type Bo = gem::shmem::Object<BoData>;
+
+/// Creates a dummy GEM object to serve as the root of a GPUVM.
+pub(crate) fn new_dummy_object(ddev: &TyrDrmDevice) -> Result<ARef<Bo>> {
+ let bo = Bo::new(
+ ddev,
+ 4096,
+ shmem::ObjectConfig {
+ map_wc: true,
+ parent_resv_obj: None,
+ },
+ BoCreateArgs { flags: 0 },
+ )?;
+
+ Ok(bo)
+}
diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
index 83e022f9168b..5e88c9e6a4cd 100644
--- a/drivers/gpu/drm/tyr/mmu.rs
+++ b/drivers/gpu/drm/tyr/mmu.rs
@@ -10,7 +10,6 @@
//! The [`SlotManager`] manages the assignment of virtual address spaces to hardware address-space
//! (AS) slots. MMU commands such as updates and flushes are carried out by the
//! [`AddressSpaceManager`] which actually writes to the MMU registers.
-#![expect(dead_code)]
use core::ops::Range;
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index 79045d0135a8..92f6885cdaae 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -14,6 +14,7 @@
mod mmu;
mod regs;
mod slot;
+mod vm;
kernel::module_platform_driver! {
type: TyrPlatformDriver,
diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
new file mode 100644
index 000000000000..53a17dd8d949
--- /dev/null
+++ b/drivers/gpu/drm/tyr/vm.rs
@@ -0,0 +1,950 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! GPU virtual memory management using the DRM GPUVM framework.
+//!
+//! This module manages GPU virtual address spaces, providing memory isolation and
+//! the illusion of owning the entire virtual address (VA) range, similar to CPU virtual memory.
+//! Each virtual memory (VM) area is backed by ARM64 LPAE Stage 1 page tables and can be
+//! mapped into hardware address space (AS) slots for GPU execution.
+#![expect(dead_code)]
+
+use core::marker::PhantomData;
+use core::ops::Range;
+
+use kernel::{
+ device::{
+ Bound,
+ Device, //
+ },
+ drm::{
+ gem::BaseObject,
+ gpuvm::{
+ DriverGpuVm,
+ GpuVaAlloc,
+ GpuVm,
+ GpuVmBo,
+ OpMap,
+ OpMapRequest,
+ OpMapped,
+ OpRemap,
+ OpRemapped,
+ OpUnmap,
+ OpUnmapped,
+ UniqueRefGpuVm, //
+ }, //
+ },
+ fmt,
+ impl_flags,
+ iommu::pgtable::{
+ prot,
+ IoPageTable,
+ ARM64LPAES1, //
+ },
+ new_mutex,
+ prelude::*,
+ sizes::{
+ SZ_1G,
+ SZ_2M,
+ SZ_4K, //
+ },
+ sync::{
+ aref::ARef,
+ Arc,
+ ArcBorrow,
+ Mutex, //
+ },
+ uapi, //
+};
+
+use crate::{
+ driver::{
+ TyrDrmDevice,
+ TyrDrmDriver, //
+ },
+ gem,
+ gem::Bo,
+ gpu::GpuInfo,
+ mmu::{
+ address_space::VmAsData,
+ Mmu, //
+ },
+ regs::gpu_control::MMU_FEATURES,
+};
+
+impl_flags!(
+ /// Flags controlling virtual memory mapping behavior.
+ ///
+ /// These flags control access permissions and caching behavior for GPU virtual
+ /// memory mappings.
+ #[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
+ pub(crate) struct VmMapFlags(u32);
+
+ /// Individual flags that can be combined in [`VmMapFlags`].
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ pub(crate) enum VmFlag {
+ /// Map as read-only.
+ Readonly = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY as u32,
+ /// Map as non-executable.
+ Noexec = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC as u32,
+ /// Map as uncached.
+ Uncached = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED as u32,
+ }
+);
+
+impl VmMapFlags {
+ /// Convert the flags to `pgtable::prot`.
+ fn to_prot(self) -> u32 {
+ let mut prot = 0;
+
+ if self.contains(VmFlag::Readonly) {
+ prot |= prot::READ;
+ } else {
+ prot |= prot::READ | prot::WRITE;
+ }
+
+ if self.contains(VmFlag::Noexec) {
+ prot |= prot::NOEXEC;
+ }
+
+ if !self.contains(VmFlag::Uncached) {
+ prot |= prot::CACHE;
+ }
+
+ prot
+ }
+}
+
+impl fmt::Display for VmMapFlags {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut first = true;
+
+ if self.contains(VmFlag::Readonly) {
+ write!(f, "READONLY")?;
+ first = false;
+ }
+ if self.contains(VmFlag::Noexec) {
+ if !first {
+ write!(f, " | ")?;
+ }
+ write!(f, "NOEXEC")?;
+ first = false;
+ }
+
+ if self.contains(VmFlag::Uncached) {
+ if !first {
+ write!(f, " | ")?;
+ }
+ write!(f, "UNCACHED")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl TryFrom<u32> for VmMapFlags {
+ type Error = Error;
+
+ fn try_from(value: u32) -> Result<Self, Self::Error> {
+ let valid = VmFlag::Readonly as u32 | VmFlag::Noexec as u32 | VmFlag::Uncached as u32;
+
+ if value & !valid != 0 {
+ return Err(EINVAL);
+ }
+ Ok(Self(value))
+ }
+}
+
+/// Arguments for a virtual memory map operation.
+struct VmMapArgs<'drm> {
+ /// Access permissions and caching behavior for the mapping.
+ flags: VmMapFlags,
+ /// GEM buffer object registered with the GPUVM framework.
+ vm_bo: ARef<GpuVmBo<GpuVmData<'drm>>>,
+ /// Offset in bytes from the start of the buffer object.
+ bo_offset: u64,
+}
+
+/// Type of virtual memory operation.
+enum VmOpType<'drm> {
+ /// Map a GEM buffer object into the virtual address space.
+ Map(VmMapArgs<'drm>),
+ /// Unmap a region from the virtual address space.
+ Unmap,
+}
+
+/// Preallocated resources needed to execute a VM operation.
+///
+/// VM operations may require allocating new GPUVA objects to track mappings.
+/// To avoid allocation failures during the operation, preallocate the
+/// maximum number of GPUVAs that might be needed.
+struct VmOpResources<'drm> {
+ /// Preallocated GPUVA objects for remap operations.
+ ///
+ /// Partial unmap requests or map requests overlapping existing mappings
+ /// will trigger a remap call, which needs to register up to three VA
+ /// objects (one for the new mapping, and two for the previous and next
+ /// mappings).
+ preallocated_gpuvas: [Option<GpuVaAlloc<GpuVmData<'drm>>>; 3],
+}
+
+/// Request to execute a virtual memory operation.
+struct VmOpRequest<'drm> {
+ /// Request type.
+ op_type: VmOpType<'drm>,
+
+ /// Region of the virtual address space covered by this request.
+ region: Range<u64>,
+}
+
+/// Arguments for a page table map operation.
+struct PtMapArgs {
+ /// Memory protection flags describing allowed accesses for this mapping.
+ ///
+ /// This is directly derived from [`VmMapFlags`] via [`VmMapFlags::to_prot`].
+ prot: u32,
+}
+
+/// Type of page table operation.
+enum PtOpType {
+ /// Map pages into the page table.
+ Map(PtMapArgs),
+ /// Unmap pages from the page table.
+ Unmap,
+}
+
+/// Context for updating the GPU page table.
+///
+/// This context is created when beginning a page table update operation and
+/// automatically flushes changes when dropped. It ensures that the
+/// Memory Management Unit (MMU) state is properly managed and Translation
+/// Lookaside Buffer (TLB) entries are flushed.
+pub(crate) struct PtUpdateContext<'ctx, 'drm> {
+ /// Device used for DMA-mapping GEM shmem SG tables.
+ dev: &'ctx Device<Bound>,
+
+ /// Page table.
+ pt: &'ctx IoPageTable<'drm, ARM64LPAES1>,
+
+ /// MMU manager.
+ mmu: &'ctx Mmu<'drm>,
+
+ /// Reference to the address space data to pass to the MMU functions.
+ as_data: &'ctx VmAsData<'drm>,
+
+ /// Region of the virtual address space covered by this request.
+ region: Range<u64>,
+
+ /// Operation type.
+ op_type: PtOpType,
+
+ /// Preallocated resources that can be used when executing the request.
+ resources: &'ctx mut VmOpResources<'drm>,
+}
+
+impl<'ctx, 'drm> PtUpdateContext<'ctx, 'drm> {
+ /// Creates a new page table update context.
+ ///
+ /// This prepares the MMU for a page table update.
+ /// The context will automatically flush the TLB and
+ /// complete the update when dropped.
+ fn new(
+ dev: &'ctx Device<Bound>,
+ pt: &'ctx IoPageTable<'drm, ARM64LPAES1>,
+ mmu: &'ctx Mmu<'drm>,
+ as_data: &'ctx VmAsData<'drm>,
+ region: Range<u64>,
+ op_type: PtOpType,
+ resources: &'ctx mut VmOpResources<'drm>,
+ ) -> Result<PtUpdateContext<'ctx, 'drm>> {
+ mmu.start_vm_update(as_data, ®ion)?;
+
+ Ok(Self {
+ dev,
+ pt,
+ mmu,
+ as_data,
+ region,
+ op_type,
+ resources,
+ })
+ }
+
+ /// Finds one of our pre-allocated VAs.
+ fn preallocated_gpuva(&mut self) -> Result<GpuVaAlloc<GpuVmData<'drm>>> {
+ self.resources
+ .preallocated_gpuvas
+ .iter_mut()
+ .find_map(|f| f.take())
+ .ok_or(EINVAL)
+ }
+
+ /// Returns an unused GPUVA object to the preallocated pool.
+ /// If the pool is already full, the unused allocation is simply dropped.
+ fn return_preallocated_gpuva(&mut self, gpuva: GpuVaAlloc<GpuVmData<'drm>>) {
+ if let Some(slot) = self
+ .resources
+ .preallocated_gpuvas
+ .iter_mut()
+ .find(|slot| slot.is_none())
+ {
+ *slot = Some(gpuva);
+ }
+ }
+}
+
+impl Drop for PtUpdateContext<'_, '_> {
+ fn drop(&mut self) {
+ if let Err(e) = self.mmu.end_vm_update(self.as_data) {
+ dev_err!(self.dev, "Failed to end VM update {:?}", e);
+ }
+
+ if let Err(e) = self.mmu.flush_vm(self.as_data) {
+ dev_err!(self.dev, "Failed to flush VM {:?}", e);
+ }
+ }
+}
+
+/// Driver implementation for the GPUVM framework.
+///
+/// Implements [`DriverGpuVm`] to provide VM operation callbacks (map, unmap, remap)
+/// and associated types for buffer objects, virtual addresses, and contexts.
+pub(crate) struct GpuVmData<'drm> {
+ _phantom: PhantomData<&'drm ()>,
+}
+
+/// GPU virtual address space.
+///
+/// Each VM can be mapped into a hardware address space slot.
+#[pin_data]
+pub(crate) struct Vm<'drm> {
+ /// Data referenced by an AS when the VM is active
+ as_data: Arc<VmAsData<'drm>>,
+ /// MMU manager.
+ mmu: Arc<Mmu<'drm>>,
+ /// Parent device used for DMA mapping and page-table operations.
+ dev: &'drm Device<Bound>,
+ /// DRM GPUVM core for managing virtual address space.
+ #[pin]
+ gpuvm_unique: Mutex<UniqueRefGpuVm<GpuVmData<'drm>>>,
+ /// Non-core part of the GPUVM. Can be used for stuff that doesn't modify the
+ /// internal mapping tree, like GpuVm::obtain()
+ gpuvm: ARef<GpuVm<GpuVmData<'drm>>>,
+ /// VA range for this VM.
+ va_range: Range<u64>,
+}
+
+impl<'drm> Vm<'drm> {
+ /// Creates a new GPU virtual address space.
+ ///
+ /// The VM is initialized with a page table configured according to the GPU's
+ /// address translation capabilities and registered with the GPUVM framework.
+ pub(crate) fn new(
+ dev: &'drm Device<Bound>,
+ ddev: &TyrDrmDevice,
+ mmu: ArcBorrow<'_, Mmu<'drm>>,
+ gpu_info: &GpuInfo,
+ ) -> Result<Arc<Vm<'drm>>> {
+ let mmu_features = MMU_FEATURES::from_raw(gpu_info.mmu_features);
+ let va_bits = mmu_features.va_bits().get();
+ let pa_bits = mmu_features.pa_bits().get();
+
+ let range = 0..(1u64 << va_bits);
+ let reserve_range = 0..0u64;
+
+ // dummy_obj is used to initialize the GPUVM tree.
+ let dummy_obj = gem::new_dummy_object(ddev).inspect_err(|e| {
+ dev_err!(dev, "Failed to create dummy GEM object: {:?}", e);
+ })?;
+
+ let gpuvm_unique = GpuVm::new::<Error, _>(
+ c"Tyr::GpuVm",
+ ddev,
+ &*dummy_obj,
+ range.clone(),
+ reserve_range,
+ GpuVmData::<'drm> {
+ _phantom: PhantomData::<&()>,
+ },
+ )
+ .inspect_err(|e| {
+ dev_err!(dev, "Failed to create GpuVm: {:?}", e);
+ })?;
+ let gpuvm = ARef::from(&*gpuvm_unique);
+
+ let as_data = Arc::pin_init(VmAsData::new(&mmu, dev, va_bits, pa_bits), GFP_KERNEL)?;
+
+ let vm = Arc::pin_init(
+ pin_init!(Self{
+ as_data,
+ dev,
+ mmu: mmu.into(),
+ gpuvm,
+ gpuvm_unique <- new_mutex!(gpuvm_unique),
+ va_range: range,
+ }),
+ GFP_KERNEL,
+ )?;
+
+ Ok(vm)
+ }
+
+ /// Returns the parent device used by this VM for DMA mapping and page-table operations.
+ pub(crate) fn dev(&self) -> &'drm Device<Bound> {
+ self.dev
+ }
+
+ /// Activate the VM in a hardware address space slot.
+ pub(crate) fn activate(&self) -> Result {
+ self.mmu
+ .activate_vm(self.as_data.as_arc_borrow())
+ .inspect_err(|e| {
+ dev_err!(self.dev, "Failed to activate VM: {:?}", e);
+ })
+ }
+
+ /// Deactivate the VM by evicting it from its address space slot.
+ fn deactivate(&self) -> Result {
+ self.mmu.deactivate_vm(&self.as_data).inspect_err(|e| {
+ dev_err!(self.dev, "Failed to deactivate VM: {:?}", e);
+ })
+ }
+
+ /// Kills the VM by deactivating it and unmapping all regions.
+ pub(crate) fn kill(&self) {
+ // TODO: Turn the VM into a state where it can't be used.
+ let _ = self.deactivate();
+ let _ = self
+ .unmap_range(self.va_range.start, self.va_range.end - self.va_range.start)
+ .inspect_err(|e| {
+ dev_err!(self.dev, "Failed to unmap range during deactivate: {:?}", e);
+ });
+ }
+
+ /// Executes a virtual memory operation.
+ ///
+ /// This handles both map and unmap operations by coordinating between the
+ /// GPUVM framework and the hardware page table.
+ fn exec_op<'a>(
+ &self,
+ gpuvm_unique: &mut UniqueRefGpuVm<GpuVmData<'drm>>,
+ req: VmOpRequest<'drm>,
+ resources: &'a mut VmOpResources<'drm>,
+ ) -> Result {
+ let pt = &self.as_data.page_table;
+
+ match req.op_type {
+ VmOpType::Map(args) => {
+ let mut pt_upd = PtUpdateContext::new(
+ self.dev,
+ pt,
+ &self.mmu,
+ &self.as_data,
+ req.region,
+ PtOpType::Map(PtMapArgs {
+ prot: args.flags.to_prot(),
+ }),
+ resources,
+ )?;
+
+ gpuvm_unique.sm_map(OpMapRequest {
+ addr: pt_upd.region.start,
+ range: pt_upd.region.end - pt_upd.region.start,
+ gem_offset: args.bo_offset,
+ vm_bo: &args.vm_bo,
+ context: &mut pt_upd,
+ })
+ //PtUpdateContext drops here flushing the page table
+ }
+ VmOpType::Unmap => {
+ let mut pt_upd = PtUpdateContext::new(
+ self.dev,
+ pt,
+ &self.mmu,
+ &self.as_data,
+ req.region,
+ PtOpType::Unmap,
+ resources,
+ )?;
+
+ gpuvm_unique.sm_unmap(
+ pt_upd.region.start,
+ pt_upd.region.end - pt_upd.region.start,
+ &mut pt_upd,
+ )
+ //PtUpdateContext drops here flushing the page table
+ }
+ }
+ }
+
+ /// Maps a GEM buffer object range into the VM at the specified virtual address.
+ ///
+ /// This creates a mapping from GPU virtual address `va` to the physical pages
+ /// backing the GEM object, starting at `bo_offset` bytes into the object and
+ /// spanning `map_size` bytes. The mapping respects the access permissions and
+ /// caching behavior specified in `flags`.
+ pub(crate) fn map_bo_range(
+ &self,
+ bo: &Bo,
+ bo_offset: u64,
+ map_size: u64,
+ va: u64,
+ flags: VmMapFlags,
+ ) -> Result {
+ if map_size == 0
+ || va % SZ_4K as u64 != 0
+ || bo_offset % SZ_4K as u64 != 0
+ || map_size % SZ_4K as u64 != 0
+ {
+ return Err(EINVAL);
+ }
+
+ let bo_size = u64::try_from(bo.size()).map_err(|_| EOVERFLOW)?;
+ let bo_end = bo_offset.checked_add(map_size).ok_or(EINVAL)?;
+
+ if bo_end > bo_size {
+ dev_err!(
+ self.dev,
+ "BO mapping range {:#x}..{:#x} exceeds BO size {:#x}",
+ bo_offset,
+ bo_end,
+ bo_size
+ );
+ return Err(EINVAL);
+ }
+
+ let va_end: u64 = va.checked_add(map_size).ok_or(EINVAL)?;
+
+ let req = VmOpRequest {
+ op_type: VmOpType::Map(VmMapArgs {
+ vm_bo: self.gpuvm.obtain(bo, ())?,
+ flags,
+ bo_offset,
+ }),
+ region: va..va_end,
+ };
+ let mut resources = VmOpResources {
+ preallocated_gpuvas: [
+ Some(GpuVaAlloc::<GpuVmData<'drm>>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData<'drm>>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData<'drm>>::new(GFP_KERNEL)?),
+ ],
+ };
+ let result = {
+ let mut gpuvm_unique = self.gpuvm_unique.lock();
+ self.exec_op(gpuvm_unique.as_mut().get_mut(), req, &mut resources)
+ };
+ // We flush the defer cleanup list now. Things will be different in
+ // the asynchronous VM_BIND path, where we want the cleanup to
+ // happen outside the DMA signalling path.
+ self.gpuvm.deferred_cleanup();
+ result
+ }
+
+ /// Unmaps a virtual address range from the VM.
+ ///
+ /// This removes any existing mappings in the specified range, freeing the
+ /// virtual address space for reuse.
+ pub(crate) fn unmap_range(&self, va: u64, size: u64) -> Result {
+ if size == 0 || va % SZ_4K as u64 != 0 || size % SZ_4K as u64 != 0 {
+ return Err(EINVAL);
+ }
+
+ let end = va.checked_add(size).ok_or(EINVAL)?;
+
+ if va < self.va_range.start || end > self.va_range.end {
+ dev_err!(
+ self.dev,
+ "Unmap range {:#x}..{:#x} exceeds VM range {:#x}..{:#x}",
+ va,
+ end,
+ self.va_range.start,
+ self.va_range.end
+ );
+ return Err(EINVAL);
+ }
+
+ let req = VmOpRequest {
+ op_type: VmOpType::Unmap,
+ region: va..end,
+ };
+
+ let full_vm = va == self.va_range.start && end == self.va_range.end;
+
+ let mut resources = VmOpResources {
+ preallocated_gpuvas: if full_vm {
+ // Unmapping the entire VM cannot split an existing mapping,
+ // so no GPUVA objects are needed for remap operations.
+ [None, None, None]
+ } else {
+ [
+ Some(GpuVaAlloc::<GpuVmData<'drm>>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData<'drm>>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData<'drm>>::new(GFP_KERNEL)?),
+ ]
+ },
+ };
+ let result = {
+ let mut gpuvm_unique = self.gpuvm_unique.lock();
+ self.exec_op(gpuvm_unique.as_mut().get_mut(), req, &mut resources)
+ };
+ // We flush the defer cleanup list now. Things will be different in
+ // the asynchronous VM_BIND path, where we want the cleanup to
+ // happen outside the DMA signalling path.
+ self.gpuvm.deferred_cleanup();
+ result
+ }
+}
+
+impl<'drm> DriverGpuVm for GpuVmData<'drm> {
+ type Driver = TyrDrmDriver;
+ type Object = Bo;
+ type VmBoData = ();
+ type VaData = ();
+ type SmContext<'ctx>
+ = PtUpdateContext<'ctx, 'drm>
+ where
+ Self: 'ctx;
+
+ /// Create a new mapping.
+ fn sm_step_map<'op>(
+ &mut self,
+ op: OpMap<'op, Self>,
+ context: &mut Self::SmContext<'_>,
+ ) -> Result<OpMapped<'op, Self>, Error> {
+ let start_iova = op.addr();
+ let mut iova = start_iova;
+ let mut bytes_left_to_map = op.length();
+ let mut gem_offset = op.gem_offset();
+
+ // Make sure that the end of the requested GEM range doesn't run past the
+ // end of the GEM buffer itself.
+ let gem_range_end = op.gem_offset().checked_add(op.length()).ok_or(EINVAL)?;
+
+ if gem_range_end > op.obj().size() as u64 {
+ dev_err!(
+ context.dev,
+ "Requested GEM range ends at {} which is beyond the GEM buffer size {}",
+ gem_range_end,
+ op.obj().size()
+ );
+ return Err(EINVAL);
+ }
+
+ let sgt = op.obj().sg_table(context.dev).inspect_err(|e| {
+ dev_err!(context.dev, "Failed to get sg_table: {:?}", e);
+ })?;
+ let prot = match &context.op_type {
+ PtOpType::Map(args) => args.prot,
+ _ => {
+ return Err(EINVAL);
+ }
+ };
+
+ for sgt_entry in sgt.iter() {
+ // Expressly convert to u64 to work with arm 32-bit builds.
+ #[allow(clippy::useless_conversion)]
+ let mut paddr = u64::from(sgt_entry.dma_address());
+ #[allow(clippy::useless_conversion)]
+ let mut sgt_entry_length = u64::from(sgt_entry.dma_len());
+
+ if bytes_left_to_map == 0 {
+ break;
+ }
+
+ if gem_offset > 0 {
+ // Skip the entire SGT entry if the gem_offset exceeds its length.
+ let skip = u64::min(sgt_entry_length, gem_offset);
+ paddr += skip;
+ sgt_entry_length -= skip;
+ gem_offset -= skip;
+ }
+
+ if sgt_entry_length == 0 {
+ continue;
+ }
+
+ let len = u64::min(sgt_entry_length, bytes_left_to_map);
+
+ let segment_mapped = match pt_map(context.dev, context.pt, iova, paddr, len, prot) {
+ Ok(segment_mapped) => segment_mapped,
+ Err(e) => {
+ // clean up any successful mappings from previous SGT entries.
+ let total_mapped = iova - start_iova;
+ if total_mapped > 0 {
+ let _ = pt_unmap(
+ context.dev,
+ context.pt,
+ start_iova..(start_iova + total_mapped),
+ );
+ }
+ return Err(e);
+ }
+ };
+
+ bytes_left_to_map -= segment_mapped;
+ iova += segment_mapped;
+ }
+
+ if bytes_left_to_map != 0 {
+ let total_mapped = iova - start_iova;
+
+ if total_mapped > 0 {
+ let _ = pt_unmap(context.dev, context.pt, start_iova..iova);
+ }
+
+ dev_err!(
+ context.dev,
+ "SG table is too small for requested mapping: {} bytes remain",
+ bytes_left_to_map
+ );
+
+ return Err(EINVAL);
+ }
+
+ let gpuva = context.preallocated_gpuva()?;
+ let op = op.insert(gpuva, pin_init::init_zeroed());
+
+ Ok(op)
+ }
+
+ /// Indicates that an existing mapping should be removed.
+ fn sm_step_unmap<'op>(
+ &mut self,
+ op: OpUnmap<'op, Self>,
+ context: &mut Self::SmContext<'_>,
+ ) -> Result<OpUnmapped<'op, Self>, Error> {
+ let start_iova = op.va().addr();
+ let length = op.va().length();
+
+ let region = start_iova..(start_iova + length);
+ pt_unmap(context.dev, context.pt, region.clone()).inspect_err(|e| {
+ dev_err!(
+ context.dev,
+ "Failed to unmap region {:#x}..{:#x}: {:?}",
+ region.start,
+ region.end,
+ e
+ );
+ })?;
+
+ let (op_unmapped, _va_removed) = op.remove();
+
+ Ok(op_unmapped)
+ }
+
+ /// Split up an existing mapping.
+ fn sm_step_remap<'op>(
+ &mut self,
+ op: OpRemap<'op, Self>,
+ context: &mut Self::SmContext<'_>,
+ ) -> Result<OpRemapped<'op, Self>, Error> {
+ let unmap_start = if let Some(prev) = op.prev() {
+ prev.addr() + prev.length()
+ } else {
+ op.va_to_unmap().addr()
+ };
+
+ let unmap_end = if let Some(next) = op.next() {
+ next.addr()
+ } else {
+ op.va_to_unmap().addr() + op.va_to_unmap().length()
+ };
+
+ let unmap_length = unmap_end - unmap_start;
+
+ if unmap_length > 0 {
+ let region = unmap_start..(unmap_start + unmap_length);
+ pt_unmap(context.dev, context.pt, region.clone()).inspect_err(|e| {
+ dev_err!(
+ context.dev,
+ "Failed to unmap remap region {:#x}..{:#x}: {:?}",
+ region.start,
+ region.end,
+ e
+ );
+ })?;
+ }
+
+ let prev_va = context.preallocated_gpuva()?;
+ let next_va = context.preallocated_gpuva()?;
+
+ let (op_remapped, remap_ret) = op.remap(
+ [prev_va, next_va],
+ pin_init::init_zeroed(),
+ pin_init::init_zeroed(),
+ );
+
+ if let Some(unused_va) = remap_ret.unused_va {
+ context.return_preallocated_gpuva(unused_va);
+ }
+
+ Ok(op_remapped)
+ }
+}
+
+/// This function selects the largest supported block size (currently 4KB or 2MB)
+/// that can be used for a mapping at the given address and size, respecting alignment constraints.
+///
+/// We can map multiple pages at once but we can't exceed the size of the
+/// table entry itself. So, if mapping 4KB pages, figure out how many pages
+/// can be mapped before we hit the 2MB boundary. Or, if mapping 2MB pages,
+/// figure out how many pages can be mapped before hitting the 1GB boundary
+/// Returns the page size (4KB or 2MB) and the number of pages that can be mapped at that size.
+fn get_pgsize(addr: u64, size: u64) -> (u64, u64) {
+ // Get the distance to the next boundary of 2MB block
+ let blk_offset_2m = addr.wrapping_neg() % (SZ_2M as u64);
+
+ // Use 4K blocks if the address is not 2MB aligned, or we have less than 2MB to map
+ if blk_offset_2m != 0 || size < SZ_2M as u64 {
+ let pgcount = if blk_offset_2m == 0 {
+ size / SZ_4K as u64
+ } else {
+ u64::min(blk_offset_2m, size) / SZ_4K as u64
+ };
+ return (SZ_4K as u64, pgcount);
+ }
+
+ let blk_offset_1g = addr.wrapping_neg() % (SZ_1G as u64);
+ let blk_offset = if blk_offset_1g == 0 {
+ SZ_1G as u64
+ } else {
+ blk_offset_1g
+ };
+ let pgcount = u64::min(blk_offset, size) / SZ_2M as u64;
+
+ (SZ_2M as u64, pgcount)
+}
+
+/// Maps a physical address range into the page table at the specified virtual address.
+///
+/// This function maps `len` bytes of physical memory starting at `paddr` to the
+/// virtual address `iova`, using the protection flags specified in `prot`. It
+/// automatically selects optimal page sizes to minimize page table overhead.
+///
+/// If the mapping fails partway through, all successfully mapped pages are
+/// unmapped before returning an error.
+///
+/// Returns the number of bytes successfully mapped.
+fn pt_map(
+ dev: &Device,
+ pt: &IoPageTable<'_, ARM64LPAES1>,
+ iova: u64,
+ paddr: u64,
+ len: u64,
+ prot: u32,
+) -> Result<u64> {
+ let mut segment_mapped = 0u64;
+ while segment_mapped < len {
+ let remaining = len - segment_mapped;
+ let curr_iova = iova + segment_mapped;
+ let curr_paddr = paddr + segment_mapped;
+
+ let (pgsize, pgcount) = get_pgsize(curr_iova | curr_paddr, remaining);
+
+ // On 32-bit systems, usize is only 32 bits, so check that
+ // the iova can be converted without truncation.
+ let curr_iova = match usize::try_from(curr_iova) {
+ Ok(curr_iova) => curr_iova,
+ Err(_) => {
+ dev_err!(
+ dev,
+ "curr_iova {:#x} cannot be represented as usize (max {:#x})",
+ curr_iova,
+ usize::MAX
+ );
+
+ if segment_mapped > 0 {
+ let _ = pt_unmap(dev, pt, iova..(iova + segment_mapped));
+ }
+
+ return Err(EOVERFLOW);
+ }
+ };
+
+ // SAFETY:
+ // No other io-pgtable operation can currently access this range because Tyr holds
+ // the gpuvm_unique mutex for the entire sm_map() operation.
+ // The addresses being mapped won't overlap any existing mappings in this
+ // page table because drm_gpuvm_sm_map() checks each requested mapping and either unmaps
+ // or remaps any overlap before creating the new mapping.
+ let (mapped, result) = unsafe {
+ pt.map_pages(
+ curr_iova,
+ curr_paddr,
+ pgsize as usize,
+ pgcount as usize,
+ prot,
+ GFP_KERNEL,
+ )
+ };
+
+ if let Err(e) = result {
+ // If map_pages fails, mapped will be zero because the ARM LPAE backend
+ // only updates the mapped value after the entire request succeeds.
+ dev_err!(dev, "pt.map_pages failed at iova {:#x}: {:?}", curr_iova, e);
+ if segment_mapped > 0 {
+ let _ = pt_unmap(dev, pt, iova..(iova + segment_mapped));
+ }
+ return Err(e);
+ }
+
+ if mapped == 0 {
+ dev_err!(dev, "Failed to map any pages at iova {:#x}", curr_iova);
+ if segment_mapped > 0 {
+ let _ = pt_unmap(dev, pt, iova..(iova + segment_mapped));
+ }
+ return Err(ENOMEM);
+ }
+
+ segment_mapped += mapped as u64;
+ }
+
+ Ok(segment_mapped)
+}
+
+/// Unmaps a virtual address range from the page table.
+///
+/// This function removes all page table entries in the specified range,
+/// automatically handling different page sizes that may be present.
+fn pt_unmap(dev: &Device, pt: &IoPageTable<'_, ARM64LPAES1>, range: Range<u64>) -> Result {
+ let mut iova = range.start;
+ let mut bytes_left_to_unmap = range.end - range.start;
+
+ while bytes_left_to_unmap > 0 {
+ // It is fine to use just the iova to determine the page size
+ // because if the actual mapping was represented with smaller page sizes,
+ // (e.g. because the physical address was not 2MiB aligned)
+ // the ARM LPAE backend will notice and handle the lower-level table correctly.
+ let (pgsize, pgcount) = get_pgsize(iova, bytes_left_to_unmap);
+
+ // On 32-bit systems, usize is only 32 bits, so check that
+ // the iova can be converted without truncation.
+ let iova_usize = usize::try_from(iova).map_err(|_| {
+ dev_err!(
+ dev,
+ "IOVA {:#x} cannot be represented as usize (max {:#x})",
+ iova,
+ usize::MAX
+ );
+ EOVERFLOW
+ })?;
+
+ // SAFETY:
+ // No other io-pgtable operation can currently access this range because Tyr holds
+ // the gpuvm_unique mutex for the entire sm_unmap() operation.
+ // We know that this page table has one or more consecutive mappings
+ // starting at `iova` with the total size of `pgcount * pgsize` because
+ // gpuvm callbacks provide exactly the range that was previously mapped.
+ let unmapped = unsafe { pt.unmap_pages(iova_usize, pgsize as usize, pgcount as usize) };
+
+ if unmapped == 0 {
+ dev_err!(dev, "Failed to unmap any bytes at iova {:#x}", iova_usize);
+ return Err(EINVAL);
+ }
+
+ bytes_left_to_unmap -= unmapped as u64;
+ iova += unmapped as u64;
+ }
+
+ Ok(())
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v10 5/7] drm/tyr: add a kernel buffer object
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (3 preceding siblings ...)
2026-07-28 18:39 ` [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
2026-07-28 18:54 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 6/7] drm/tyr: add parser for firmware binary Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 7/7] drm/tyr: add Microcontroller Unit (MCU) booting Deborah Brouwer
6 siblings, 1 reply; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
Introduce a buffer object type (KernelBo) for internal driver allocations
that are managed by the kernel rather than userspace.
KernelBo wraps a GEM shmem object and automatically handles GPU virtual
address space mapping during creation and unmapping on drop. This provides
a safe and convenient way for the driver to both allocate and clean up
internal buffers for kernel-managed resources.
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/gem.rs | 113 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 109 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index c28be61a01bb..69e1c75e59a5 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -4,18 +4,29 @@
//! This module provides buffer object (BO) management functionality using
//! DRM's GEM subsystem with shmem backing.
+use core::ops::Range;
+
use kernel::{
drm::gem::{
self,
shmem, //
},
prelude::*,
- sync::aref::ARef, //
+ sync::{
+ aref::ARef,
+ Arc, //
+ }, //
};
-use crate::driver::{
- TyrDrmDevice,
- TyrDrmDriver, //
+use crate::{
+ driver::{
+ TyrDrmDevice,
+ TyrDrmDriver, //
+ },
+ vm::{
+ Vm,
+ VmMapFlags, //
+ },
};
/// Tyr's DriverObject type for GEM objects.
@@ -56,3 +67,97 @@ pub(crate) fn new_dummy_object(ddev: &TyrDrmDevice) -> Result<ARef<Bo>> {
Ok(bo)
}
+
+/// Specifies how to choose a GPU virtual address for a [`KernelBo`].
+/// An automatic VA allocation strategy will be added in the future.
+pub(crate) enum KernelBoVaAlloc {
+ /// Explicit VA address specified by the caller.
+ #[expect(dead_code)]
+ Explicit(u64),
+}
+
+/// A kernel-owned buffer object with automatic GPU virtual address mapping.
+///
+/// This structure represents a buffer object that is created and managed entirely
+/// by the kernel driver, as opposed to userspace-created GEM objects. It combines
+/// a GEM object with automatic GPU virtual address (VA) space mapping and cleanup.
+///
+/// When dropped, the buffer is automatically unmapped from the GPU VA space.
+pub(crate) struct KernelBo<'drm> {
+ /// The underlying GEM buffer object.
+ bo: ARef<Bo>,
+ /// The GPU VM this buffer is mapped into.
+ vm: Arc<Vm<'drm>>,
+ /// The GPU VA range occupied by this buffer.
+ va_range: Range<u64>,
+}
+
+impl<'drm> KernelBo<'drm> {
+ /// Creates a new kernel-owned buffer object and maps it into GPU VA space.
+ ///
+ /// This function allocates a new shmem-backed GEM object and immediately maps
+ /// it into the specified GPU virtual memory space. The mapping is automatically
+ /// cleaned up when the [`KernelBo`] is dropped.
+ #[expect(dead_code)]
+ pub(crate) fn new(
+ ddev: &TyrDrmDevice,
+ vm: Arc<Vm<'drm>>,
+ size: u64,
+ va_alloc: KernelBoVaAlloc,
+ flags: VmMapFlags,
+ ) -> Result<Self> {
+ if size == 0 {
+ dev_err!(vm.dev(), "Cannot create KernelBo with size 0");
+ return Err(EINVAL);
+ }
+
+ let KernelBoVaAlloc::Explicit(va) = va_alloc;
+
+ let bo_size = usize::try_from(size).map_err(|_| EOVERFLOW)?;
+ let va_end = va.checked_add(size).ok_or(EINVAL)?;
+
+ let bo = Bo::new(
+ ddev,
+ bo_size,
+ shmem::ObjectConfig {
+ map_wc: true,
+ parent_resv_obj: None,
+ },
+ BoCreateArgs { flags: 0 },
+ )?;
+
+ vm.map_bo_range(&bo, 0, size, va, flags)?;
+
+ Ok(KernelBo {
+ bo,
+ vm,
+ va_range: va..va_end,
+ })
+ }
+
+ #[expect(dead_code)]
+ pub(crate) fn bo(&self) -> &Bo {
+ &self.bo
+ }
+}
+
+impl Drop for KernelBo<'_> {
+ fn drop(&mut self) {
+ let va = self.va_range.start;
+ let size = self.va_range.end - self.va_range.start;
+
+ if let Err(e) = self.vm.unmap_range(va, size) {
+ // If unmap_range fails, it is still safe to drop the
+ // KernelBo and its ARef to the GEM buffer object because
+ // GPUVM also holds a reference to the GEM buffer object.
+ // The physical pages won't be freed or reallocated.
+ dev_err!(
+ self.vm.dev(),
+ "Failed to unmap KernelBo range {:#x}..{:#x}: {:?}",
+ self.va_range.start,
+ self.va_range.end,
+ e
+ );
+ }
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v10 6/7] drm/tyr: add parser for firmware binary
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (4 preceding siblings ...)
2026-07-28 18:39 ` [PATCH v10 5/7] drm/tyr: add a kernel buffer object Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
2026-07-28 18:47 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 7/7] drm/tyr: add Microcontroller Unit (MCU) booting Deborah Brouwer
6 siblings, 1 reply; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
From: Daniel Almeida <daniel.almeida@collabora.com>
Add a parser for the Mali CSF GPU firmware binary format. The firmware
consists of a header followed by entries describing how to load firmware
sections into the MCU's memory.
The parser extracts section metadata including virtual address ranges,
data byte offsets within the binary, and section flags controlling
permissions and cache modes. It validates the basic firmware structure
and alignment and ignores protected-mode sections for now.
Signed-off-by: Daniel Almeida <daniel.almeida@collabora.com>
Co-developed-by: Beata Michalska <beata.michalska@arm.com>
Signed-off-by: Beata Michalska <beata.michalska@arm.com>
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/fw/parser.rs | 588 +++++++++++++++++++++++++++++++++++++++
1 file changed, 588 insertions(+)
diff --git a/drivers/gpu/drm/tyr/fw/parser.rs b/drivers/gpu/drm/tyr/fw/parser.rs
new file mode 100644
index 000000000000..c4d0ad1d7899
--- /dev/null
+++ b/drivers/gpu/drm/tyr/fw/parser.rs
@@ -0,0 +1,588 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Firmware binary parser for Mali CSF (Command Stream Frontend) GPU.
+//!
+//! This module implements a parser for the Mali GPU firmware binary format. The firmware
+//! file contains a header followed by a sequence of entries, each describing how to load
+//! firmware sections into the MCU (Microcontroller Unit) memory. The parser extracts section
+//! metadata including:
+//! - Virtual address ranges where sections should be mapped
+//! - Data ranges (byte offsets) within the firmware binary
+//! - Section flags (permissions, cache modes)
+
+use core::{
+ mem::size_of,
+ ops::Range, //
+};
+
+use kernel::{
+ bits::bit_u32,
+ device::Device,
+ prelude::*,
+ sizes::SZ_4K, //
+};
+
+use crate::{
+ fw::{
+ CacheMode,
+ SectionFlags,
+ CSF_MCU_SHARED_REGION_START, //
+ },
+ vm::{
+ VmFlag,
+ VmMapFlags, //
+ }, //
+};
+
+/// A parsed firmware section ready for loading into MCU memory.
+///
+/// Represents a single firmware section extracted from the firmware binary, containing
+/// all information needed to map the section's data into the MCU's virtual address space.
+pub(super) struct ParsedSection {
+ /// Byte offset range within the firmware binary where this section's data resides.
+ pub(super) data_range: Range<u32>,
+ /// MCU virtual address range where this section should be mapped.
+ pub(super) va: Range<u32>,
+ /// Memory protection and caching flags for the mapping.
+ pub(super) vm_map_flags: VmMapFlags,
+}
+
+/// A bare-bones `std::io::Cursor<[u8]>` clone to keep track of the current position in the
+/// firmware binary.
+///
+/// Provides methods to sequentially read primitive types and byte arrays from the firmware
+/// binary while maintaining the current read position.
+struct Cursor<'a> {
+ dev: &'a Device,
+ data: &'a [u8],
+ pos: usize,
+}
+
+impl<'a> Cursor<'a> {
+ fn new(dev: &'a Device, data: &'a [u8]) -> Self {
+ Self { dev, data, pos: 0 }
+ }
+
+ fn len(&self) -> usize {
+ self.data.len()
+ }
+
+ fn pos(&self) -> usize {
+ self.pos
+ }
+
+ /// Returns a view into the cursor's data.
+ ///
+ /// This spawns a new cursor, leaving the current cursor unchanged.
+ fn view(&self, range: Range<usize>) -> Result<Cursor<'_>> {
+ if range.start < self.pos || range.end > self.data.len() {
+ dev_err!(
+ self.dev,
+ "Invalid cursor range {:?} for data of length {}",
+ range,
+ self.data.len()
+ );
+
+ Err(EINVAL)
+ } else {
+ Ok(Self {
+ dev: self.dev,
+ data: &self.data[range],
+ pos: 0,
+ })
+ }
+ }
+
+ /// Reads a slice of bytes from the current position and advances the cursor.
+ ///
+ /// Returns an error if the read would exceed the data bounds.
+ fn read(&mut self, nbytes: usize) -> Result<&[u8]> {
+ let start = self.pos;
+ let end = start + nbytes;
+
+ if end > self.data.len() {
+ dev_err!(
+ self.dev,
+ "Invalid firmware file: read of size {} at position {} is out of bounds",
+ nbytes,
+ start,
+ );
+ return Err(EINVAL);
+ }
+
+ self.pos += nbytes;
+ Ok(&self.data[start..end])
+ }
+
+ /// Reads a little-endian `u8` from the current position and advances the cursor.
+ fn read_u8(&mut self) -> Result<u8> {
+ let bytes = self.read(size_of::<u8>())?;
+ Ok(bytes[0])
+ }
+
+ /// Reads a little-endian `u16` from the current position and advances the cursor.
+ fn read_u16(&mut self) -> Result<u16> {
+ let bytes: [u8; 2] = self
+ .read(size_of::<u16>())?
+ .try_into()
+ .map_err(|_| EINVAL)?;
+
+ Ok(u16::from_le_bytes(bytes))
+ }
+
+ /// Reads a little-endian `u32` from the current position and advances the cursor.
+ fn read_u32(&mut self) -> Result<u32> {
+ let bytes: [u8; 4] = self
+ .read(size_of::<u32>())?
+ .try_into()
+ .map_err(|_| EINVAL)?;
+
+ Ok(u32::from_le_bytes(bytes))
+ }
+
+ /// Advances the cursor position by the specified number of bytes.
+ ///
+ /// Returns an error if the advance would exceed the data bounds.
+ fn advance(&mut self, nbytes: usize) -> Result {
+ if self.pos + nbytes > self.data.len() {
+ dev_err!(
+ self.dev,
+ "Invalid firmware file: advance of size {} at position {} is out of bounds",
+ nbytes,
+ self.pos,
+ );
+ return Err(EINVAL);
+ }
+ self.pos += nbytes;
+ Ok(())
+ }
+}
+
+/// Parser for Mali CSF GPU firmware binaries.
+///
+/// Parses the firmware binary format, extracting section metadata including virtual
+/// address ranges, data offsets, and memory protection flags needed to load firmware
+/// into the MCU's memory.
+pub(super) struct FwParser<'a> {
+ cursor: Cursor<'a>,
+}
+
+impl<'a> FwParser<'a> {
+ /// Creates a new firmware parser for the given firmware binary data.
+ pub(super) fn new(dev: &'a Device, data: &'a [u8]) -> Self {
+ Self {
+ cursor: Cursor::new(dev, data),
+ }
+ }
+
+ /// Parses the firmware binary and returns a collection of parsed sections.
+ ///
+ /// This method validates the firmware header and iterates through all entries
+ /// in the binary, extracting section information needed for loading.
+ pub(super) fn parse(&mut self) -> Result<KVec<ParsedSection>> {
+ let fw_header = self.parse_fw_header()?;
+ let header_end = fw_header.size as usize;
+
+ let mut parsed_sections = KVec::new();
+ while self.cursor.pos() < header_end {
+ let entry_section = self.parse_entry(header_end)?;
+
+ if let Some(inner) = entry_section.inner {
+ parsed_sections.push(inner, GFP_KERNEL)?;
+ }
+ }
+
+ if parsed_sections.is_empty() {
+ dev_err!(self.cursor.dev, "Firmware contains no loadable sections");
+ return Err(EINVAL);
+ }
+
+ Ok(parsed_sections)
+ }
+
+ fn parse_fw_header(&mut self) -> Result<FirmwareHeader> {
+ let fw_header: FirmwareHeader = match FirmwareHeader::new(&mut self.cursor) {
+ Ok(fw_header) => fw_header,
+ Err(e) => {
+ dev_err!(self.cursor.dev, "Invalid firmware file: {}", e.to_errno());
+ return Err(e);
+ }
+ };
+
+ if fw_header.size as usize > self.cursor.len() {
+ dev_err!(self.cursor.dev, "Firmware image is truncated");
+ return Err(EINVAL);
+ }
+ Ok(fw_header)
+ }
+
+ fn parse_entry(&mut self, header_end: usize) -> Result<EntrySection> {
+ let entry_start = self.cursor.pos();
+
+ let entry_header_end = entry_start
+ .checked_add(size_of::<EntryHeader>())
+ .ok_or(EINVAL)?;
+
+ if entry_header_end > header_end {
+ dev_err!(
+ self.cursor.dev,
+ "Firmware entry header at {:#x} exceeds header region ending at {:#x}",
+ entry_start,
+ header_end
+ );
+ return Err(EINVAL);
+ }
+
+ let entry_section = EntrySection {
+ entry_hdr: EntryHeader(self.cursor.read_u32()?),
+ inner: None,
+ };
+
+ let firmware_size = self.cursor.len();
+ let entry_size = entry_section.entry_hdr.size() as usize;
+
+ if self.cursor.pos() % size_of::<u32>() != 0
+ || entry_size % size_of::<u32>() != 0
+ || entry_size < size_of::<EntryHeader>()
+ {
+ dev_err!(
+ self.cursor.dev,
+ "Firmware entry isn't 32 bit aligned, offset={:#x} size={:#x}",
+ self.cursor.pos() - size_of::<u32>(),
+ entry_size
+ );
+ return Err(EINVAL);
+ }
+
+ let entry_end = entry_start.checked_add(entry_size).ok_or(EINVAL)?;
+
+ if entry_end > header_end {
+ dev_err!(
+ self.cursor.dev,
+ "Firmware entry at {:#x} extends beyond header region ending at {:#x}",
+ entry_start,
+ header_end
+ );
+ return Err(EINVAL);
+ }
+
+ let section_hdr_size = entry_size - size_of::<EntryHeader>();
+
+ let entry_section = {
+ let mut entry_cursor = self.cursor.view(self.cursor.pos()..entry_end)?;
+
+ match entry_section.entry_hdr.entry_type() {
+ Ok(EntryType::Iface) => Ok(EntrySection {
+ entry_hdr: entry_section.entry_hdr,
+ inner: Self::parse_section_entry(&mut entry_cursor, firmware_size)?,
+ }),
+ Ok(
+ EntryType::Config
+ | EntryType::FutfTest
+ | EntryType::TraceBuffer
+ | EntryType::TimelineMetadata
+ | EntryType::BuildInfoMetadata,
+ ) => Ok(entry_section),
+
+ Err(_) => {
+ if entry_section.entry_hdr.optional() {
+ Ok(entry_section)
+ } else {
+ dev_err!(
+ self.cursor.dev,
+ "Failed to handle firmware entry type: {}",
+ entry_section.entry_hdr.entry_type_raw()
+ );
+ Err(EINVAL)
+ }
+ }
+ }
+ };
+
+ if entry_section.is_ok() {
+ self.cursor.advance(section_hdr_size)?;
+ }
+
+ entry_section
+ }
+
+ fn parse_section_entry(
+ entry_cursor: &mut Cursor<'_>,
+ firmware_size: usize,
+ ) -> Result<Option<ParsedSection>> {
+ let section_hdr: SectionHeader = SectionHeader::new(entry_cursor)?;
+
+ if section_hdr.data.end < section_hdr.data.start {
+ dev_err!(
+ entry_cursor.dev,
+ "Firmware corrupted, data.end < data.start (0x{:x} < 0x{:x})",
+ section_hdr.data.end,
+ section_hdr.data.start
+ );
+ return Err(EINVAL);
+ }
+
+ if section_hdr.data.end as usize > firmware_size {
+ dev_err!(
+ entry_cursor.dev,
+ "Firmware data range {:#x}..{:#x} exceeds firmware size {:#x}",
+ section_hdr.data.start,
+ section_hdr.data.end,
+ firmware_size,
+ );
+ return Err(EINVAL);
+ }
+
+ if section_hdr.va.start as usize % SZ_4K != 0 || section_hdr.va.end as usize % SZ_4K != 0 {
+ dev_err!(
+ entry_cursor.dev,
+ "Firmware virtual address range {:#x}..{:#x} is not page aligned",
+ section_hdr.va.start,
+ section_hdr.va.end
+ );
+ return Err(EINVAL);
+ }
+
+ if section_hdr.section_flags.prot() {
+ dev_dbg!(
+ entry_cursor.dev,
+ "Firmware protected mode entry not supported, ignoring"
+ );
+ return Ok(None);
+ }
+
+ if section_hdr.va.start == CSF_MCU_SHARED_REGION_START
+ && !section_hdr.section_flags.shared()
+ {
+ dev_err!(
+ entry_cursor.dev,
+ "Interface at 0x{:x} must be shared",
+ CSF_MCU_SHARED_REGION_START
+ );
+ return Err(EINVAL);
+ }
+
+ if section_hdr.va.is_empty() {
+ return Ok(None);
+ }
+
+ let mut vm_map_flags = VmMapFlags::empty();
+
+ if !section_hdr.section_flags.write() {
+ vm_map_flags |= VmFlag::Readonly;
+ }
+
+ if !section_hdr.section_flags.exec() {
+ vm_map_flags |= VmFlag::Noexec;
+ }
+
+ // TODO: As in Panthor, map coherent firmware sections uncached until the VM
+ // supports a coherent mapping attribute.
+ if section_hdr.section_flags.cache_mode() != CacheMode::Cached {
+ vm_map_flags |= VmFlag::Uncached;
+ }
+
+ Ok(Some(ParsedSection {
+ data_range: section_hdr.data.clone(),
+ va: section_hdr.va,
+ vm_map_flags,
+ }))
+ }
+}
+
+/// Firmware binary header containing version and size information.
+///
+/// The header is located at the beginning of the firmware binary and contains
+/// a magic value for validation, version information, and the total size of
+/// all structured headers that follow.
+#[expect(dead_code)]
+struct FirmwareHeader {
+ /// Magic value to check binary validity.
+ magic: u32,
+
+ /// Minor firmware version.
+ minor: u8,
+
+ /// Major firmware version.
+ major: u8,
+
+ /// Padding. Must be set to zero.
+ _padding1: u16,
+
+ /// Firmware version hash.
+ version_hash: u32,
+
+ /// Padding. Must be set to zero.
+ _padding2: u32,
+
+ /// Total size of all the structured data headers at beginning of firmware binary.
+ size: u32,
+}
+
+impl FirmwareHeader {
+ const FW_BINARY_MAGIC: u32 = 0xc3f13a6e;
+ const FW_BINARY_MAJOR_MAX: u8 = 0;
+
+ /// Reads and validates a firmware header from the cursor.
+ ///
+ /// Verifies the magic value, version compatibility, and padding fields.
+ fn new(cursor: &mut Cursor<'_>) -> Result<Self> {
+ let magic = cursor.read_u32()?;
+ if magic != Self::FW_BINARY_MAGIC {
+ dev_err!(cursor.dev, "Invalid firmware magic");
+ return Err(EINVAL);
+ }
+
+ let minor = cursor.read_u8()?;
+ let major = cursor.read_u8()?;
+
+ if major > Self::FW_BINARY_MAJOR_MAX {
+ dev_err!(
+ cursor.dev,
+ "Unsupported firmware binary header version {}.{} (expected {}.x)",
+ major,
+ minor,
+ Self::FW_BINARY_MAJOR_MAX
+ );
+ return Err(EINVAL);
+ }
+
+ let padding1 = cursor.read_u16()?;
+ let version_hash = cursor.read_u32()?;
+ let padding2 = cursor.read_u32()?;
+ let size = cursor.read_u32()?;
+
+ if padding1 != 0 || padding2 != 0 {
+ dev_err!(
+ cursor.dev,
+ "Invalid firmware file: header padding is not zero"
+ );
+ return Err(EINVAL);
+ }
+
+ let fw_header = Self {
+ magic,
+ minor,
+ major,
+ _padding1: padding1,
+ version_hash,
+ _padding2: padding2,
+ size,
+ };
+
+ Ok(fw_header)
+ }
+}
+
+/// Firmware section header for loading binary sections into MCU memory.
+#[derive(Debug)]
+struct SectionHeader {
+ section_flags: SectionFlags,
+ /// MCU virtual range to map this binary section to.
+ va: Range<u32>,
+ /// References the data in the FW binary.
+ data: Range<u32>,
+}
+
+impl SectionHeader {
+ /// Reads and validates a section header from the cursor.
+ ///
+ /// Parses section flags, virtual address range, and data range from the firmware binary.
+ fn new(cursor: &mut Cursor<'_>) -> Result<Self> {
+ let section_flags = SectionFlags::try_from_fw(cursor.read_u32()?)?;
+
+ let va_start = cursor.read_u32()?;
+ let va_end = cursor.read_u32()?;
+
+ let va = va_start..va_end;
+
+ if va.end < va.start {
+ dev_err!(
+ cursor.dev,
+ "Invalid firmware file: VA end precedes start at pos {}",
+ cursor.pos(),
+ );
+ return Err(EINVAL);
+ }
+
+ let data_start = cursor.read_u32()?;
+ let data_end = cursor.read_u32()?;
+ let data = data_start..data_end;
+
+ Ok(Self {
+ section_flags,
+ va,
+ data,
+ })
+ }
+}
+
+/// A firmware entry containing a header and optional parsed section data.
+///
+/// Represents a single entry in the firmware binary, which may contain loadable
+/// section data or metadata that doesn't require loading.
+struct EntrySection {
+ entry_hdr: EntryHeader,
+ inner: Option<ParsedSection>,
+}
+
+/// Header for a firmware entry, packed into a single u32.
+///
+/// The entry header encodes the entry type, size, and optional flag in a
+/// 32-bit value with the following layout:
+/// - Bits 0-7: Entry type
+/// - Bits 8-15: Size in bytes
+/// - Bit 31: Optional flag
+struct EntryHeader(u32);
+
+impl EntryHeader {
+ fn entry_type_raw(&self) -> u8 {
+ (self.0 & 0xff) as u8
+ }
+
+ fn entry_type(&self) -> Result<EntryType> {
+ let v = self.entry_type_raw();
+ EntryType::try_from(v)
+ }
+
+ fn optional(&self) -> bool {
+ self.0 & bit_u32(31) != 0
+ }
+
+ fn size(&self) -> u32 {
+ self.0 >> 8 & 0xff
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+#[repr(u8)]
+enum EntryType {
+ /// Host <-> FW interface.
+ Iface = 0,
+ /// FW config.
+ Config = 1,
+ /// Unit tests.
+ FutfTest = 2,
+ /// Trace buffer interface.
+ TraceBuffer = 3,
+ /// Timeline metadata interface.
+ TimelineMetadata = 4,
+ /// Metadata about how the FW binary was built.
+ BuildInfoMetadata = 6,
+}
+
+impl TryFrom<u8> for EntryType {
+ type Error = Error;
+
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(EntryType::Iface),
+ 1 => Ok(EntryType::Config),
+ 2 => Ok(EntryType::FutfTest),
+ 3 => Ok(EntryType::TraceBuffer),
+ 4 => Ok(EntryType::TimelineMetadata),
+ 6 => Ok(EntryType::BuildInfoMetadata),
+ _ => Err(EINVAL),
+ }
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v10 7/7] drm/tyr: add Microcontroller Unit (MCU) booting
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (5 preceding siblings ...)
2026-07-28 18:39 ` [PATCH v10 6/7] drm/tyr: add parser for firmware binary Deborah Brouwer
@ 2026-07-28 18:39 ` Deborah Brouwer
6 siblings, 0 replies; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 18:39 UTC (permalink / raw)
To: Daniel Almeida, Alice Ryhl, Danilo Krummrich, David Airlie,
Simona Vetter, Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan
Cc: dri-devel, linux-kernel, rust-for-linux, Deborah Brouwer,
laura.nao, samitolvanen, lyude, beata.michalska, boris.brezillon,
steven.price, alvin.sun
Add a firmware module to load, parse, and map the MCU firmware sections
into shared GEM memory at the required virtual addresses accessible by the
GPU.
Create a firmware instance during probe and store it inside the
TyrDrmRegistrationData to keep it alive after probe. Use the firmware
instance to boot the MCU.
Remove the dead-code annotations from the MMU, VM, slot manager, and
kernel BO code now that these paths are used by the firmware module.
Update Kconfig to add the RUST_FW_LOADER_ABSTRACTIONS dependency
required by this module.
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/Kconfig | 1 +
drivers/gpu/drm/tyr/driver.rs | 23 ++-
drivers/gpu/drm/tyr/fw.rs | 321 ++++++++++++++++++++++++++++++++++++++++++
drivers/gpu/drm/tyr/gem.rs | 3 -
drivers/gpu/drm/tyr/tyr.rs | 1 +
drivers/gpu/drm/tyr/vm.rs | 1 -
6 files changed, 341 insertions(+), 9 deletions(-)
diff --git a/drivers/gpu/drm/tyr/Kconfig b/drivers/gpu/drm/tyr/Kconfig
index 79ea4bb214de..8f13e49f11f9 100644
--- a/drivers/gpu/drm/tyr/Kconfig
+++ b/drivers/gpu/drm/tyr/Kconfig
@@ -13,6 +13,7 @@ config DRM_TYR
select IOMMU_IO_PGTABLE_LPAE
select RUST_DRM_GEM_SHMEM_HELPER
select RUST_DRM_GPUVM
+ select RUST_FW_LOADER_ABSTRACTIONS
help
Rust DRM driver for ARM Mali CSF-based GPUs.
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index f487b3996293..d78ad9d292ff 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -37,6 +37,7 @@
use crate::{
file::TyrDrmFileData,
+ fw::Firmware,
gem::Bo,
gpu,
gpu::GpuInfo,
@@ -67,6 +68,9 @@ pub(crate) struct TyrDrmRegistrationData<'drm> {
/// Parent platform device.
pub(crate) pdev: &'drm platform::Device<Bound>,
+ /// Firmware sections.
+ pub(crate) fw: Firmware<'drm>,
+
#[pin]
clks: Mutex<Clocks>,
@@ -144,10 +148,21 @@ fn probe<'bound>(
let unreg_dev = drm::UnregisteredDevice::<TyrDrmDriver>::new(pdev, Ok(()))?;
- let _mmu = Mmu::new(pdev.as_ref(), iomem.as_arc_borrow(), &gpu_info)?;
+ let mmu = Mmu::new(pdev.as_ref(), iomem.as_arc_borrow(), &gpu_info)?;
+
+ let firmware = Firmware::new(
+ pdev.as_ref(),
+ iomem.clone(),
+ &unreg_dev,
+ mmu.as_arc_borrow(),
+ &gpu_info,
+ )?;
+
+ firmware.boot()?;
- let reg_data = try_pin_init!(TyrDrmRegistrationData {
+ let reg_data = pin_init!(TyrDrmRegistrationData {
pdev,
+ fw: firmware,
clks <- new_mutex!(Clocks {
core: core_clk,
stacks: stacks_clk,
@@ -167,9 +182,7 @@ fn probe<'bound>(
let driver = TyrPlatformDriverData { _reg: reg };
- // We need this to be dev_info!() because dev_dbg!() does not work at
- // all in Rust for now, and we need to see whether probe succeeded.
- dev_info!(pdev, "Tyr initialized correctly.\n");
+ dev_dbg!(pdev, "Tyr initialized correctly.");
Ok(driver)
}
}
diff --git a/drivers/gpu/drm/tyr/fw.rs b/drivers/gpu/drm/tyr/fw.rs
new file mode 100644
index 000000000000..47d25c901bd0
--- /dev/null
+++ b/drivers/gpu/drm/tyr/fw.rs
@@ -0,0 +1,321 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Firmware loading and management for Mali CSF GPU.
+//!
+//! This module handles loading the Mali GPU firmware binary, parsing it into sections,
+//! and mapping those sections into the MCU's virtual address space. Each firmware section
+//! has specific properties (read/write/execute permissions, cache modes) and must be loaded
+//! at specific virtual addresses expected by the MCU.
+//!
+//! See [`Firmware`] for the main firmware management interface and [`Section`] for
+//! individual firmware sections.
+//!
+//! [`Firmware`]: crate::fw::Firmware
+//! [`Section`]: crate::fw::Section
+
+use kernel::{
+ device::{
+ Bound,
+ Device, //
+ },
+ drm::{
+ gem::BaseObject, //
+ },
+ io::{
+ poll,
+ Io, //
+ },
+ num::Bounded,
+ prelude::*,
+ register,
+ str::CString,
+ sync::{
+ Arc,
+ ArcBorrow, //
+ },
+ time, //
+};
+
+use crate::{
+ driver::{
+ IoMem,
+ TyrDrmDevice, //
+ },
+ fw::parser::{
+ FwParser,
+ ParsedSection, //
+ },
+ gem,
+ gem::{
+ KernelBo,
+ KernelBoVaAlloc, //
+ },
+ gpu::GpuInfo,
+
+ mmu::Mmu,
+ regs::{
+ gpu_control::{
+ McuControlMode,
+ McuStatus,
+ GPU_ID,
+ MCU_CONTROL,
+ MCU_STATUS, //
+ }, //
+ job_control::{
+ JOB_IRQ_CLEAR,
+ JOB_IRQ_RAWSTAT, //
+ }, //
+ },
+ vm::Vm, //
+};
+
+mod parser;
+
+pub(super) const CSF_MCU_SHARED_REGION_START: u32 = 0x04000000;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[repr(u8)]
+pub(super) enum CacheMode {
+ None = 0,
+ Cached = 1,
+ UncachedCoherent = 2,
+ CachedCoherent = 3,
+}
+
+impl From<Bounded<u32, 2>> for CacheMode {
+ fn from(value: Bounded<u32, 2>) -> Self {
+ match value.get() {
+ 0 => Self::None,
+ 1 => Self::Cached,
+ 2 => Self::UncachedCoherent,
+ 3 => Self::CachedCoherent,
+ _ => unreachable!(),
+ }
+ }
+}
+
+impl From<CacheMode> for Bounded<u32, 2> {
+ fn from(value: CacheMode) -> Self {
+ Bounded::try_new(value as u32).unwrap()
+ }
+}
+
+register! {
+ #[allow(non_upper_case_globals)]
+ pub(super) SectionFlags(u32) @ 0x0 {
+ 0:0 read => bool;
+ 1:1 write => bool;
+ 2:2 exec => bool;
+ 4:3 cache_mode => CacheMode;
+ 5:5 prot => bool;
+ 30:30 shared => bool;
+ 31:31 zero => bool;
+ }
+}
+
+impl SectionFlags {
+ const VALID_MASK: u32 = Self::READ_MASK
+ | Self::WRITE_MASK
+ | Self::EXEC_MASK
+ | Self::CACHE_MODE_MASK
+ | Self::PROT_MASK
+ | Self::SHARED_MASK
+ | Self::ZERO_MASK;
+
+ fn try_from_fw(value: u32) -> Result<Self> {
+ if value & !Self::VALID_MASK != 0 {
+ Err(EINVAL)
+ } else {
+ Ok(Self::from_raw(value))
+ }
+ }
+}
+
+/// A parsed section of the firmware binary.
+struct Section<'drm> {
+ // Raw firmware section data for reset purposes
+ #[expect(dead_code)]
+ data: KVec<u8>,
+
+ // Keep the BO backing this firmware section so that both the
+ // GPU mapping and CPU mapping remain valid until the Section is dropped.
+ #[expect(dead_code)]
+ mem: gem::KernelBo<'drm>,
+}
+
+/// Loaded firmware with sections mapped into MCU VM.
+pub(crate) struct Firmware<'drm> {
+ /// Iomem need to access registers.
+ iomem: Arc<IoMem<'drm>>,
+
+ /// MCU VM.
+ vm: Arc<Vm<'drm>>,
+
+ /// List of firmware sections.
+ #[expect(dead_code)]
+ sections: KVec<Section<'drm>>,
+}
+
+impl<'drm> Drop for Firmware<'drm> {
+ fn drop(&mut self) {
+ // Stop the MCU before releasing its firmware mappings and memory.
+ let _ = self.stop();
+
+ // AS slots retain a VM ref, we need to kill the circular ref manually.
+ self.vm.kill();
+ }
+}
+
+impl<'drm> Firmware<'drm> {
+ fn init_section_mem(dev: &Device, mem: &mut KernelBo<'drm>, data: &KVec<u8>) -> Result {
+ if data.is_empty() {
+ return Ok(());
+ }
+
+ let vmap = mem.bo().vmap::<0>()?;
+ let size = mem.bo().size();
+
+ if data.len() > size {
+ dev_err!(dev, "fw section {} bigger than BO {}", data.len(), size);
+ return Err(EINVAL);
+ }
+
+ for (i, &byte) in data.iter().enumerate() {
+ vmap.try_write8(byte, i)?;
+ }
+
+ Ok(())
+ }
+
+ fn request(ddev: &TyrDrmDevice, gpu_info: &GpuInfo) -> Result<kernel::firmware::Firmware> {
+ let gpu_id = GPU_ID::from_raw(gpu_info.gpu_id);
+
+ let path = CString::try_from_fmt(fmt!(
+ "arm/mali/arch{}.{}/mali_csffw.bin",
+ gpu_id.arch_major().get(),
+ gpu_id.arch_minor().get()
+ ))?;
+
+ kernel::firmware::Firmware::request(&path, ddev.as_ref().as_ref())
+ }
+
+ fn load(
+ dev: &Device,
+ ddev: &TyrDrmDevice,
+ gpu_info: &GpuInfo,
+ ) -> Result<(kernel::firmware::Firmware, KVec<ParsedSection>)> {
+ let fw = Self::request(ddev, gpu_info)?;
+ let mut parser = FwParser::new(dev, fw.data());
+
+ let parsed_sections = parser.parse()?;
+
+ Ok((fw, parsed_sections))
+ }
+
+ /// Load firmware and map sections into MCU VM.
+ pub(crate) fn new(
+ dev: &'drm Device<Bound>,
+ iomem: Arc<IoMem<'drm>>,
+ ddev: &TyrDrmDevice,
+ mmu: ArcBorrow<'_, Mmu<'drm>>,
+ gpu_info: &GpuInfo,
+ ) -> Result<Firmware<'drm>> {
+ let vm = Vm::new(dev, ddev, mmu, gpu_info)?;
+ vm.activate()?;
+
+ let result = (|| {
+ let (fw, parsed_sections) = Self::load(dev, ddev, gpu_info)?;
+ let mut sections = KVec::new();
+ for parsed in parsed_sections {
+ let size = u64::from(parsed.va.end.checked_sub(parsed.va.start).ok_or(EINVAL)?);
+
+ let va = u64::from(parsed.va.start);
+
+ let mut mem = KernelBo::new(
+ ddev,
+ vm.clone(),
+ size,
+ KernelBoVaAlloc::Explicit(va),
+ parsed.vm_map_flags,
+ )?;
+
+ let section_start = parsed.data_range.start as usize;
+ let section_end = parsed.data_range.end as usize;
+ let mut data = KVec::new();
+
+ // Ensure that the firmware slice is not out of bounds.
+ let fw_data = fw.data();
+ let bytes = fw_data.get(section_start..section_end).ok_or(EINVAL)?;
+ data.extend_from_slice(bytes, GFP_KERNEL)?;
+
+ Self::init_section_mem(dev, &mut mem, &data)?;
+
+ sections.push(Section { data, mem }, GFP_KERNEL)?;
+ }
+
+ Ok(Firmware {
+ iomem,
+ vm: vm.clone(),
+ sections,
+ })
+ })();
+
+ if result.is_err() {
+ vm.kill();
+ }
+
+ result
+ }
+
+ pub(crate) fn boot(&self) -> Result {
+ let io = &self.iomem;
+
+ // Discard any stale global interrupt.
+ io.write_reg(JOB_IRQ_CLEAR::zeroed().with_glb(true));
+
+ io.write_reg(MCU_CONTROL::zeroed().with_req(McuControlMode::Auto));
+
+ if let Err(e) = poll::read_poll_timeout(
+ || Ok((io.read(MCU_STATUS), io.read(JOB_IRQ_RAWSTAT))),
+ |(mcu_status, irq_rawstat)| {
+ mcu_status.value() == McuStatus::Enabled && irq_rawstat.glb()
+ },
+ time::Delta::from_millis(1),
+ time::Delta::from_millis(100),
+ ) {
+ let status = io.read(MCU_STATUS);
+ dev_err!(
+ self.vm.dev(),
+ "MCU failed to boot, status: {:?}",
+ status.value()
+ );
+ return Err(e);
+ }
+
+ io.write_reg(JOB_IRQ_CLEAR::zeroed().with_glb(true));
+
+ Ok(())
+ }
+
+ fn stop(&self) -> Result {
+ let io = &self.iomem;
+ io.write_reg(MCU_CONTROL::zeroed().with_req(McuControlMode::Disable));
+
+ if let Err(e) = poll::read_poll_timeout(
+ || Ok(io.read(MCU_STATUS)),
+ |status| status.value() == McuStatus::Disabled,
+ time::Delta::from_micros(10),
+ time::Delta::from_millis(100),
+ ) {
+ let status = io.read(MCU_STATUS);
+ dev_err!(
+ self.vm.dev(),
+ "MCU failed to stop, status: {:?}",
+ status.value()
+ );
+ return Err(e);
+ }
+
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index 69e1c75e59a5..3bf3787f5c3f 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -72,7 +72,6 @@ pub(crate) fn new_dummy_object(ddev: &TyrDrmDevice) -> Result<ARef<Bo>> {
/// An automatic VA allocation strategy will be added in the future.
pub(crate) enum KernelBoVaAlloc {
/// Explicit VA address specified by the caller.
- #[expect(dead_code)]
Explicit(u64),
}
@@ -98,7 +97,6 @@ impl<'drm> KernelBo<'drm> {
/// This function allocates a new shmem-backed GEM object and immediately maps
/// it into the specified GPU virtual memory space. The mapping is automatically
/// cleaned up when the [`KernelBo`] is dropped.
- #[expect(dead_code)]
pub(crate) fn new(
ddev: &TyrDrmDevice,
vm: Arc<Vm<'drm>>,
@@ -135,7 +133,6 @@ pub(crate) fn new(
})
}
- #[expect(dead_code)]
pub(crate) fn bo(&self) -> &Bo {
&self.bo
}
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index 92f6885cdaae..e7ec450bdc9c 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -9,6 +9,7 @@
mod driver;
mod file;
+mod fw;
mod gem;
mod gpu;
mod mmu;
diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
index 53a17dd8d949..74c3d6c8efc4 100644
--- a/drivers/gpu/drm/tyr/vm.rs
+++ b/drivers/gpu/drm/tyr/vm.rs
@@ -6,7 +6,6 @@
//! the illusion of owning the entire virtual address (VA) range, similar to CPU virtual memory.
//! Each virtual memory (VM) area is backed by ARM64 LPAE Stage 1 page tables and can be
//! mapped into hardware address space (AS) slots for GPU execution.
-#![expect(dead_code)]
use core::marker::PhantomData;
use core::ops::Range;
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v10 6/7] drm/tyr: add parser for firmware binary
2026-07-28 18:39 ` [PATCH v10 6/7] drm/tyr: add parser for firmware binary Deborah Brouwer
@ 2026-07-28 18:47 ` sashiko-bot
0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-07-28 18:47 UTC (permalink / raw)
To: Deborah Brouwer; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing validation that the firmware section data size does not exceed the virtual address mapping size.
- [Low] Crate-local imports in the `fw` module are not sorted alphabetically.
--
--- Patch [6]: [PATCH v10 6/7] drm/tyr: add parser for firmware binary ---
commit 2d4004e4d7f81ddd626b7391da06ddfa0d78eb29
Author: Daniel Almeida <daniel.almeida@collabora.com>
drm/tyr: add parser for firmware binary
This commit adds a parser for the Mali CSF GPU firmware binary format. It
extracts section metadata, validates the basic firmware structure and
alignment, and ignores protected-mode sections.
> diff --git a/drivers/gpu/drm/tyr/fw/parser.rs b/drivers/gpu/drm/tyr/fw/parser.rs
> new file mode 100644
> index 0000000000000..c4d0ad1d7899d
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/fw/parser.rs
[ ... ]
> +use crate::{
> + fw::{
> + CacheMode,
> + SectionFlags,
> + CSF_MCU_SHARED_REGION_START, //
[Severity: Low]
This isn't a bug, but the Rust coding guidelines require nested imports to be
sorted alphabetically. CSF_MCU_SHARED_REGION_START should be listed before
SectionFlags.
> + },
> + vm::{
> + VmFlag,
> + VmMapFlags, //
> + }, //
> +};
[ ... ]
> + fn parse_section_entry(
> + entry_cursor: &mut Cursor<'_>,
> + firmware_size: usize,
> + ) -> Result<Option<ParsedSection>> {
> + let section_hdr: SectionHeader = SectionHeader::new(entry_cursor)?;
> +
> + if section_hdr.data.end < section_hdr.data.start {
[Severity: Medium]
Does this code need to ensure the initialization data size doesn't exceed the
destination virtual address mapping size?
If a malformed firmware binary is provided where the data length
(data.end - data.start) is greater than the virtual address length
(va.end - va.start), could this lead to out-of-bounds writes when a firmware
loader uses the virtual address size for memory allocation but copies the
initialization data based on the binary data size?
> + dev_err!(
> + entry_cursor.dev,
> + "Firmware corrupted, data.end < data.start (0x{:x} < 0x{:x})",
> + section_hdr.data.end,
> + section_hdr.data.start
> + );
> + return Err(EINVAL);
> + }
> +
> + if section_hdr.data.end as usize > firmware_size {
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-fw-boot-b4-v10-0-9187aefa3f2f@collabora.com?part=6
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support
2026-07-28 18:39 ` [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support Deborah Brouwer
@ 2026-07-28 18:52 ` sashiko-bot
2026-07-28 19:31 ` Danilo Krummrich
1 sibling, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-07-28 18:52 UTC (permalink / raw)
To: Deborah Brouwer; +Cc: ojeda, dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Missing MmuCommand::Unlock in as_end_update() leaves the address space locked.
--
--- Patch [3]: [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support ---
commit 4673c22526eac7702f0e2418212a28a74a5e56d8
Author: Boris Brezillon <boris.brezillon@collabora.com>
drm/tyr: add Memory Management Unit (MMU) support
Add Memory Management Unit (MMU) support in Tyr, which wraps a SlotManager
instance to allocate MMU address-space slots for virtual memory. This includes
an AddressSpaceManager for MMU hardware register operations such as locking
and flushing address spaces.
> diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
> new file mode 100644
> index 0000000000000..76e3d2df6afd4
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
[ ... ]
> + /// Locks a region of the translation tables for an atomic update.
> + ///
> + /// Programs the MMU LOCKADDR register for the given address space and issues
> + /// the lock command. The hardware rounds the requested range up to a
> + /// power-of-two region aligned to its size.
> + ///
> + /// Returns an error if the slot is invalid or if register writes/commands fail.
> + fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
[ ... ]
> + self.as_send_cmd_and_wait(as_nr, MmuCommand::Lock)
> + }
> +
> + /// Completes an atomic translation table update.
> + ///
> + /// Returns an error if the slot is invalid or if the flush command fails.
> + fn as_end_update(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> + self.as_send_cmd_and_wait(as_nr, MmuCommand::FlushPt)?;
[Severity: High]
Does this function also need to send MmuCommand::Unlock to release the locked
region?
The start of the update sequence in as_start_update() locks the region by
sending MmuCommand::Lock, but as_end_update() currently only sends
MmuCommand::FlushPt and omits the unlock command.
If the address space remains locked, GPU accesses to the updated virtual
memory region could be permanently blocked. This might lead to GPU hangs or
command timeouts when userspace triggers VM mapping or unmapping operations.
> + Ok(())
> + }
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-fw-boot-b4-v10-0-9187aefa3f2f@collabora.com?part=3
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 5/7] drm/tyr: add a kernel buffer object
2026-07-28 18:39 ` [PATCH v10 5/7] drm/tyr: add a kernel buffer object Deborah Brouwer
@ 2026-07-28 18:54 ` sashiko-bot
0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-07-28 18:54 UTC (permalink / raw)
To: Deborah Brouwer; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Resource leak: `KernelBo::drop` permanently leaks the GEM buffer object and its virtual mapping if `vm.unmap_range()` fails.
--
--- Patch [5]: [PATCH v10 5/7] drm/tyr: add a kernel buffer object ---
commit f17ba69672f18401371cf98a1da7881e0b938c23
Author: Deborah Brouwer <deborah.brouwer@collabora.com>
drm/tyr: add a kernel buffer object
This commit introduces a buffer object type (KernelBo) for internal driver
allocations managed by the kernel. It wraps a GEM shmem object and automatically
handles GPU virtual address space mapping during creation and unmapping on drop.
> diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> index c28be61a01bb2..69e1c75e59a55 100644
> --- a/drivers/gpu/drm/tyr/gem.rs
> +++ b/drivers/gpu/drm/tyr/gem.rs
[ ... ]
> +impl Drop for KernelBo<'_> {
> + fn drop(&mut self) {
> + let va = self.va_range.start;
> + let size = self.va_range.end - self.va_range.start;
> +
> + if let Err(e) = self.vm.unmap_range(va, size) {
[Severity: Medium]
Does this leak the GEM buffer object if unmap_range() fails?
If unmap_range() encounters an error (such as a kmalloc failure due to
memory pressure when preallocating GPUVA objects to handle a mapping split),
it aborts the unmap operation.
Because the error is caught and ignored here, the drm_gpuvm will retain its
reference to the GEM object indefinitely, leaking it along with its
physical pages.
> + // If unmap_range fails, it is still safe to drop the
> + // KernelBo and its ARef to the GEM buffer object because
> + // GPUVM also holds a reference to the GEM buffer object.
> + // The physical pages won't be freed or reallocated.
> + dev_err!(
> + self.vm.dev(),
> + "Failed to unmap KernelBo range {:#x}..{:#x}: {:?}",
> + self.va_range.start,
> + self.va_range.end,
> + e
> + );
> + }
> + }
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-fw-boot-b4-v10-0-9187aefa3f2f@collabora.com?part=5
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support
2026-07-28 18:39 ` [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support Deborah Brouwer
@ 2026-07-28 18:54 ` sashiko-bot
0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-07-28 18:54 UTC (permalink / raw)
To: Deborah Brouwer; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] `map_bo_range` lacks a bounds check against the VM's virtual address range, allowing out-of-bounds MMU update commands to be sent to the hardware.
- [High] Partial unmap of 2MB page table blocks causes the ARM LPAE io-pgtable backend to reject the operation, breaking drm_gpuvm unmap and remap.
--
--- Patch [4]: [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support ---
commit 7f9a396490c23472f40e677be97d921859c50864
Author: Boris Brezillon <boris.brezillon@collabora.com>
drm/tyr: add GPU virtual memory (VM) support
Add GPU virtual address space management using the DRM GPUVM framework.
Each virtual memory (VM) space is backed by ARM64 LPAE Stage 1 page tables
and can be mapped into hardware address space (AS) slots for GPU execution.
The implementation provides memory isolation and virtual address
allocation. VMs support mapping GEM buffer objects with configurable
protection flags (readonly, noexec, uncached) and handle both 4KB and 2MB
page sizes. A new_dummy_object() helper is provided to create a dummy GEM
object for use as a GPUVM root.
> diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
> new file mode 100644
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/vm.rs
[ ... ]
> + pub(crate) fn map_bo_range(
> + &self,
> + bo: &Bo,
> + bo_offset: u64,
> + map_size: u64,
> + va: u64,
> + flags: VmMapFlags,
> + ) -> Result {
[ ... ]
> + let bo_end = bo_offset.checked_add(map_size).ok_or(EINVAL)?;
> +
> + if bo_end > bo_size {
> + dev_err!(
> + self.dev,
> + "BO mapping range {:#x}..{:#x} exceeds BO size {:#x}",
> + bo_offset,
> + bo_end,
> + bo_size
> + );
> + return Err(EINVAL);
> + }
> +
> + let va_end: u64 = va.checked_add(map_size).ok_or(EINVAL)?;
> +
> + let req = VmOpRequest {
[Severity: High]
Does this code need to check if the calculated va_end exceeds the VM's
virtual address range?
If a user-space caller provides a very large map_size, va + map_size could
exceed the VM's valid range. Because there's no bounds check against
self.va_range.end here (unlike in unmap_range()), the out-of-bounds region
will be passed to PtUpdateContext::new(). This might issue a hardware lock
command to the MMU before drm_gpuvm rejects the mapping. Could this lead to
unhandled hardware faults or device hangs?
> + op_type: VmOpType::Map(VmMapArgs {
> + vm_bo: self.gpuvm.obtain(bo, ())?,
> + flags,
> + bo_offset,
> + }),
> + region: va..va_end,
> + };
[ ... ]
> + fn sm_step_remap<'op>(
> + &mut self,
> + op: OpRemap<'op, Self>,
> + context: &mut Self::SmContext<'_>,
> + ) -> Result<OpRemapped<'op, Self>, Error> {
[ ... ]
> + let unmap_length = unmap_end - unmap_start;
> +
> + if unmap_length > 0 {
> + let region = unmap_start..(unmap_start + unmap_length);
> + pt_unmap(context.dev, context.pt, region.clone()).inspect_err(|e| {
[Severity: High]
Will this fail if the region is a partial unmap of a 2MB page table block?
If a buffer object is naturally aligned to 2MB, pt_map() may opportunistically
use 2MB block descriptors. If a caller later attempts to unmap a 4KB
sub-region, sm_step_remap() will pass this 4KB region to pt_unmap().
The ARM LPAE io-pgtable backend (arm_lpae_unmap_pages) does not automatically
split huge pages, so it might encounter the 2MB leaf descriptor, reject the
operation, and return 0, causing pt_unmap() to return EINVAL.
Could this leave the DRM GPUVM tree out of sync with the hardware page
tables and leak the mapping in the GPU?
> + dev_err!(
> + context.dev,
> + "Failed to unmap remap region {:#x}..{:#x}: {:?}",
> + region.start,
> + region.end,
> + e
> + );
> + })?;
> + }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-fw-boot-b4-v10-0-9187aefa3f2f@collabora.com?part=4
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 1/7] drm/tyr: add resources to RegistrationData
2026-07-28 18:39 ` [PATCH v10 1/7] drm/tyr: add resources to RegistrationData Deborah Brouwer
@ 2026-07-28 18:55 ` sashiko-bot
0 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-07-28 18:55 UTC (permalink / raw)
To: Deborah Brouwer; +Cc: dri-devel, ojeda
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [Critical] The DEV_QUERY ioctl handler fails with -EFAULT when userspace provides a smaller buffer size than the kernel's structure size, violating the documented Panthor UAPI.
--
--- Patch [1]: [PATCH v10 1/7] drm/tyr: add resources to RegistrationData ---
commit 582ca5eb256eb752ad582c7535dd1fc5bf72cd26
Author: Deborah Brouwer <deborah.brouwer@collabora.com>
drm/tyr: add resources to RegistrationData
Currently Tyr is not storing any resources in its drm::Driver
RegistrationData. Move Tyr's device-private resources and gpu information
from drm::Driver::Data to drm::Driver::RegistrationData so they can be
accessed safely while the device is registered with userspace.
> diff --git a/drivers/gpu/drm/tyr/file.rs b/drivers/gpu/drm/tyr/file.rs
> index b686041d5d6b4..9f60a90d49487 100644
> --- a/drivers/gpu/drm/tyr/file.rs
> +++ b/drivers/gpu/drm/tyr/file.rs
[ ... ]
> @@ -53,7 +54,7 @@ pub(crate) fn dev_query(
> )
> .writer();
>
> - writer.write(&ddev.gpu_info)?;
> + writer.write(®_data.gpu_info)?;
[Severity: Critical]
This isn't a bug introduced by this patch, but does this strict size check
violate the documented Panthor UAPI?
When userspace invokes the DRM_IOCTL_PANTHOR_DEV_QUERY ioctl with a buffer
size smaller than the kernel's GpuInfo struct (for example, from older
userspace clients), the UserSliceWriter::write() method strictly enforces
that the length must be at least the size of the structure and returns
-EFAULT if it is not.
The C driver permits this as long as the size is at least the documented
min_size, copying only the truncated structure. Could this strict size
check in the Rust driver break backwards compatibility for older userspace?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-fw-boot-b4-v10-0-9187aefa3f2f@collabora.com?part=1
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support
2026-07-28 18:39 ` [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support Deborah Brouwer
2026-07-28 18:52 ` sashiko-bot
@ 2026-07-28 19:31 ` Danilo Krummrich
2026-07-28 20:35 ` Deborah Brouwer
1 sibling, 1 reply; 16+ messages in thread
From: Danilo Krummrich @ 2026-07-28 19:31 UTC (permalink / raw)
To: Deborah Brouwer
Cc: Daniel Almeida, Alice Ryhl, David Airlie, Simona Vetter,
Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan, dri-devel,
linux-kernel, rust-for-linux, laura.nao, samitolvanen, lyude,
beata.michalska, boris.brezillon, steven.price, alvin.sun
On Tue Jul 28, 2026 at 8:39 PM CEST, Deborah Brouwer wrote:
> +/// Locked wrapper for carrying out virtual memory (VM) operations on the MMU.
> +#[pin_data]
> +pub(crate) struct Mmu<'drm> {
This shouldn't be 'drm, but 'mmu or just something generic like 'a.
The rationale is that Mmu might be shorter lived than 'drm once we have
self-referencial pin-init. Currently it is straight forward, such as in
struct Foo<'foo> {
dev: &'foo platform::Device<Bound>,
}
struct Data<'bound> {
foo: Foo<'bound>,
}
where the lifetime of `dev` really ties back to 'bound. However, with
self-referencial pin-init it could looks like this:
struct Foo<'foo> {
dev: &'foo platform::Device<Bound>,
io: &'foo IoMem<'foo>,
}
struct Data<'bound> {
foo: Foo<'io>,
io: IoMem<'bound>,
}
Now Foo is not constrained to 'bound anymore, as this would be longer lived than
`io`, so it has to capture 'io instead in order to still compile.
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support
2026-07-28 19:31 ` Danilo Krummrich
@ 2026-07-28 20:35 ` Deborah Brouwer
2026-07-28 21:19 ` Danilo Krummrich
0 siblings, 1 reply; 16+ messages in thread
From: Deborah Brouwer @ 2026-07-28 20:35 UTC (permalink / raw)
To: Danilo Krummrich
Cc: Daniel Almeida, Alice Ryhl, David Airlie, Simona Vetter,
Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan, dri-devel,
linux-kernel, rust-for-linux, laura.nao, samitolvanen, lyude,
beata.michalska, boris.brezillon, steven.price, alvin.sun
On Tue, Jul 28, 2026 at 09:31:50PM +0200, Danilo Krummrich wrote:
> On Tue Jul 28, 2026 at 8:39 PM CEST, Deborah Brouwer wrote:
> > +/// Locked wrapper for carrying out virtual memory (VM) operations on the MMU.
> > +#[pin_data]
> > +pub(crate) struct Mmu<'drm> {
>
> This shouldn't be 'drm, but 'mmu or just something generic like 'a.
>
> The rationale is that Mmu might be shorter lived than 'drm once we have
> self-referencial pin-init. Currently it is straight forward, such as in
>
> struct Foo<'foo> {
> dev: &'foo platform::Device<Bound>,
> }
>
> struct Data<'bound> {
> foo: Foo<'bound>,
> }
>
> where the lifetime of `dev` really ties back to 'bound. However, with
> self-referencial pin-init it could looks like this:
>
> struct Foo<'foo> {
> dev: &'foo platform::Device<Bound>,
> io: &'foo IoMem<'foo>,
> }
>
> struct Data<'bound> {
> foo: Foo<'io>,
> io: IoMem<'bound>,
> }
>
> Now Foo is not constrained to 'bound anymore, as this would be longer lived than
> `io`, so it has to capture 'io instead in order to still compile.
Danilo, would you be ok with renaming this lifetime as a follow up
patch? Currently, if I understand this correctly, the mmu lifetime is
still tied to the DRM registration data, so the name is not inaccurate
for Tyr at least as it stands right now.
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support
2026-07-28 20:35 ` Deborah Brouwer
@ 2026-07-28 21:19 ` Danilo Krummrich
0 siblings, 0 replies; 16+ messages in thread
From: Danilo Krummrich @ 2026-07-28 21:19 UTC (permalink / raw)
To: Deborah Brouwer
Cc: Daniel Almeida, Alice Ryhl, David Airlie, Simona Vetter,
Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
Björn Roy Baron, Andreas Hindborg, Trevor Gross,
Tamir Duberstein, Alexandre Courbot, Onur Özkan, dri-devel,
linux-kernel, rust-for-linux, laura.nao, samitolvanen, lyude,
beata.michalska, boris.brezillon, steven.price, alvin.sun
On Tue Jul 28, 2026 at 10:35 PM CEST, Deborah Brouwer wrote:
> On Tue, Jul 28, 2026 at 09:31:50PM +0200, Danilo Krummrich wrote:
>> On Tue Jul 28, 2026 at 8:39 PM CEST, Deborah Brouwer wrote:
>> > +/// Locked wrapper for carrying out virtual memory (VM) operations on the MMU.
>> > +#[pin_data]
>> > +pub(crate) struct Mmu<'drm> {
>>
>> This shouldn't be 'drm, but 'mmu or just something generic like 'a.
>>
>> The rationale is that Mmu might be shorter lived than 'drm once we have
>> self-referencial pin-init. Currently it is straight forward, such as in
>>
>> struct Foo<'foo> {
>> dev: &'foo platform::Device<Bound>,
>> }
>>
>> struct Data<'bound> {
>> foo: Foo<'bound>,
>> }
>>
>> where the lifetime of `dev` really ties back to 'bound. However, with
>> self-referencial pin-init it could looks like this:
>>
>> struct Foo<'foo> {
>> dev: &'foo platform::Device<Bound>,
>> io: &'foo IoMem<'foo>,
>> }
>>
>> struct Data<'bound> {
>> foo: Foo<'io>,
>> io: IoMem<'bound>,
>> }
>>
>> Now Foo is not constrained to 'bound anymore, as this would be longer lived than
>> `io`, so it has to capture 'io instead in order to still compile.
>
> Danilo, would you be ok with renaming this lifetime as a follow up
> patch?
Sure; note that I also said that I'm happy for this to be a follow-up in the
previous version. :)
> Currently, if I understand this correctly, the mmu lifetime is still tied to
> the DRM registration data, so the name is not inaccurate for Tyr at least as
> it stands right now.
With the current implementation, the Mmu lifetime technically ties back to
'bound; the DRM registration data itself is shorter lived (it is dropped before
'bound ends).
The reason I mentioned not to use the 'bound lifetime name in the first place is
that we agreed to not use the 'bound lifetime name on structs that may be
shorter lived than the device being bound, which is true for anything other than
the bus device private data struct.
^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-07-28 21:19 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-28 18:39 [PATCH v10 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 1/7] drm/tyr: add resources to RegistrationData Deborah Brouwer
2026-07-28 18:55 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 2/7] drm/tyr: add a generic slot manager Deborah Brouwer
2026-07-28 18:39 ` [PATCH v10 3/7] drm/tyr: add Memory Management Unit (MMU) support Deborah Brouwer
2026-07-28 18:52 ` sashiko-bot
2026-07-28 19:31 ` Danilo Krummrich
2026-07-28 20:35 ` Deborah Brouwer
2026-07-28 21:19 ` Danilo Krummrich
2026-07-28 18:39 ` [PATCH v10 4/7] drm/tyr: add GPU virtual memory (VM) support Deborah Brouwer
2026-07-28 18:54 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 5/7] drm/tyr: add a kernel buffer object Deborah Brouwer
2026-07-28 18:54 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 6/7] drm/tyr: add parser for firmware binary Deborah Brouwer
2026-07-28 18:47 ` sashiko-bot
2026-07-28 18:39 ` [PATCH v10 7/7] drm/tyr: add Microcontroller Unit (MCU) booting Deborah Brouwer
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.