linux/drivers/gpu/drm/tyr/mmu/address_space.rs
Boris Brezillon ae047468e0 drm/tyr: add Memory Management Unit (MMU) support
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>
Link: https://patch.msgid.link/20260728-fw-boot-b4-v10-3-9187aefa3f2f@collabora.com
Signed-off-by: Alice Ryhl <aliceryhl@google.com>
2026-07-30 13:43:06 +00:00

512 lines
17 KiB
Rust

// 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)
}
}