// 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, 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, va_bits: u32, pa_bits: u32, ) -> impl pin_init::PinInit, 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 { 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` 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, /// Memory-mapped I/O region for GPU register access. iomem: Arc>, /// 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, iomem: Arc>, as_present: u32, ) -> Result> { 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) -> 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 for AddressSpaceManager<'drm> { /// VM address space data associated with a hardware slot. type SlotData = Arc>; fn seat(slot_data: &Self::SlotData) -> &LockedSeat { &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, ) -> 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) } }