mirror of
https://github.com/torvalds/linux.git
synced 2026-09-24 06:24:02 +02:00
Add a firmware module to load, parse, and map the MCU firmware sections into shared GEM memory at the required virtual addresses accessible by the GPU. Create a firmware instance during probe and store it inside the TyrDrmRegistrationData to keep it alive after probe. Use the firmware instance to boot the MCU. Remove the dead-code annotations from the MMU, VM, slot manager, and kernel BO code now that these paths are used by the firmware module. Update Kconfig to add the RUST_FW_LOADER_ABSTRACTIONS dependency required by this module. Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com> Link: https://patch.msgid.link/20260728-fw-boot-b4-v10-7-9187aefa3f2f@collabora.com Signed-off-by: Alice Ryhl <aliceryhl@google.com>
951 lines
30 KiB
Rust
951 lines
30 KiB
Rust
// 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(())
|
|
}
|