| // 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. |
| |
| 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, |
| io::PhysAddr, |
| 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 as PhysAddr, |
| 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(()) |
| } |