// SPDX-License-Identifier: GPL-2.0 //! Virtual Memory Manager for NVIDIA GPU page table management. //! //! The [`Vmm`] provides high-level page mapping and unmapping operations for GPU //! virtual address spaces (Channels, BAR1, BAR2). use kernel::{ gpu::buddy::AllocatedBlocks, maple_tree::MapleTreeAlloc, prelude::*, rbtree::RBTree, // }; use core::{ cell::Cell, ops::Range, // }; use crate::{ mm::{ pagetable::{ map::{ PtMap, // }, walk::{ PtWalk, WalkResult, // }, MmuVersion, // }, GpuMm, Pfn, Vfn, VramAddress, PAGE_SIZE, // }, num::{ IntoSafeCast, // }, }; /// Multi-page prepared mapping -- VA range allocated, ready for execute. /// /// Produced by [`Vmm::prepare_map()`], consumed by [`Vmm::execute_map()`]. /// The VA space allocation is tracked in the [`Vmm`]'s maple tree and freed /// on error or via [`Vmm::unmap_pages()`]. /// /// Dropping without calling [`Vmm::execute_map()`] logs a warning and leaks /// the VA range in the maple tree. pub(crate) struct PreparedMapping { vfn_start: Vfn, num_pages: usize, /// Logs a warning if dropped without executing. _drop_guard: MustExecuteGuard, } /// Result of a mapping operation -- tracks the active mapped range. /// /// Returned by [`Vmm::execute_map()`] and [`Vmm::map_pages()`]. /// Callers must call [`Vmm::unmap_pages()`] before dropping to invalidate /// PTEs and free the VA range. Dropping without unmapping logs a warning /// and leaks the VA range in the maple tree. pub(crate) struct MappedRange { pub(super) vfn_start: Vfn, pub(super) num_pages: usize, /// Logs a warning if dropped without unmapping. _drop_guard: MustUnmapGuard, } /// Guard that logs a warning if a [`PreparedMapping`] is dropped without /// being consumed by [`Vmm::execute_map()`]. struct MustExecuteGuard { armed: Cell, } impl MustExecuteGuard { const fn new() -> Self { Self { armed: Cell::new(true), } } fn disarm(&self) { self.armed.set(false); } } impl Drop for MustExecuteGuard { fn drop(&mut self) { if self.armed.get() { kernel::pr_warn!("PreparedMapping dropped without calling execute_map()\n"); } } } /// Guard that logs a warning if a [`MappedRange`] is dropped without /// calling [`Vmm::unmap_pages()`]. struct MustUnmapGuard { armed: Cell, } impl MustUnmapGuard { const fn new() -> Self { Self { armed: Cell::new(true), } } fn disarm(&self) { self.armed.set(false); } } impl Drop for MustUnmapGuard { fn drop(&mut self) { if self.armed.get() { kernel::pr_warn!("MappedRange dropped without calling unmap_pages()\n"); } } } /// Virtual Memory Manager for a GPU address space. /// /// Each [`Vmm`] instance manages a single address space identified by its Page /// Directory Base (`PDB`) address. Used for Channel, BAR1 and BAR2 mappings. pub(crate) struct Vmm { /// Page Directory Base address for this address space. #[expect(dead_code)] pdb_addr: VramAddress, /// Page table walker for reading existing mappings. pt_walk: PtWalk, /// Page table mapper for prepare/execute operations. pt_map: PtMap, /// Page table allocations required for mappings. page_table_allocs: KVec>>, /// Maple tree allocator for virtual address range tracking. virt_alloc: Pin>>, /// Total number of pages in the virtual address space. va_pages: usize, /// Prepared PT pages pending PDE installation, keyed by `install_addr`. /// /// Populated during prepare phase and drained in execute phase. Shared by all /// pending maps, preventing races on the same PDE slot. pt_pages: RBTree, } impl Vmm { /// Create a new [`Vmm`] for the given Page Directory Base address. /// /// The [`Vmm`] will manage a virtual address space of `va_size` bytes. pub(crate) fn new( pdb_addr: VramAddress, mmu_version: MmuVersion, va_size: u64, ) -> Result { let page_size: u64 = PAGE_SIZE.into_safe_cast(); let va_pages: usize = (va_size / page_size).into_safe_cast(); let virt_alloc = KBox::pin_init(MapleTreeAlloc::<()>::new(), GFP_KERNEL)?; Ok(Self { pdb_addr, pt_walk: PtWalk::new(pdb_addr, mmu_version), pt_map: PtMap::new(pdb_addr, mmu_version), page_table_allocs: KVec::new(), virt_alloc, va_pages, pt_pages: RBTree::new(), }) } /// Allocate a contiguous virtual frame number range. fn alloc_vfn_range(&self, num_pages: usize, va_range: Option>) -> Result { let page_size: u64 = PAGE_SIZE.into_safe_cast(); let start_vfn = match va_range { Some(r) => { let num_pages_u64: u64 = num_pages.into_safe_cast(); let size = num_pages_u64.checked_mul(page_size).ok_or(EOVERFLOW)?; let range_size = r.end.checked_sub(r.start).ok_or(EOVERFLOW)?; if range_size != size { return Err(EINVAL); } let start_vfn: usize = (r.start / page_size).into_safe_cast(); let end_vfn: usize = (r.end / page_size).into_safe_cast(); self.virt_alloc .insert_range(start_vfn..end_vfn, (), GFP_KERNEL)?; start_vfn } None => self .virt_alloc .alloc_range(num_pages, (), ..self.va_pages, GFP_KERNEL)?, }; Ok(Vfn::new(start_vfn.into_safe_cast())) } /// Free a virtual frame number range back to the maple tree. fn free_vfn(&self, vfn: Vfn) { let vfn_index: usize = vfn.raw().into_safe_cast(); if self.virt_alloc.erase(vfn_index).is_none() { kernel::pr_warn!("free_vfn: VFN {} not found in maple tree\n", vfn_index); } } /// Read the [`Pfn`] for a mapped [`Vfn`] if one is mapped. pub(super) fn read_mapping(&self, mm: &mut GpuMm<'_>, vfn: Vfn) -> Result> { match self.pt_walk.walk_to_pte(mm, vfn)? { WalkResult::Mapped { pfn, .. } => Ok(Some(pfn)), WalkResult::Unmapped { .. } | WalkResult::PageTableMissing => Ok(None), } } /// Prepare resources for mapping `num_pages` pages. /// /// Allocates a contiguous VA range, then walks the hierarchy per-VFN to prepare pages /// for all missing PDEs. Returns a [`PreparedMapping`] with the VA allocation. /// /// If `va_range` is not `None`, the VA range is constrained to the given range. Safe /// to call outside the fence signalling critical path. pub(crate) fn prepare_map( &mut self, mm: &mut GpuMm<'_>, num_pages: usize, va_range: Option>, ) -> Result { if num_pages == 0 { return Err(EINVAL); } // Allocate contiguous VA range. let vfn_start = self.alloc_vfn_range(num_pages, va_range)?; if let Err(e) = self.pt_map.prepare_map( mm, vfn_start, num_pages, &mut self.page_table_allocs, &mut self.pt_pages, ) { self.free_vfn(vfn_start); return Err(e); } Ok(PreparedMapping { vfn_start, num_pages, _drop_guard: MustExecuteGuard::new(), }) } /// Execute a prepared multi-page mapping. /// /// Installs all prepared PDEs and writes PTEs into the page table, then flushes TLB. pub(crate) fn execute_map( &mut self, mm: &mut GpuMm<'_>, prepared: PreparedMapping, pfns: &[Pfn], writable: bool, ) -> Result { if pfns.len() != prepared.num_pages { self.free_vfn(prepared.vfn_start); return Err(EINVAL); } let PreparedMapping { vfn_start, num_pages, _drop_guard, } = prepared; _drop_guard.disarm(); if let Err(e) = self.pt_map.install_mappings( mm, &mut self.pt_pages, &mut self.page_table_allocs, vfn_start, pfns, writable, ) { self.free_vfn(vfn_start); return Err(e); } Ok(MappedRange { vfn_start, num_pages, _drop_guard: MustUnmapGuard::new(), }) } /// Map pages doing prepare and execute in the same call. /// /// This is a convenience wrapper for callers outside the fence signalling critical /// path (e.g., BAR mappings). For DRM usecases, [`Vmm::prepare_map()`] and /// [`Vmm::execute_map()`] will be called separately. pub(crate) fn map_pages( &mut self, mm: &mut GpuMm<'_>, pfns: &[Pfn], va_range: Option>, writable: bool, ) -> Result { if pfns.is_empty() { return Err(EINVAL); } // Check if provided VA range is sufficient (if provided). if let Some(ref range) = va_range { let required: u64 = pfns .len() .checked_mul(PAGE_SIZE) .ok_or(EOVERFLOW)? .into_safe_cast(); let available = range.end.checked_sub(range.start).ok_or(EINVAL)?; if available < required { return Err(EINVAL); } } let prepared = self.prepare_map(mm, pfns.len(), va_range)?; self.execute_map(mm, prepared, pfns, writable) } /// Unmap all pages in a [`MappedRange`] with a single TLB flush. pub(crate) fn unmap_pages(&mut self, mm: &mut GpuMm<'_>, range: MappedRange) -> Result { let result = self .pt_map .invalidate_ptes(mm, range.vfn_start, range.num_pages); // TODO: Internal page table pages (PDE, PTE pages) are still kept around. // This is by design as repeated maps/unmaps will be fast. As a future TODO, // we can add a reclaimer here to reclaim if VRAM is short. For now, the PT // pages are dropped once the `Vmm` is dropped. // Free the VA range regardless of PTE invalidation success, so that the VA // range is recovered even on failure (PTEs may be stale, but that is better // than leaking both PTEs and VA range). self.free_vfn(range.vfn_start); // Unmap complete, safe to drop `MappedRange`. range._drop_guard.disarm(); result } }