blob: 76e3d2df6afd490835edbd8bbbffb63df93323cd [file] [edit]
// 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)
}
}