From: Daniel Almeida <daniel.almeida@collabora.com>
To: Deborah Brouwer <deborah.brouwer@collabora.com>
Cc: Alice Ryhl <aliceryhl@google.com>,
Danilo Krummrich <dakr@kernel.org>,
David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
Benno Lossin <lossin@kernel.org>, Gary Guo <gary@garyguo.net>,
dri-devel@lists.freedesktop.org, linux-kernel@vger.kernel.org,
rust-for-linux@vger.kernel.org, boris.brezillon@collabora.com,
samitolvanen@google.com, acourbot@nvidia.com,
alvin.sun@linux.dev, laura.nao@collabora.com, work@onurozkan.dev,
beata.michalska@arm.com, steven.price@arm.com, lyude@redhat.com
Subject: Re: [PATCH v6 3/7] drm/tyr: add Memory Management Unit (MMU) support
Date: Thu, 16 Jul 2026 20:24:41 -0300 [thread overview]
Message-ID: <EE2B96EF-5FDF-4215-85C1-2F94EE3BFD06@collabora.com> (raw)
In-Reply-To: <20260709-fw-boot-b4-v6-3-ca391e1a4108@collabora.com>
> On 9 Jul 2026, at 18:36, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> 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 | 103 +++++++
> drivers/gpu/drm/tyr/mmu/address_space.rs | 484 +++++++++++++++++++++++++++++++
> drivers/gpu/drm/tyr/regs.rs | 121 ++++++++
> drivers/gpu/drm/tyr/tyr.rs | 1 +
> 6 files changed, 722 insertions(+), 3 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 46ce5c41e310..74b55a389754 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<'bound> {
> regulators: Mutex<Regulators>,
>
> /// GPU MMIO register mapping.
> - pub(crate) iomem: IoMem<'bound>,
> + pub(crate) iomem: Arc<IoMem<'bound>>,
>
> /// 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(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..c0341557d730
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu.rs
> @@ -0,0 +1,103 @@
> +// 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.
> +#![allow(dead_code)]
> +
> +use core::ops::Range;
> +
> +use kernel::{
> + 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<'bound> = SlotManager<AddressSpaceManager<'bound>, MAX_AS>;
> +
> +/// Locked wrapper for carrying out virtual memory (VM) operations on the MMU.
> +#[pin_data]
> +pub(crate) struct Mmu<'bound> {
> + /// Slot Manager instance used to allocate hardware slots and write to MMU registers.
> + #[pin]
> + pub(crate) as_manager: Mutex<AsSlotManager<'bound>>,
> +}
> +
> +impl<'bound> Mmu<'bound> {
> + /// Create an MMU component for this device.
> + pub(crate) fn new(
> + iomem: ArcBorrow<'_, IoMem<'bound>>,
> + gpu_info: &GpuInfo,
> + ) -> Result<Arc<Mmu<'bound>>> {
> + let present = AS_PRESENT::from_raw(gpu_info.as_present).present().get();
> + let slot_count = present.count_ones().try_into()?;
> +
> + let as_manager = AddressSpaceManager::new(iomem.into(), present)?;
> + let mmu_init = try_pin_init!(Self{
> + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
> + });
> + 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: ArcBorrow<'_, VmAsData<'bound>>) -> Result {
> + self.as_manager.lock().activate_vm(vm)
> + }
> +
> + /// Evict a VM from its AS slot and flush the MMU.
> + pub(crate) fn deactivate_vm(&self, vm: &VmAsData<'bound>) -> Result {
> + self.as_manager.lock().deactivate_vm(vm)
> + }
> +
> + /// Flush MMU translation caches after a VM update.
> + pub(crate) fn flush_vm(&self, vm: &VmAsData<'bound>) -> Result {
> + self.as_manager.lock().flush_vm(vm)
> + }
> +
> + /// 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: &VmAsData<'bound>, region: &Range<u64>) -> Result {
> + self.as_manager.lock().start_vm_update(vm, 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: &VmAsData<'bound>) -> Result {
> + self.as_manager.lock().end_vm_update(vm)
> + }
> +}
> 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..a97f44774576
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
> @@ -0,0 +1,484 @@
> +// 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, //
> + },
> + 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::{
> + 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<'bound> {
> + /// The address space seat tracks this VM's binding to a hardware address space slot.
> + /// Uses [`LockedBy`] to ensure safe concurrent access to the slot assignment state,
> + /// protected by the [`AsSlotManager`] lock.
> + as_seat: LockedBy<Seat, AsSlotManager<'bound>>,
> +
> + /// 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<'bound, ARM64LPAES1>,
> +}
> +
> +impl<'bound> VmAsData<'bound> {
> + /// Creates VM address space data by initializing all of its fields.
> + pub(crate) fn new<'a>(
> + mmu: &'a Mmu<'bound>,
> + pdev: &'bound Device<Bound>,
> + va_bits: u32,
> + pa_bits: u32,
> + ) -> impl pin_init::PinInit<VmAsData<'bound>, 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(pdev, 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 ina_bits_field_value = 55 - self.va_bits;
> + let ina_bits = match ina_bits_field_value {
> + 7 => mmu_as_control::InaBits::Bits48,
> + 8 => mmu_as_control::InaBits::Bits47,
> + 9 => mmu_as_control::InaBits::Bits46,
> + 10 => mmu_as_control::InaBits::Bits45,
> + 11 => mmu_as_control::InaBits::Bits44,
> + 12 => mmu_as_control::InaBits::Bits43,
> + 13 => mmu_as_control::InaBits::Bits42,
> + 14 => mmu_as_control::InaBits::Bits41,
> + 15 => mmu_as_control::InaBits::Bits40,
> + 16 => mmu_as_control::InaBits::Bits39,
> + 17 => mmu_as_control::InaBits::Bits38,
> + 18 => mmu_as_control::InaBits::Bits37,
> + 19 => mmu_as_control::InaBits::Bits36,
> + 20 => mmu_as_control::InaBits::Bits35,
> + 21 => mmu_as_control::InaBits::Bits34,
> + 22 => mmu_as_control::InaBits::Bits33,
> + 23 => mmu_as_control::InaBits::Bits32,
> + 24 => mmu_as_control::InaBits::Bits31,
> + 25 => mmu_as_control::InaBits::Bits30,
> + 26 => mmu_as_control::InaBits::Bits29,
> + 27 => mmu_as_control::InaBits::Bits28,
> + 28 => mmu_as_control::InaBits::Bits27,
> + 29 => mmu_as_control::InaBits::Bits26,
> + 30 => mmu_as_control::InaBits::Bits25,
> + _ => return Err(EINVAL),
Would this work?
let field = 55u64.checked_sub(self.va_bits.into()).ok_or(EINVAL)?;
let ina_bits = match InaBits::try_from(Bounded::try_new(field).ok_or(EINVAL)?)? {
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 caller must ensure that the address space is evicted
> + // and cleaned up before the `VmAsData` is dropped.
> + transtab: unsafe { pt.ttbr() },
We need to say why this is safe instead ^
> + 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<'bound> {
> + /// Memory-mapped I/O region for GPU register access.
> + iomem: Arc<IoMem<'bound>>,
> +
> + /// Bitmask of present address space slots from GPU_AS_PRESENT register.
> + as_present: u32,
Throughout this whole patch, we treat AS’s as usize. I wonder if we would benefit
from using Bounded instead? This is a mere question.
> +}
> +
> +impl<'bound> AddressSpaceManager<'bound> {
> + /// 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(
> + iomem: Arc<IoMem<'bound>>,
> + as_present: u32,
> + ) -> Result<AddressSpaceManager<'bound>> {
> + Ok(Self { 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 {
> + pr_err!("AS slot {} out of valid range (max {})\n", as_nr, MAX_AS);
> + return Err(EINVAL);
^ This comparison would come for free with Bounded
> + }
> +
> + if (self.as_present & (1 << as_nr)) == 0 {
^ But not this one, as it’s a runtime check :/
> + pr_err!(
> + "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
> + 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_millis(0), 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
Can we link to this?
> + /// 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)?;
> +
> + // 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 = 32 * 1024;
> +
> + // 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);
> +
> + // 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.
> + let lockaddr_base = region.start & !((1u64 << lock_region_log2) - 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;
> +
> + let lockaddr_val = LOCKADDR::zeroed()
> + .try_with_size(lockaddr_size)?
> + .try_with_base(lockaddr_base)?
> + .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(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(as_nr, MmuCommand::FlushPt)
> + }
> +}
> +
> +impl<'bound> SlotOperations for AddressSpaceManager<'bound> {
> + /// VM address space data associated with a hardware slot.
> + type SlotData = Arc<VmAsData<'bound>>;
> +
> + /// Activates an address space 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 an address space 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<'bound> AsSlotManager<'bound> {
> + /// Locks a region for translation table updates if the VM has an active slot.
> + pub(super) fn start_vm_update(&mut self, vm: &VmAsData<'bound>, region: &Range<u64>) -> Result {
> + let seat = vm.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: &VmAsData<'bound>) -> Result {
> + let seat = vm.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: &VmAsData<'bound>) -> Result {
> + let seat = vm.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: ArcBorrow<'_, VmAsData<'bound>>) -> Result {
> + self.activate(&vm.as_seat, vm.into())
> + }
> +
> + /// Deactivates a VM by evicting it from its hardware slot.
> + pub(super) fn deactivate_vm(&mut self, vm: &VmAsData<'bound>) -> Result {
> + self.evict(&vm.as_seat)
> + }
> +}
> diff --git a/drivers/gpu/drm/tyr/regs.rs b/drivers/gpu/drm/tyr/regs.rs
> index 831357a8ef87..c4f6b1596b31 100644
> --- a/drivers/gpu/drm/tyr/regs.rs
> +++ b/drivers/gpu/drm/tyr/regs.rs
> @@ -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,124 @@ fn from(val: MMU_MEMATTR_STAGE1) -> Self {
> pub(crate) MEMATTR_HI(u32)[MAX_AS, stride = STRIDE] @ 0x240c {
> 31:0 value;
> }
> + }
> +
> + impl MEMATTR {
> + ///
> + /// 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;
> + /// ARM MAIR Write-back bit (bit 2 of a cache policy nibble).
> + const ARM_MAIR_WRITE_BACK: 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 and must
> + /// be mapped as GPU `NonCacheable`.
> + fn is_device_memory(mair_attr: u8) -> bool {
> + // In AArch64 MAIR, device memory has bits [1:0] of outer nibble = 0.
> + let outer = mair_attr >> 4;
> + (outer & 0x3) == 0
> + }
> +
> + /// 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_WRITE_BACK) != 0 && (inner & Self::ARM_MAIR_WRITE_BACK) != 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/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.54.0
>
Reviewed-by: Daniel Almeida <daniel.almeida@collabora.com>
next prev parent reply other threads:[~2026-07-16 23:25 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-09 21:36 [PATCH v6 0/7] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-07-09 21:36 ` [PATCH v6 1/7] drm/tyr: add resources to RegistrationData Deborah Brouwer
2026-07-15 21:51 ` Daniel Almeida
2026-07-09 21:36 ` [PATCH v6 2/7] drm/tyr: add a generic slot manager Deborah Brouwer
2026-07-10 13:23 ` Alice Ryhl
2026-07-15 23:17 ` Daniel Almeida
2026-07-09 21:36 ` [PATCH v6 3/7] drm/tyr: add Memory Management Unit (MMU) support Deborah Brouwer
2026-07-10 13:45 ` Alice Ryhl
2026-07-16 23:24 ` Daniel Almeida [this message]
2026-07-09 21:36 ` [PATCH v6 4/7] drm/tyr: add GPU virtual memory (VM) support Deborah Brouwer
2026-07-10 14:15 ` Alice Ryhl
2026-07-14 3:12 ` Deborah Brouwer
2026-07-10 14:27 ` Alice Ryhl
2026-07-09 21:36 ` [PATCH v6 5/7] drm/tyr: add a kernel buffer object Deborah Brouwer
2026-07-09 21:36 ` [PATCH v6 6/7] drm/tyr: add parser for firmware binary Deborah Brouwer
2026-07-09 21:36 ` [PATCH v6 7/7] drm/tyr: add Microcontroller Unit (MCU) booting Deborah Brouwer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=EE2B96EF-5FDF-4215-85C1-2F94EE3BFD06@collabora.com \
--to=daniel.almeida@collabora.com \
--cc=acourbot@nvidia.com \
--cc=airlied@gmail.com \
--cc=aliceryhl@google.com \
--cc=alvin.sun@linux.dev \
--cc=beata.michalska@arm.com \
--cc=boris.brezillon@collabora.com \
--cc=dakr@kernel.org \
--cc=deborah.brouwer@collabora.com \
--cc=dri-devel@lists.freedesktop.org \
--cc=gary@garyguo.net \
--cc=laura.nao@collabora.com \
--cc=linux-kernel@vger.kernel.org \
--cc=lossin@kernel.org \
--cc=lyude@redhat.com \
--cc=rust-for-linux@vger.kernel.org \
--cc=samitolvanen@google.com \
--cc=simona@ffwll.ch \
--cc=steven.price@arm.com \
--cc=work@onurozkan.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Powered by JetHome