blob: c5e307b1e2416837c85f890c074f62bc74289178 [file] [edit]
// 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, &region)?;
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(())
}