@@ -0,0 +1,293 @@+// SPDX-License-Identifier: GPL-2.0++//! Direct VRAM access through the PRAMIN aperture.+//!+//! PRAMIN provides a 1MB sliding window into VRAM through BAR0, allowing the CPU to access+//! video memory directly. Access is managed through a two-level API:+//!+//! - [`Pramin`]: The parent object that owns the BAR0 reference and synchronization lock.+//! - [`PraminWindow`]: A guard object that holds exclusive PRAMIN access for its lifetime.+//!+//! The PRAMIN aperture is a 1MB region at BAR0 + 0x700000 for all GPUs. The window base is+//! controlled by the `NV_PBUS_BAR0_WINDOW` register and must be 64KB aligned.+//!+//! # Examples+//!+//! ## Basic read/write+//!+//! ```no_run+//! use crate::driver::Bar0;+//! use crate::mm::pramin;+//! use kernel::devres::Devres;+//! use kernel::prelude::*;+//! use kernel::sync::Arc;+//!+//! fn example(devres_bar: Arc<Devres<Bar0>>) -> Result<()> {+//! let pramin = Arc::pin_init(pramin::Pramin::new(devres_bar)?, GFP_KERNEL)?;+//! let mut window = pramin.window()?;+//!+//! // Write and read back.+//! window.try_write32(0x100, 0xDEADBEEF)?;+//! let val = window.try_read32(0x100)?;+//! assert_eq!(val, 0xDEADBEEF);+//!+//! Ok(())+//! // Original window position restored on drop.+//! }+//! ```+//!+//! ## Auto-repositioning across VRAM regions+//!+//! ```no_run+//! use crate::driver::Bar0;+//! use crate::mm::pramin;+//! use kernel::devres::Devres;+//! use kernel::prelude::*;+//! use kernel::sync::Arc;+//!+//! fn example(devres_bar: Arc<Devres<Bar0>>) -> Result<()> {+//! let pramin = Arc::pin_init(pramin::Pramin::new(devres_bar)?, GFP_KERNEL)?;+//! let mut window = pramin.window()?;+//!+//! // Access first 1MB region.+//! window.try_write32(0x100, 0x11111111)?;+//!+//! // Access at 2MB - window auto-repositions.+//! window.try_write32(0x200000, 0x22222222)?;+//!+//! // Back to first region - window repositions again.+//! let val = window.try_read32(0x100)?;+//! assert_eq!(val, 0x11111111);+//!+//! Ok(())+//! }+//! ```++#![allow(unused)]++usecrate::{+driver::Bar0,+regs,//+};++usekernel::bits::genmask_u64;+usekernel::devres::Devres;+usekernel::io::Io;+usekernel::new_mutex;+usekernel::prelude::*;+usekernel::ptr::{+Alignable,+Alignment,//+};+usekernel::sizes::{+SZ_1M,+SZ_64K,//+};+usekernel::sync::{+lock::mutex::MutexGuard,+Arc,+Mutex,//+};++/// PRAMIN aperture base offset in BAR0.+constPRAMIN_BASE:usize=0x700000;++/// PRAMIN aperture size (1MB).+constPRAMIN_SIZE:usize=SZ_1M;++/// 64KB alignment for window base.+constWINDOW_ALIGN:Alignment=Alignment::new::<SZ_64K>();++/// Maximum addressable VRAM offset (40-bit address space).+///+/// The `NV_PBUS_BAR0_WINDOW` register has a 24-bit `window_base` field (bits 23:0) that stores+/// bits [39:16] of the target VRAM address. This limits the addressable space to 2^40 bytes.+///+/// CAST: On 64-bit systems, this fits in usize.+constMAX_VRAM_OFFSET:usize=genmask_u64(0..=39)asusize;++/// Generate a PRAMIN read accessor.+macro_rules!define_pramin_read{+($name:ident,$ty:ty)=>{+#[doc = concat!("Read a `", stringify!($ty), "` from VRAM at the given offset.")]+pub(crate)fn$name(&mutself,vram_offset:usize)->Result<$ty>{+// Compute window parameters without bar reference.+let(bar_offset,new_base)=+self.compute_window(vram_offset,::core::mem::size_of::<$ty>())?;++// Update window base if needed and perform read.+letbar=self.bar.try_access().ok_or(ENODEV)?;+ifletSome(base)=new_base{+Self::write_window_base(&bar,base);+self.state.current_base=base;+}+bar.$name(bar_offset)+}+};+}++/// Generate a PRAMIN write accessor.+macro_rules!define_pramin_write{+($name:ident,$ty:ty)=>{+#[doc = concat!("Write a `", stringify!($ty), "` to VRAM at the given offset.")]+pub(crate)fn$name(&mutself,vram_offset:usize,value:$ty)->Result{+// Compute window parameters without bar reference.+let(bar_offset,new_base)=+self.compute_window(vram_offset,::core::mem::size_of::<$ty>())?;++// Update window base if needed and perform write.+letbar=self.bar.try_access().ok_or(ENODEV)?;+ifletSome(base)=new_base{+Self::write_window_base(&bar,base);+self.state.current_base=base;+}+bar.$name(value,bar_offset)+}+};+}++/// PRAMIN state protected by mutex.+structPraminState{+current_base:usize,+}++/// PRAMIN aperture manager.+///+/// Call [`Pramin::window()`] to acquire exclusive PRAMIN access.+#[pin_data]+pub(crate)structPramin{+bar:Arc<Devres<Bar0>>,+/// PRAMIN aperture state, protected by a mutex.+///+/// # Safety+///+/// This lock is acquired during the DMA fence signalling critical path.+/// It must NEVER be held across any reclaimable CPU memory / allocations+/// (`GFP_KERNEL`), because the memory reclaim path can call+/// `dma_fence_wait()`, which would deadlock with this lock held.+#[pin]+state:Mutex<PraminState>,+}++implPramin{+/// Create a pin-initializer for PRAMIN.+pub(crate)fnnew(bar:Arc<Devres<Bar0>>)->Result<implPinInit<Self>>{+letbar_access=bar.try_access().ok_or(ENODEV)?;+letcurrent_base=Self::try_read_window_base(&bar_access)?;++Ok(pin_init!(Self{+bar,+state<-new_mutex!(PraminState{current_base},"pramin_state"),+}))+}++/// Acquire exclusive PRAMIN access.+///+/// Returns a [`PraminWindow`] guard that provides VRAM read/write accessors.+/// The [`PraminWindow`] is exclusive and only one can exist at a time.+pub(crate)fnwindow(&self)->Result<PraminWindow<'_>>{+letstate=self.state.lock();+letsaved_base=state.current_base;+Ok(PraminWindow{+bar:self.bar.clone(),+state,+saved_base,+})+}++/// Read the current window base from the BAR0_WINDOW register.+fntry_read_window_base(bar:&Bar0)->Result<usize>{+letreg=regs::NV_PBUS_BAR0_WINDOW::read(bar);+letbase=u64::from(reg.window_base());+letshifted=base.checked_shl(16).ok_or(EOVERFLOW)?;+shifted.try_into().map_err(|_|EOVERFLOW)+}+}++/// PRAMIN window guard for direct VRAM access.+///+/// This guard holds exclusive access to the PRAMIN aperture. The window auto-repositions+/// when accessing VRAM offsets outside the current 1MB range. Original window position+/// is saved on creation and restored on drop.+///+/// Only one [`PraminWindow`] can exist at a time per [`Pramin`] instance (enforced by the+/// internal `MutexGuard`).+pub(crate)structPraminWindow<'a>{+bar:Arc<Devres<Bar0>>,+state:MutexGuard<'a,PraminState>,+saved_base:usize,+}++implPraminWindow<'_>{+/// Write a new window base to the BAR0_WINDOW register.+fnwrite_window_base(bar:&Bar0,base:usize){+// CAST:+// - We have guaranteed that the base is within the addressable range (40-bits).+// - After >> 16, a 40-bit aligned base becomes 24 bits, which fits in u32.+regs::NV_PBUS_BAR0_WINDOW::default()+.set_window_base((base>>16)asu32)+.write(bar);+}++/// Compute window parameters for a VRAM access.+///+/// Returns (`bar_offset`, `new_base`) where:+/// - `bar_offset`: The BAR0 offset to use for the access.+/// - `new_base`: `Some(base)` if window needs repositioning, `None` otherwise.+fncompute_window(+&self,+vram_offset:usize,+access_size:usize,+)->Result<(usize,Option<usize>)>{+// Validate VRAM offset is within addressable range (40-bit address space).+letend_offset=vram_offset.checked_add(access_size).ok_or(EINVAL)?;+ifend_offset>MAX_VRAM_OFFSET+1{+returnErr(EINVAL);+}++// Calculate which 64KB-aligned base we need.+letneeded_base=vram_offset.align_down(WINDOW_ALIGN);++// Calculate offset within the window.+letoffset_in_window=vram_offset-needed_base;++// Check if access fits in 1MB window from this base.+ifoffset_in_window+access_size>PRAMIN_SIZE{+returnErr(EINVAL);+}++// Return bar offset and whether window needs repositioning.+letnew_base=ifself.state.current_base!=needed_base{+Some(needed_base)+}else{+None+};++Ok((PRAMIN_BASE+offset_in_window,new_base))+}++define_pramin_read!(try_read8,u8);+define_pramin_read!(try_read16,u16);+define_pramin_read!(try_read32,u32);+define_pramin_read!(try_read64,u64);++define_pramin_write!(try_write8,u8);+define_pramin_write!(try_write16,u16);+define_pramin_write!(try_write32,u32);+define_pramin_write!(try_write64,u64);+}++implDropforPraminWindow<'_>{+fndrop(&mutself){+// Restore the original window base if it changed.+ifself.state.current_base!=self.saved_base{+ifletSome(bar)=self.bar.try_access(){+Self::write_window_base(&bar,self.saved_base);++// Update state to reflect the restored base.+self.state.current_base=self.saved_base;+}+}+// MutexGuard drops automatically, releasing the lock.+}+}
@@ -102,6 +102,11 @@ fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {31:16frts_err_codeasu16;});+register!(NV_PBUS_BAR0_WINDOW@0x00001700,"BAR0 window control for PRAMIN access"{+25:24targetasu8,"Target memory (0=VRAM, 1=SYS_MEM_COH, 2=SYS_MEM_NONCOH)";+23:0window_baseasu32,"Window base address (bits 39:16 of FB addr)";+});+// PFB// The following two registers together hold the physical system memory address that is used by the
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:11
Add documentation for the PRAMIN aperture mechanism used by nova-core
for direct VRAM access.
Nova only uses TARGET=VID_MEM for VRAM access. The SYS_MEM target values
are documented for completeness but not used by the driver.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
Documentation/gpu/nova/core/pramin.rst | 125 +++++++++++++++++++++++++
Documentation/gpu/nova/index.rst | 1 +
2 files changed, 126 insertions(+)
create mode 100644 Documentation/gpu/nova/core/pramin.rst
@@ -0,0 +1,125 @@+.. SPDX-License-Identifier: GPL-2.0++=========================+PRAMIN aperture mechanism+=========================++..note::+ The following description is approximate and current as of the Ampere family.+ It may change for future generations and is intended to assist in understanding+ the driver code.++Introduction+============++PRAMIN is a hardware aperture mechanism that provides CPU access to GPU Video RAM (VRAM) before+the GPU's Memory Management Unit (MMU) and page tables are initialized. This 1MB sliding window,+located at a fixed offset within BAR0, is essential for setting up page tables and other critical+GPU data structures without relying on the GPU's MMU.++Architecture Overview+=====================++The PRAMIN aperture mechanism is logically implemented by the GPU's PBUS (PCIe Bus Controller Unit)+and provides a CPU-accessible window into VRAM through the PCIe interface::++ +-----------------+ PCIe +------------------------------++| CPU |<----------->| GPU |+ +-----------------+ | |+| +----------------------+ |+| | PBUS | |+| | (Bus Controller) | |+| | | |+| | +--------------+<------------ (window starts at+| | | PRAMIN | | | BAR0 + 0x700000)+| | | Window | | |+| | | (1MB) | | |+| | +--------------+ | |+| | | | |+| +---------|------------+ |+| | |+| v |+| +----------------------+<------------ (Program PRAMIN to any+| | VRAM | | 64KB-aligned VRAM boundary)+| | (Several GBs) | |+| | | |+| | FB[0x000000000000] | |+| | ... | |+| | FB[0x7FFFFFFFFFF] | |+| +----------------------+ |+ +------------------------------+++PBUS (PCIe Bus Controller) is responsible for, among other things, handling MMIO+accesses to the BAR registers.++PRAMIN Window Operation+=======================++The PRAMIN window provides a 1MB sliding aperture that can be repositioned over+the entire VRAM address space using the ``NV_PBUS_BAR0_WINDOW`` register.++Window Control Mechanism+-------------------------++The window position is controlled via the PBUS ``BAR0_WINDOW`` register::++ NV_PBUS_BAR0_WINDOW Register (0x1700):+ +-------+--------+--------------------------------------++| 31:26 | 25:24 | 23:0 |+| RSVD | TARGET | BASE_ADDR |+| | | (bits 39:16 of VRAM address) |+ +-------+--------+--------------------------------------+++ BASE_ADDR field (bits 23:0):+- Contains bits [39:16] of the target VRAM address+- Provides 40-bit (1TB) address space coverage+- Must be programmed with 64KB-aligned addresses++ TARGET field (bits 25:24):+- 0x0: VRAM (Video Memory)+- 0x1: SYS_MEM_COH (Coherent System Memory)+- 0x2: SYS_MEM_NONCOH (Non-coherent System Memory)+- 0x3: Reserved++ ..note::+ Nova only uses TARGET=VRAM (0x0) for video memory access. The SYS_MEM+ target values are documented here for hardware completeness but are+ not used by the driver.++64KB Alignment Requirement+---------------------------++The PRAMIN window must be aligned to 64KB boundaries in VRAM. This is enforced+by the ``BASE_ADDR`` field representing bits [39:16] of the target address::++ VRAM Address Calculation:+ actual_vram_addr = (BASE_ADDR << 16) + pramin_offset+ Where:+- BASE_ADDR: 24-bit value from NV_PBUS_BAR0_WINDOW[23:0]+- pramin_offset: 20-bit offset within the PRAMIN window [0x00000-0xFFFFF]++ Example Window Positioning:+ +---------------------------------------------------------++| VRAM Space |+| |+| 0x000000000 +-----------------+ <-- 64KB aligned |+| | PRAMIN Window | |+| | (1MB) | |+| 0x0000FFFFF +-----------------+ |+| |+| | ^ |+| | | Window can slide |+| v | to any 64KB-aligned boundary |+| |+| 0x123400000 +-----------------+ <-- 64KB aligned |+| | PRAMIN Window | |+| | (1MB) | |+| 0x1234FFFFF +-----------------+ |+| |+| ... |+| |+| 0x7FFFF0000 +-----------------+ <-- 64KB aligned |+| | PRAMIN Window | |+| | (1MB) | |+| 0x7FFFFFFFF +-----------------+ |+ +---------------------------------------------------------+
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:13
Add BAR1_SIZE constant and Bar1 type alias for the 256MB BAR1 aperture.
These are prerequisites for BAR1 memory access functionality.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/driver.rs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
@@ -28,6 +31,7 @@ pub(crate) struct NovaCore {}constBAR0_SIZE:usize=SZ_16M;+pub(crate)constBAR1_SIZE:usize=SZ_256M;// For now we only support Ampere which can use up to 47-bit DMA addresses.//
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:14
Add accessor methods to GspStaticConfigInfo for retrieving the BAR1 Page
Directory Entry base addresses from GSP-RM firmware.
These addresses point to the root page tables for BAR1 virtual memory spaces.
The page tables are set up by GSP-RM during GPU initialization.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/driver.rs | 1 +
drivers/gpu/nova-core/gsp/commands.rs | 8 ++++++++
drivers/gpu/nova-core/gsp/fw/commands.rs | 8 ++++++++
3 files changed, 17 insertions(+)
@@ -189,6 +189,7 @@ fn init(&self) -> impl Init<Self::Command, Self::InitError> {/// The reply from the GSP to the [`GetGspInfo`] command.pub(crate)structGetGspStaticInfoReply{gpu_name:[u8;64],+bar1_pde_base:u64,}implMessageFromGspforGetGspStaticInfoReply{
@@ -228,6 +230,12 @@ pub(crate) fn gpu_name(&self) -> core::result::Result<&str, GpuNameError> {.to_str().map_err(GpuNameError::InvalidUtf8)}++/// Returns the BAR1 Page Directory Entry base address.+#[expect(dead_code)]+pub(crate)fnbar1_pde_base(&self)->u64{+self.bar1_pde_base+}}/// Send the [`GetGspInfo`] command and awaits for its reply.
@@ -114,6 +114,14 @@ impl GspStaticConfigInfo {pub(crate)fngpu_name_str(&self)->[u8;64]{self.0.gpuNameString}++/// Returns the BAR1 Page Directory Entry base address.+///+/// This is the root page table address for BAR1 virtual memory,+/// set up by GSP-RM firmware.+pub(crate)fnbar1_pde_base(&self)->u64{+self.0.bar1PdeBase+}}// SAFETY: Padding is explicit and will not contain uninitialized data.
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:16
Add foundational types for GPU memory management. These types are used
throughout the nova memory management subsystem for page table
operations, address translation, and memory allocation.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/mod.rs | 171 ++++++++++++++++++++++++++++++++
1 file changed, 171 insertions(+)
@@ -2,4 +2,175 @@//! Memory management subsystems for nova-core.+#![expect(dead_code)]+pub(crate)modpramin;++usekernel::sizes::SZ_4K;++/// Page size in bytes (4 KiB).+pub(crate)constPAGE_SIZE:usize=SZ_4K;++bitfield!{+pub(crate)structVramAddress(u64),"Physical VRAM address in GPU video memory"{+11:0offsetasu64,"Offset within 4KB page";+63:12frame_numberasu64=>Pfn,"Physical frame number";+}+}++implVramAddress{+/// Create a new VRAM address from a raw value.+pub(crate)constfnnew(addr:u64)->Self{+Self(addr)+}++/// Get the raw address value as `usize` (useful for MMIO offsets).+pub(crate)constfnraw(&self)->usize{+self.0asusize+}++/// Get the raw address value as `u64`.+pub(crate)constfnraw_u64(&self)->u64{+self.0+}+}++implPartialEqforVramAddress{+fneq(&self,other:&Self)->bool{+self.0==other.0+}+}++implEqforVramAddress{}++implPartialOrdforVramAddress{+fnpartial_cmp(&self,other:&Self)->Option<core::cmp::Ordering>{+Some(self.cmp(other))+}+}++implOrdforVramAddress{+fncmp(&self,other:&Self)->core::cmp::Ordering{+self.0.cmp(&other.0)+}+}++implFrom<Pfn>forVramAddress{+fnfrom(pfn:Pfn)->Self{+Self::default().set_frame_number(pfn)+}+}++bitfield!{+pub(crate)structVirtualAddress(u64),"Virtual address in GPU address space"{+11:0offsetasu64,"Offset within 4KB page";+20:12l4_indexasu64,"Level 4 index (PTE)";+29:21l3_indexasu64,"Level 3 index (Dual PDE)";+38:30l2_indexasu64,"Level 2 index";+47:39l1_indexasu64,"Level 1 index";+56:48l0_indexasu64,"Level 0 index (PDB)";+63:12frame_numberasu64=>Vfn,"Virtual frame number";+}+}++implVirtualAddress{+/// Create a new virtual address from a raw value.+#[expect(dead_code)]+pub(crate)constfnnew(addr:u64)->Self{+Self(addr)+}++/// Get the page table index for a given level (0-5).+pub(crate)fnlevel_index(&self,level:u64)->u64{+matchlevel{+0=>self.l0_index(),+1=>self.l1_index(),+2=>self.l2_index(),+3=>self.l3_index(),+4=>self.l4_index(),+// L5 is only used by MMU v3 (PTE level).+5=>self.l4_index(),+_=>0,+}+}+}++implFrom<Vfn>forVirtualAddress{+fnfrom(vfn:Vfn)->Self{+Self::default().set_frame_number(vfn)+}+}++/// Physical Frame Number.+///+/// Represents a physical page in VRAM.+#[repr(transparent)]+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]+pub(crate)structPfn(u64);++implPfn{+/// Create a new PFN from a frame number.+pub(crate)constfnnew(frame_number:u64)->Self{+Self(frame_number)+}++/// Get the raw frame number.+pub(crate)constfnraw(self)->u64{+self.0+}+}++implFrom<VramAddress>forPfn{+fnfrom(addr:VramAddress)->Self{+addr.frame_number()+}+}++implFrom<u64>forPfn{+fnfrom(val:u64)->Self{+Self(val)+}+}++implFrom<Pfn>foru64{+fnfrom(pfn:Pfn)->Self{+pfn.0+}+}++/// Virtual Frame Number.+///+/// Represents a virtual page in GPU address space.+#[repr(transparent)]+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]+pub(crate)structVfn(u64);++implVfn{+/// Create a new VFN from a frame number.+pub(crate)constfnnew(frame_number:u64)->Self{+Self(frame_number)+}++/// Get the raw frame number.+pub(crate)constfnraw(self)->u64{+self.0+}+}++implFrom<VirtualAddress>forVfn{+fnfrom(addr:VirtualAddress)->Self{+addr.frame_number()+}+}++implFrom<u64>forVfn{+fnfrom(val:u64)->Self{+Self(val)+}+}++implFrom<Vfn>foru64{+fnfrom(vfn:Vfn)->Self{+vfn.0+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:18
Add common page table types shared between MMU v2 and v3. These types
are hardware-agnostic and used by both MMU versions.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/mod.rs | 1 +
drivers/gpu/nova-core/mm/pagetable/mod.rs | 149 ++++++++++++++++++++++
2 files changed, 150 insertions(+)
create mode 100644 drivers/gpu/nova-core/mm/pagetable/mod.rs
@@ -0,0 +1,149 @@+// SPDX-License-Identifier: GPL-2.0++//! Common page table types shared between MMU v2 and v3.+//!+//! This module provides foundational types used by both MMU versions:+//! - Page table level hierarchy+//! - Memory aperture types for PDEs and PTEs++#![expect(dead_code)]++usecrate::gpu::Architecture;++/// MMU version enumeration.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub(crate)enumMmuVersion{+/// MMU v2 for Turing/Ampere/Ada.+V2,+/// MMU v3 for Hopper and later.+V3,+}++implFrom<Architecture>forMmuVersion{+fnfrom(arch:Architecture)->Self{+matcharch{+Architecture::Turing|Architecture::Ampere|Architecture::Ada=>Self::V2,+// In the future, uncomment:+// _ => Self::V3,+}+}+}++/// Page Table Level hierarchy for MMU v2/v3.+#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub(crate)enumPageTableLevel{+/// Level 0 - Page Directory Base (root).+Pdb,+/// Level 1 - Intermediate page directory.+L1,+/// Level 2 - Intermediate page directory.+L2,+/// Level 3 - Intermediate page directory or dual PDE (version-dependent).+L3,+/// Level 4 - PTE level for v2, intermediate page directory for v3.+L4,+/// Level 5 - PTE level used for MMU v3 only.+L5,+}++implPageTableLevel{+/// Number of entries per page table (512 for 4KB pages).+pub(crate)constENTRIES_PER_TABLE:usize=512;++/// Get the next level in the hierarchy.+pub(crate)constfnnext(&self)->Option<PageTableLevel>{+matchself{+Self::Pdb=>Some(Self::L1),+Self::L1=>Some(Self::L2),+Self::L2=>Some(Self::L3),+Self::L3=>Some(Self::L4),+Self::L4=>Some(Self::L5),+Self::L5=>None,+}+}++/// Convert level to index.+pub(crate)constfnas_index(&self)->u64{+matchself{+Self::Pdb=>0,+Self::L1=>1,+Self::L2=>2,+Self::L3=>3,+Self::L4=>4,+Self::L5=>5,+}+}+}++/// Memory aperture for Page Table Entries (`PTE`s).+///+/// Determines which memory region the `PTE` points to.+#[repr(u8)]+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]+pub(crate)enumAperturePte{+/// Local video memory (VRAM).+#[default]+VideoMemory=0,+/// Peer GPU's video memory.+PeerMemory=1,+/// System memory with cache coherence.+SystemCoherent=2,+/// System memory without cache coherence.+SystemNonCoherent=3,+}++// TODO[FPRI]: Replace with `#[derive(FromPrimitive)]` when available.+implFrom<u8>forAperturePte{+fnfrom(val:u8)->Self{+matchval{+0=>Self::VideoMemory,+1=>Self::PeerMemory,+2=>Self::SystemCoherent,+3=>Self::SystemNonCoherent,+_=>Self::VideoMemory,+}+}+}++// TODO[FPRI]: Replace with `#[derive(ToPrimitive)]` when available.+implFrom<AperturePte>foru8{+fnfrom(val:AperturePte)->Self{+valasu8+}+}++/// Memory aperture for Page Directory Entries (`PDE`s).+///+/// Note: For `PDE`s, `Invalid` (0) means the entry is not valid.+#[repr(u8)]+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]+pub(crate)enumAperturePde{+/// Invalid/unused entry.+#[default]+Invalid=0,+/// Page table is in video memory.+VideoMemory=1,+/// Page table is in system memory with coherence.+SystemCoherent=2,+/// Page table is in system memory without coherence.+SystemNonCoherent=3,+}++// TODO[FPRI]: Replace with `#[derive(FromPrimitive)]` when available.+implFrom<u8>forAperturePde{+fnfrom(val:u8)->Self{+matchval{+1=>Self::VideoMemory,+2=>Self::SystemCoherent,+3=>Self::SystemNonCoherent,+_=>Self::Invalid,+}+}+}++// TODO[FPRI]: Replace with `#[derive(ToPrimitive)]` when available.+implFrom<AperturePde>foru8{+fnfrom(val:AperturePde)->Self{+valasu8+}+}
@@ -0,0 +1,199 @@+// SPDX-License-Identifier: GPL-2.0++//! MMU v2 page table types for Turing and Ampere GPUs.+//!+//! This module defines MMU version 2 specific types (Turing, Ampere and Ada GPUs).+//!+//! Bit field layouts derived from the NVIDIA OpenRM documentation:+//! `open-gpu-kernel-modules/src/common/inc/swref/published/turing/tu102/dev_mmu.h`++#![expect(dead_code)]++usesuper::{+AperturePde,+AperturePte,+PageTableLevel,//+};+usecrate::mm::{+Pfn,+VramAddress,//+};++/// PDE levels for MMU v2 (5-level hierarchy: PDB -> L1 -> L2 -> L3 -> L4).+pub(crate)constPDE_LEVELS:&[PageTableLevel]=&[+PageTableLevel::Pdb,+PageTableLevel::L1,+PageTableLevel::L2,+PageTableLevel::L3,+];++/// PTE level for MMU v2.+pub(crate)constPTE_LEVEL:PageTableLevel=PageTableLevel::L4;++/// Dual PDE level for MMU v2 (128-bit entries).+pub(crate)constDUAL_PDE_LEVEL:PageTableLevel=PageTableLevel::L3;++// Page Table Entry (PTE) for MMU v2 - 64-bit entry at level 4.+bitfield!{+pub(crate)structPte(u64),"Page Table Entry for MMU v2"{+0:0validasbool,"Entry is valid";+2:1apertureasu8=>AperturePte,"Memory aperture type";+3:3volatileasbool,"Volatile (bypass L2 cache)";+4:4encryptedasbool,"Encryption enabled (Confidential Computing)";+5:5privilegeasbool,"Privileged access only";+6:6read_onlyasbool,"Write protection";+7:7atomic_disableasbool,"Atomic operations disabled";+53:8frame_number_sysasu64=>Pfn,"Frame number for system memory";+32:8frame_number_vidasu64=>Pfn,"Frame number for video memory";+35:33peer_idasu8,"Peer GPU ID for peer memory (0-7)";+53:36comptaglineasu32,"Compression tag line bits";+63:56kindasu8,"Surface kind/format";+}+}++implPte{+/// Create a PTE from a `u64` value.+pub(crate)fnnew(val:u64)->Self{+Self(val)+}++/// Create a valid PTE for video memory.+pub(crate)fnnew_vram(pfn:Pfn,writable:bool)->Self{+Self::default()+.set_valid(true)+.set_aperture(AperturePte::VideoMemory)+.set_frame_number_vid(pfn)+.set_read_only(!writable)+}++/// Create an invalid PTE.+pub(crate)fninvalid()->Self{+Self::default()+}++/// Get the frame number based on aperture type.+pub(crate)fnframe_number(&self)->Pfn{+matchself.aperture(){+AperturePte::VideoMemory=>self.frame_number_vid(),+_=>self.frame_number_sys(),+}+}++/// Get the raw `u64` value.+pub(crate)fnraw_u64(&self)->u64{+self.0+}+}++// Page Directory Entry (PDE) for MMU v2 - 64-bit entry at levels 0-2.+bitfield!{+pub(crate)structPde(u64),"Page Directory Entry for MMU v2"{+0:0valid_invertedasbool,"Valid bit (inverted logic)";+2:1apertureasu8=>AperturePde,"Memory aperture type";+3:3volatileasbool,"Volatile (bypass L2 cache)";+5:5no_atsasbool,"Disable Address Translation Services";+53:8table_frame_sysasu64=>Pfn,"Table frame number for system memory";+32:8table_frame_vidasu64=>Pfn,"Table frame number for video memory";+35:33peer_idasu8,"Peer GPU ID (0-7)";+}+}++implPde{+/// Create a PDE from a `u64` value.+pub(crate)fnnew(val:u64)->Self{+Self(val)+}++/// Create a valid PDE pointing to a page table in video memory.+pub(crate)fnnew_vram(table_pfn:Pfn)->Self{+Self::default()+.set_valid_inverted(false)// 0 = valid+.set_aperture(AperturePde::VideoMemory)+.set_table_frame_vid(table_pfn)+}++/// Create an invalid PDE.+pub(crate)fninvalid()->Self{+Self::default()+.set_valid_inverted(true)+.set_aperture(AperturePde::Invalid)+}++/// Check if this PDE is valid.+pub(crate)fnis_valid(&self)->bool{+!self.valid_inverted()&&self.aperture()!=AperturePde::Invalid+}++/// Get the table frame number based on aperture type.+pub(crate)fntable_frame(&self)->Pfn{+matchself.aperture(){+AperturePde::VideoMemory=>self.table_frame_vid(),+_=>self.table_frame_sys(),+}+}++/// Get the VRAM address of the page table.+pub(crate)fntable_vram_address(&self)->VramAddress{+debug_assert!(+self.aperture()==AperturePde::VideoMemory,+"table_vram_address called on non-VRAM PDE (aperture: {:?})",+self.aperture()+);+VramAddress::from(self.table_frame_vid())+}++/// Get the raw `u64` value of the PDE.+pub(crate)fnraw_u64(&self)->u64{+self.0+}+}++/// Dual PDE at Level 3 - 128-bit entry of Large/Small Page Table pointers.+///+/// The dual PDE supports both large (64KB) and small (4KB) page tables.+#[repr(C)]+#[derive(Debug, Clone, Copy, Default)]+pub(crate)structDualPde{+/// Large/Big Page Table pointer (lower 64 bits).+pubbig:Pde,+/// Small Page Table pointer (upper 64 bits).+pubsmall:Pde,+}++implDualPde{+/// Create a dual PDE from raw 128-bit value (two `u64`s).+pub(crate)fnnew(big:u64,small:u64)->Self{+Self{+big:Pde::new(big),+small:Pde::new(small),+}+}++/// Create a dual PDE with only the small page table pointer set.+///+/// Note: The big (LPT) portion is set to 0, not `Pde::invalid()`.+/// According to hardware documentation, clearing bit 0 of the 128-bit+/// entry makes the PDE behave as a "normal" PDE. Using `Pde::invalid()`+/// would set bit 0 (valid_inverted), which breaks page table walking.+pub(crate)fnnew_small(table_pfn:Pfn)->Self{+Self{+big:Pde::new(0),+small:Pde::new_vram(table_pfn),+}+}++/// Check if the small page table pointer is valid.+pub(crate)fnhas_small(&self)->bool{+self.small.is_valid()+}++/// Check if the big page table pointer is valid.+pub(crate)fnhas_big(&self)->bool{+self.big.is_valid()+}++/// Get the small page table PFN.+pub(crate)fnsmall_pfn(&self)->Pfn{+self.small.table_frame()+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:29
Add page table entry and directory structures for MMU version 3
used by Hopper and later GPUs.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/pagetable/mod.rs | 1 +
drivers/gpu/nova-core/mm/pagetable/ver3.rs | 302 +++++++++++++++++++++
2 files changed, 303 insertions(+)
create mode 100644 drivers/gpu/nova-core/mm/pagetable/ver3.rs
@@ -0,0 +1,302 @@+// SPDX-License-Identifier: GPL-2.0++//! MMU v3 page table types for Hopper and later GPUs.+//!+//! This module defines MMU version 3 specific types (Hopper and later GPUs).+//!+//! Key differences from MMU v2:+//! - Unified 40-bit address field for all apertures (v2 had separate sys/vid fields).+//! - PCF (Page Classification Field) replaces separate privilege/RO/atomic/cache bits.+//! - KIND field is 4 bits (not 8).+//! - IS_PTE bit in PDE to support large pages directly.+//! - No COMPTAGLINE field (compression handled differently in v3).+//! - No separate ENCRYPTED bit.+//!+//! Bit field layouts derived from the NVIDIA OpenRM documentation:+//! `open-gpu-kernel-modules/src/common/inc/swref/published/hopper/gh100/dev_mmu.h`++#![expect(dead_code)]++usesuper::{+AperturePde,+AperturePte,+PageTableLevel,//+};+usecrate::mm::{+Pfn,+VramAddress,//+};+usekernel::prelude::*;++/// PDE levels for MMU v3 (6-level hierarchy).+pub(crate)constPDE_LEVELS:&[PageTableLevel]=&[+PageTableLevel::Pdb,+PageTableLevel::L1,+PageTableLevel::L2,+PageTableLevel::L3,+PageTableLevel::L4,+];++/// PTE level for MMU v3.+pub(crate)constPTE_LEVEL:PageTableLevel=PageTableLevel::L5;++/// Dual PDE level for MMU v3 (128-bit entries).+pub(crate)constDUAL_PDE_LEVEL:PageTableLevel=PageTableLevel::L4;++// Page Classification Field (PCF) - 5 bits for PTEs in MMU v3.+bitfield!{+pub(crate)structPtePcf(u8),"Page Classification Field for PTEs"{+0:0uncachedasbool,"Bypass L2 cache (0=cached, 1=bypass)";+1:1acdasbool,"Access counting disabled (0=enabled, 1=disabled)";+2:2read_onlyasbool,"Read-only access (0=read-write, 1=read-only)";+3:3no_atomicasbool,"Atomics disabled (0=enabled, 1=disabled)";+4:4privilegedasbool,"Privileged access only (0=regular, 1=privileged)";+}+}++implPtePcf{+/// Create PCF for read-write mapping (cached, no atomics, regular mode).+pub(crate)fnrw()->Self{+Self::default().set_no_atomic(true)+}++/// Create PCF for read-only mapping (cached, no atomics, regular mode).+pub(crate)fnro()->Self{+Self::default().set_read_only(true).set_no_atomic(true)+}++/// Get the raw `u8` value.+pub(crate)fnraw_u8(&self)->u8{+self.0+}+}++implFrom<u8>forPtePcf{+fnfrom(val:u8)->Self{+Self(val)+}+}++// Page Classification Field (PCF) - 3 bits for PDEs in MMU v3.+// Controls Address Translation Services (ATS) and caching.+bitfield!{+pub(crate)structPdePcf(u8),"Page Classification Field for PDEs"{+0:0uncachedasbool,"Bypass L2 cache (0=cached, 1=bypass)";+1:1no_atsasbool,"Address Translation Services disabled (0=enabled, 1=disabled)";+}+}++implPdePcf{+/// Create PCF for cached mapping with ATS enabled (default).+pub(crate)fncached()->Self{+Self::default()+}++/// Get the raw `u8` value.+pub(crate)fnraw_u8(&self)->u8{+self.0+}+}++implFrom<u8>forPdePcf{+fnfrom(val:u8)->Self{+Self(val)+}+}++// Page Table Entry (PTE) for MMU v3.+bitfield!{+pub(crate)structPte(u64),"Page Table Entry for MMU v3"{+0:0validasbool,"Entry is valid";+2:1apertureasu8=>AperturePte,"Memory aperture type";+7:3pcfasu8=>PtePcf,"Page Classification Field";+11:8kindasu8,"Surface kind (4 bits, 0x0=pitch, 0xF=invalid)";+51:12frame_numberasu64=>Pfn,"Physical frame number (for all apertures)";+63:61peer_idasu8,"Peer GPU ID for peer memory (0-7)";+}+}++implPte{+/// Create a PTE from a `u64` value.+pub(crate)fnnew(val:u64)->Self{+Self(val)+}++/// Create a valid PTE for video memory.+pub(crate)fnnew_vram(frame:Pfn,writable:bool)->Self{+letpcf=ifwritable{PtePcf::rw()}else{PtePcf::ro()};+Self::default()+.set_valid(true)+.set_aperture(AperturePte::VideoMemory)+.set_pcf(pcf)+.set_frame_number(frame)+}++/// Create an invalid PTE.+pub(crate)fninvalid()->Self{+Self::default()+}++/// Get the raw `u64` value.+pub(crate)fnraw_u64(&self)->u64{+self.0+}+}++// Page Directory Entry (PDE) for MMU v3.+//+// Note: v3 uses a unified 40-bit address field (v2 had separate sys/vid address fields).+bitfield!{+pub(crate)structPde(u64),"Page Directory Entry for MMU v3 (Hopper+)"{+0:0is_pteasbool,"Entry is a PTE (0=PDE, 1=large page PTE)";+2:1apertureasu8=>AperturePde,"Memory aperture (0=invalid, 1=vidmem, 2=coherent, 3=non-coherent)";+5:3pcfasu8=>PdePcf,"Page Classification Field (3 bits for PDE)";+51:12table_frameasu64=>Pfn,"Table frame number (40-bit unified address)";+}+}++implPde{+/// Create a PDE from a `u64` value.+pub(crate)fnnew(val:u64)->Self{+Self(val)+}++/// Create a valid PDE pointing to a page table in video memory.+pub(crate)fnnew_vram(table_pfn:Pfn)->Self{+Self::default()+.set_is_pte(false)+.set_aperture(AperturePde::VideoMemory)+.set_table_frame(table_pfn)+}++/// Create an invalid PDE.+pub(crate)fninvalid()->Self{+Self::default().set_aperture(AperturePde::Invalid)+}++/// Check if this PDE is valid.+pub(crate)fnis_valid(&self)->bool{+self.aperture()!=AperturePde::Invalid+}++/// Get the VRAM address of the page table.+pub(crate)fntable_vram_address(&self)->VramAddress{+debug_assert!(+self.aperture()==AperturePde::VideoMemory,+"table_vram_address called on non-VRAM PDE (aperture: {:?})",+self.aperture()+);+VramAddress::from(self.table_frame())+}++/// Get the raw `u64` value.+pub(crate)fnraw_u64(&self)->u64{+self.0+}+}++// Big Page Table pointer for Dual PDE - 64-bit lower word of the 128-bit Dual PDE.+bitfield!{+pub(crate)structDualPdeBig(u64),"Big Page Table pointer in Dual PDE (MMU v3)"{+0:0is_pteasbool,"Entry is a PTE (for large pages)";+2:1apertureasu8=>AperturePde,"Memory aperture type";+5:3pcfasu8=>PdePcf,"Page Classification Field";+51:8table_frameasu64,"Table frame (table address 256-byte aligned)";+}+}++implDualPdeBig{+/// Create a big page table pointer from a `u64` value.+pub(crate)fnnew(val:u64)->Self{+Self(val)+}++/// Create an invalid big page table pointer.+pub(crate)fninvalid()->Self{+Self::default().set_aperture(AperturePde::Invalid)+}++/// Create a valid big PDE pointing to a page table in video memory.+pub(crate)fnnew_vram(table_addr:VramAddress)->Result<Self>{+// Big page table addresses must be 256-byte aligned (shift 8).+iftable_addr.raw_u64()&0xFF!=0{+returnErr(EINVAL);+}++lettable_frame=table_addr.raw_u64()>>8;+Ok(Self::default()+.set_is_pte(false)+.set_aperture(AperturePde::VideoMemory)+.set_table_frame(table_frame))+}++/// Check if this big PDE is valid.+pub(crate)fnis_valid(&self)->bool{+self.aperture()!=AperturePde::Invalid+}++/// Get the VRAM address of the big page table.+pub(crate)fntable_vram_address(&self)->VramAddress{+debug_assert!(+self.aperture()==AperturePde::VideoMemory,+"table_vram_address called on non-VRAM DualPdeBig (aperture: {:?})",+self.aperture()+);+VramAddress::new(self.table_frame()<<8)+}++/// Get the raw `u64` value.+pub(crate)fnraw_u64(&self)->u64{+self.0+}+}++/// Dual PDE at Level 4 for MMU v3 - 128-bit entry.+///+/// Contains both big (64KB) and small (4KB) page table pointers:+/// - Lower 64 bits: Big Page Table pointer.+/// - Upper 64 bits: Small Page Table pointer.+///+/// ## Note+///+/// The big and small page table pointers have different address layouts:+/// - Big address = field value << 8 (256-byte alignment).+/// - Small address = field value << 12 (4KB alignment).+///+/// This is why `DualPdeBig` is a separate type from `Pde`.+#[repr(C)]+#[derive(Debug, Clone, Copy, Default)]+pub(crate)structDualPde{+/// Big Page Table pointer.+pubbig:DualPdeBig,+/// Small Page Table pointer.+pubsmall:Pde,+}++implDualPde{+/// Create a dual PDE from raw 128-bit value (two `u64`s).+pub(crate)fnnew(big:u64,small:u64)->Self{+Self{+big:DualPdeBig::new(big),+small:Pde::new(small),+}+}++/// Create a dual PDE with only the small page table pointer set.+pub(crate)fnnew_small(table_pfn:Pfn)->Self{+Self{+big:DualPdeBig::invalid(),+small:Pde::new_vram(table_pfn),+}+}++/// Check if the small page table pointer is valid.+pub(crate)fnhas_small(&self)->bool{+self.small.is_valid()+}++/// Check if the big page table pointer is valid.+pub(crate)fnhas_big(&self)->bool{+self.big.is_valid()+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:30
Add unified Pte, Pde, and DualPde wrapper enums that abstract over
MMU v2 and v3 page table entry formats. These enums allow the page
table walker and VMM to work with both MMU versions.
Each unified type:
- Takes MmuVersion parameter in constructors
- Wraps both ver2 and ver3 variants
- Delegates method calls to the appropriate variant
This enables version-agnostic page table operations while keeping
version-specific implementation details encapsulated in the ver2
and ver3 modules.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/pagetable/mod.rs | 327 ++++++++++++++++++++++
1 file changed, 327 insertions(+)
@@ -10,6 +10,14 @@pub(crate)modver2;pub(crate)modver3;+usekernel::prelude::*;++usesuper::{+pramin,+Pfn,+VramAddress,+PAGE_SIZE,//+};usecrate::gpu::Architecture;/// MMU version enumeration.
@@ -77,6 +85,76 @@ pub(crate) const fn as_index(&self) -> u64 {}}+implMmuVersion{+/// Get the `PDE` levels (excluding PTE level) for page table walking.+pub(crate)fnpde_levels(&self)->&'static[PageTableLevel]{+matchself{+Self::V2=>ver2::PDE_LEVELS,+Self::V3=>ver3::PDE_LEVELS,+}+}++/// Get the PTE level for this MMU version.+pub(crate)fnpte_level(&self)->PageTableLevel{+matchself{+Self::V2=>ver2::PTE_LEVEL,+Self::V3=>ver3::PTE_LEVEL,+}+}++/// Get the dual PDE level (128-bit entries) for this MMU version.+pub(crate)fndual_pde_level(&self)->PageTableLevel{+matchself{+Self::V2=>ver2::DUAL_PDE_LEVEL,+Self::V3=>ver3::DUAL_PDE_LEVEL,+}+}++/// Get the number of PDE levels for this MMU version.+pub(crate)fnpde_level_count(&self)->usize{+self.pde_levels().len()+}++/// Get the entry size in bytes for a given level.+pub(crate)fnentry_size(&self,level:PageTableLevel)->usize{+iflevel==self.dual_pde_level(){+16// 128-bit dual PDE+}else{+8// 64-bit PDE/PTE+}+}++/// Get the number of entries per page table page for a given level.+pub(crate)fnentries_per_page(&self,level:PageTableLevel)->usize{+PAGE_SIZE/self.entry_size(level)+}++/// Compute upper bound on page table pages needed for `num_virt_pages`.+///+/// Walks from PTE level up through PDE levels, accumulating the tree.+pub(crate)fnpt_pages_upper_bound(&self,num_virt_pages:usize)->usize{+letmuttotal=0;++// PTE pages at the leaf level.+letpte_epp=self.entries_per_page(self.pte_level());+// Ceiling of (num_virt_pages / entries_per_page).+letmutpages_at_level=(num_virt_pages+pte_epp-1)/pte_epp;+total+=pages_at_level;++// Walk PDE levels bottom-up (reverse of pde_levels()).+for&levelinself.pde_levels().iter().rev(){+letepp=self.entries_per_page(level);+// How many pages at this level do we need to point to+// the previous pages_at_level?+// Calculated as ceiling of (pages_at_level / entries_per_page).+pages_at_level=(pages_at_level+epp-1)/epp;+total+=pages_at_level;+}++total+}+}+/// Memory aperture for Page Table Entries (`PTE`s).////// Determines which memory region the `PTE` points to.
@@ -149,3 +227,252 @@ fn from(val: AperturePde) -> Self {valasu8}}++/// Unified Page Table Entry wrapper for both MMU v2 and v3 `PTE`+/// types, allowing the walker to work with either format.+#[derive(Debug, Clone, Copy)]+pub(crate)enumPte{+/// MMU v2 `PTE` (Turing/Ampere/Ada).+V2(ver2::Pte),+/// MMU v3 `PTE` (Hopper+).+V3(ver3::Pte),+}++implPte{+/// Create a `PTE` from a raw `u64` value for the given MMU version.+pub(crate)fnnew(version:MmuVersion,val:u64)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::Pte::new(val)),+MmuVersion::V3=>Self::V3(ver3::Pte::new(val)),+}+}++/// Create an invalid `PTE` for the given MMU version.+pub(crate)fninvalid(version:MmuVersion)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::Pte::invalid()),+MmuVersion::V3=>Self::V3(ver3::Pte::invalid()),+}+}++/// Create a valid `PTE` for video memory.+pub(crate)fnnew_vram(version:MmuVersion,pfn:Pfn,writable:bool)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::Pte::new_vram(pfn,writable)),+MmuVersion::V3=>Self::V3(ver3::Pte::new_vram(pfn,writable)),+}+}++/// Check if this `PTE` is valid.+pub(crate)fnis_valid(&self)->bool{+matchself{+Self::V2(p)=>p.valid(),+Self::V3(p)=>p.valid(),+}+}++/// Get the physical frame number.+pub(crate)fnframe_number(&self)->Pfn{+matchself{+Self::V2(p)=>p.frame_number(),+Self::V3(p)=>p.frame_number(),+}+}++/// Get the raw `u64` value.+pub(crate)fnraw_u64(&self)->u64{+matchself{+Self::V2(p)=>p.raw_u64(),+Self::V3(p)=>p.raw_u64(),+}+}++/// Read a `PTE` from VRAM.+pub(crate)fnread(+window:&mutpramin::PraminWindow<'_>,+addr:VramAddress,+mmu_version:MmuVersion,+)->Result<Self>{+letval=window.try_read64(addr.raw())?;+Ok(Self::new(mmu_version,val))+}++/// Write this `PTE` to VRAM.+pub(crate)fnwrite(+&self,+window:&mutpramin::PraminWindow<'_>,+addr:VramAddress,+)->Result{+window.try_write64(addr.raw(),self.raw_u64())+}+}++implDefaultforPte{+fndefault()->Self{+Self::V2(ver2::Pte::default())+}+}++/// Unified Page Directory Entry wrapper for both MMU v2 and v3 `PDE`.+#[derive(Debug, Clone, Copy)]+pub(crate)enumPde{+/// MMU v2 `PDE` (Turing/Ampere/Ada).+V2(ver2::Pde),+/// MMU v3 `PDE` (Hopper+).+V3(ver3::Pde),+}++implPde{+/// Create a `PDE` from a raw `u64` value for the given MMU version.+pub(crate)fnnew(version:MmuVersion,val:u64)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::Pde::new(val)),+MmuVersion::V3=>Self::V3(ver3::Pde::new(val)),+}+}++/// Create a valid `PDE` pointing to a page table in video memory.+pub(crate)fnnew_vram(version:MmuVersion,table_pfn:Pfn)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::Pde::new_vram(table_pfn)),+MmuVersion::V3=>Self::V3(ver3::Pde::new_vram(table_pfn)),+}+}++/// Create an invalid `PDE` for the given MMU version.+pub(crate)fninvalid(version:MmuVersion)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::Pde::invalid()),+MmuVersion::V3=>Self::V3(ver3::Pde::invalid()),+}+}++/// Check if this `PDE` is valid.+pub(crate)fnis_valid(&self)->bool{+matchself{+Self::V2(p)=>p.is_valid(),+Self::V3(p)=>p.is_valid(),+}+}++/// Get the VRAM address of the page table.+pub(crate)fntable_vram_address(&self)->VramAddress{+matchself{+Self::V2(p)=>p.table_vram_address(),+Self::V3(p)=>p.table_vram_address(),+}+}++/// Get the raw `u64` value.+pub(crate)fnraw_u64(&self)->u64{+matchself{+Self::V2(p)=>p.raw_u64(),+Self::V3(p)=>p.raw_u64(),+}+}++/// Read a `PDE` from VRAM.+pub(crate)fnread(+window:&mutpramin::PraminWindow<'_>,+addr:VramAddress,+mmu_version:MmuVersion,+)->Result<Self>{+letval=window.try_read64(addr.raw())?;+Ok(Self::new(mmu_version,val))+}++/// Write this `PDE` to VRAM.+pub(crate)fnwrite(+&self,+window:&mutpramin::PraminWindow<'_>,+addr:VramAddress,+)->Result{+window.try_write64(addr.raw(),self.raw_u64())+}+}++implDefaultforPde{+fndefault()->Self{+Self::V2(ver2::Pde::default())+}+}++/// Unified Dual Page Directory Entry wrapper for both MMU v2 and v3 [`DualPde`].+#[derive(Debug, Clone, Copy)]+pub(crate)enumDualPde{+/// MMU v2 [`DualPde`] (Turing/Ampere/Ada).+V2(ver2::DualPde),+/// MMU v3 [`DualPde`] (Hopper+).+V3(ver3::DualPde),+}++implDualPde{+/// Create a [`DualPde`] from raw 128-bit value (two `u64`s) for the given MMU version.+pub(crate)fnnew(version:MmuVersion,big:u64,small:u64)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::DualPde::new(big,small)),+MmuVersion::V3=>Self::V3(ver3::DualPde::new(big,small)),+}+}++/// Create a [`DualPde`] with only the small page table pointer set.+pub(crate)fnnew_small(version:MmuVersion,table_pfn:Pfn)->Self{+matchversion{+MmuVersion::V2=>Self::V2(ver2::DualPde::new_small(table_pfn)),+MmuVersion::V3=>Self::V3(ver3::DualPde::new_small(table_pfn)),+}+}++/// Check if the small page table pointer is valid.+pub(crate)fnhas_small(&self)->bool{+matchself{+Self::V2(d)=>d.has_small(),+Self::V3(d)=>d.has_small(),+}+}++/// Get the small page table VRAM address.+pub(crate)fnsmall_vram_address(&self)->VramAddress{+matchself{+Self::V2(d)=>d.small.table_vram_address(),+Self::V3(d)=>d.small.table_vram_address(),+}+}++/// Get the raw `u64` value of the big PDE.+pub(crate)fnbig_raw_u64(&self)->u64{+matchself{+Self::V2(d)=>d.big.raw_u64(),+Self::V3(d)=>d.big.raw_u64(),+}+}++/// Get the raw `u64` value of the small PDE.+pub(crate)fnsmall_raw_u64(&self)->u64{+matchself{+Self::V2(d)=>d.small.raw_u64(),+Self::V3(d)=>d.small.raw_u64(),+}+}++/// Read a dual PDE (128-bit) from VRAM.+pub(crate)fnread(+window:&mutpramin::PraminWindow<'_>,+addr:VramAddress,+mmu_version:MmuVersion,+)->Result<Self>{+letlo=window.try_read64(addr.raw())?;+lethi=window.try_read64(addr.raw()+8)?;+Ok(Self::new(mmu_version,lo,hi))+}++/// Write this dual PDE (128-bit) to VRAM.+pub(crate)fnwrite(+&self,+window:&mutpramin::PraminWindow<'_>,+addr:VramAddress,+)->Result{+window.try_write64(addr.raw(),self.big_raw_u64())?;+window.try_write64(addr.raw()+8,self.small_raw_u64())+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:31
Add TLB (Translation Lookaside Buffer) flush support for GPU MMU.
After modifying page table entries, the GPU's TLB must be invalidated
to ensure the new mappings take effect. The Tlb struct provides flush
functionality through BAR0 registers.
The flush operation writes the page directory base address and triggers
an invalidation, polling for completion with a 2 second timeout matching
the Nouveau driver.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/mod.rs | 1 +
drivers/gpu/nova-core/mm/tlb.rs | 92 +++++++++++++++++++++++++++++++++
drivers/gpu/nova-core/regs.rs | 33 ++++++++++++
3 files changed, 126 insertions(+)
create mode 100644 drivers/gpu/nova-core/mm/tlb.rs
@@ -0,0 +1,92 @@+// SPDX-License-Identifier: GPL-2.0++//! TLB (Translation Lookaside Buffer) flush support for GPU MMU.+//!+//! After modifying page table entries, the GPU's TLB must be flushed to+//! ensure the new mappings take effect. This module provides TLB flush+//! functionality for virtual memory managers.+//!+//! # Example+//!+//! ```ignore+//! use crate::mm::tlb::Tlb;+//!+//! fn page_table_update(tlb: &Tlb, pdb_addr: VramAddress) -> Result<()> {+//! // ... modify page tables ...+//!+//! // Flush TLB to make changes visible (polls for completion).+//! tlb.flush(pdb_addr)?;+//!+//! Ok(())+//! }+//! ```++#![allow(dead_code)]++usekernel::{+devres::Devres,+io::poll::read_poll_timeout,+new_mutex,+prelude::*,+sync::{Arc,Mutex},+time::Delta,//+};++usecrate::{+driver::Bar0,+mm::VramAddress,+regs,//+};++/// TLB manager for GPU translation buffer operations.+#[pin_data]+pub(crate)structTlb{+bar:Arc<Devres<Bar0>>,+/// TLB flush serialization lock: This lock is acquired during the+/// DMA fence signalling critical path. It must NEVER be held across any+/// reclaimable CPU memory allocations because the memory reclaim path can+/// call `dma_fence_wait()`, which would deadlock with this lock held.+#[pin]+lock:Mutex<()>,+}++implTlb{+/// Create a new TLB manager.+pub(super)fnnew(bar:Arc<Devres<Bar0>>)->implPinInit<Self>{+pin_init!(Self{+bar,+lock<-new_mutex!((),"tlb_flush"),+})+}++/// Flush the GPU TLB for a specific page directory base.+///+/// This invalidates all TLB entries associated with the given PDB address.+/// Must be called after modifying page table entries to ensure the GPU sees+/// the updated mappings.+pub(crate)fnflush(&self,pdb_addr:VramAddress)->Result{+let_guard=self.lock.lock();++letbar=self.bar.try_access().ok_or(ENODEV)?;++// Write PDB address.+regs::NV_TLB_FLUSH_PDB_LO::from_pdb_addr(pdb_addr.raw_u64()).write(&*bar);+regs::NV_TLB_FLUSH_PDB_HI::from_pdb_addr(pdb_addr.raw_u64()).write(&*bar);++// Trigger flush: invalidate all pages and enable.+regs::NV_TLB_FLUSH_CTRL::default()+.set_page_all(true)+.set_enable(true)+.write(&*bar);++// Poll for completion - enable bit clears when flush is done.+read_poll_timeout(+||Ok(regs::NV_TLB_FLUSH_CTRL::read(&*bar)),+|ctrl|!ctrl.enable(),+Delta::ZERO,+Delta::from_secs(2),+)?;++Ok(())+}+}
@@ -454,3 +454,36 @@ pub(crate) mod ga100 {0:0display_disabledasbool;});}++// MMU TLB++register!(NV_TLB_FLUSH_PDB_LO@0x00b830a0,"TLB flush register: PDB address bits [39:8]"{+31:0pdb_loasu32,"PDB address bits [39:8]";+});++implNV_TLB_FLUSH_PDB_LO{+/// Create a register value from a PDB address.+///+/// Extracts bits [39:8] of the address and shifts it right by 8 bits.+pub(crate)fnfrom_pdb_addr(addr:u64)->Self{+Self::default().set_pdb_lo(((addr>>8)&0xFFFF_FFFF)asu32)+}+}++register!(NV_TLB_FLUSH_PDB_HI@0x00b830a4,"TLB flush register: PDB address bits [47:40]"{+7:0pdb_hiasu8,"PDB address bits [47:40]";+});++implNV_TLB_FLUSH_PDB_HI{+/// Create a register value from a PDB address.+///+/// Extracts bits [47:40] of the address and shifts it right by 40 bits.+pub(crate)fnfrom_pdb_addr(addr:u64)->Self{+Self::default().set_pdb_hi(((addr>>40)&0xFF)asu8)+}+}++register!(NV_TLB_FLUSH_CTRL@0x00b830b0,"TLB flush control register"{+0:0page_allasbool,"Invalidate all pages";+31:31enableasbool,"Enable/trigger flush (clears when flush completes)";+});
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:32
Introduce GpuMm as the centralized GPU memory manager that owns:
- Buddy allocator for VRAM allocation.
- PRAMIN window for direct VRAM access.
- TLB manager for translation buffer operations.
This provides clean ownership model where GpuMm provides accessor
methods for its components that can be used for memory management
operations.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/gpu.rs | 15 ++++++++
drivers/gpu/nova-core/mm/mod.rs | 63 ++++++++++++++++++++++++++++++++-
2 files changed, 77 insertions(+), 1 deletion(-)
@@ -249,6 +252,9 @@ pub(crate) struct Gpu {gsp_falcon:Falcon<GspFalcon>,/// SEC2 falcon instance, used for GSP boot up and cleanup.sec2_falcon:Falcon<Sec2Falcon>,+/// GPU memory manager owning memory management resources.+#[pin]+mm:GpuMm,/// GSP runtime data. Temporarily an empty placeholder.#[pin]gsp:Gsp,
@@ -281,6 +287,15 @@ pub(crate) fn new<'a>(sec2_falcon:Falcon::new(pdev.as_ref(),spec.chipset)?,+// Create GPU memory manager owning memory management resources.+// This will be initialized with the usable VRAM region from GSP in a later+// patch. For now, we use a placeholder of 1MB.+mm<-GpuMm::new(devres_bar.clone(),&GpuBuddyParams{+base_offset_bytes:0,+physical_memory_size_bytes:SZ_1Masu64,+chunk_size_bytes:SZ_4Kasu64,+})?,+gsp<-Gsp::new(pdev),_:{gsp.boot(pdev,bar,spec.chipset,gsp_falcon,sec2_falcon)?},
@@ -8,7 +8,68 @@pub(crate)modpramin;pub(crate)modtlb;-usekernel::sizes::SZ_4K;+usekernel::{+devres::Devres,+gpu::buddy::{+GpuBuddy,+GpuBuddyParams,//+},+prelude::*,+sizes::SZ_4K,+sync::Arc,//+};++usecrate::driver::Bar0;++pub(crate)usetlb::Tlb;++/// GPU Memory Manager - owns all core MM components.+///+/// Provides centralized ownership of memory management resources:+/// - [`GpuBuddy`] allocator for VRAM page table allocation.+/// - [`pramin::Pramin`] for direct VRAM access.+/// - [`Tlb`] manager for translation buffer flush operations.+#[pin_data]+pub(crate)structGpuMm{+buddy:GpuBuddy,+#[pin]+pramin:pramin::Pramin,+#[pin]+tlb:Tlb,+}++implGpuMm{+/// Create a pin-initializer for `GpuMm`.+pub(crate)fnnew(+bar:Arc<Devres<Bar0>>,+buddy_params:GpuBuddyParams,+)->Result<implPinInit<Self>>{+letbuddy=GpuBuddy::new(buddy_params)?;+lettlb_init=Tlb::new(bar.clone());+letpramin_init=pramin::Pramin::new(bar)?;++Ok(pin_init!(Self{+buddy,+pramin<-pramin_init,+tlb<-tlb_init,+}))+}++/// Access the [`GpuBuddy`] allocator.+pub(crate)fnbuddy(&self)->&GpuBuddy{+&self.buddy+}++/// Access the [`pramin::Pramin`].+pub(crate)fnpramin(&self)->&pramin::Pramin{+&self.pramin+}++/// Access the [`Tlb`] manager.+pub(crate)fntlb(&self)->&Tlb{+&self.tlb+}+}/// Page size in bytes (4 KiB).pub(crate)constPAGE_SIZE:usize=SZ_4K;
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:34
Add the Virtual Memory Manager (VMM) infrastructure for GPU address
space management. Each Vmm instance manages a single address space
identified by its Page Directory Base (PDB) address, used for Channel,
BAR1 and BAR2 mappings.
Mapping APIs and virtual address range tracking are added in later
commits.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/mod.rs | 1 +
drivers/gpu/nova-core/mm/vmm.rs | 63 +++++++++++++++++++++++++++++++++
2 files changed, 64 insertions(+)
create mode 100644 drivers/gpu/nova-core/mm/vmm.rs
@@ -0,0 +1,63 @@+// 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). It wraps the page table walker+//! and handles TLB flushing after modifications.++#![allow(dead_code)]++usekernel::{+gpu::buddy::AllocatedBlocks,+prelude::*,//+};++usecrate::mm::{+pagetable::{+walk::{PtWalk,WalkResult},+MmuVersion,//+},+GpuMm,+Pfn,+Vfn,+VramAddress,//+};++/// Virtual Memory Manager for a GPU address space.+///+/// Each [`Vmm`] instance manages a single address space identified by its Page+/// Directory Base (`PDB`) address. The [`Vmm`] is used for Channel, BAR1 and+/// BAR2 mappings.+pub(crate)structVmm{+pub(crate)pdb_addr:VramAddress,+pub(crate)mmu_version:MmuVersion,+/// Page table allocations required for mappings.+page_table_allocs:KVec<Pin<KBox<AllocatedBlocks>>>,+}++implVmm{+/// Create a new [`Vmm`] for the given Page Directory Base address.+pub(crate)fnnew(pdb_addr:VramAddress,mmu_version:MmuVersion)->Result<Self>{+// Only MMU v2 is supported for now.+ifmmu_version!=MmuVersion::V2{+returnErr(ENOTSUPP);+}++Ok(Self{+pdb_addr,+mmu_version,+page_table_allocs:KVec::new(),+})+}++/// Read the PFN for a mapped VFN if one is mapped.+pub(crate)fnread_mapping(&self,mm:&GpuMm,vfn:Vfn)->Result<Option<Pfn>>{+letwalker=PtWalk::new(self.pdb_addr,self.mmu_version);++matchwalker.walk_to_pte_lookup(mm,vfn)?{+WalkResult::Mapped{pfn,..}=>Ok(Some(pfn)),+WalkResult::Unmapped{..}|WalkResult::PageTableMissing=>Ok(None),+}+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:34
Add the page table walker implementation that traverses the page table
hierarchy for both MMU v2 (5-level) and MMU v3 (6-level) to resolve
virtual addresses to physical addresses or find PTE locations.
Currently only v2 has been tested (nova-core currently boots pre-hopper)
with some initial prepatory work done for v3.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/pagetable/mod.rs | 1 +
drivers/gpu/nova-core/mm/pagetable/walk.rs | 213 +++++++++++++++++++++
2 files changed, 214 insertions(+)
create mode 100644 drivers/gpu/nova-core/mm/pagetable/walk.rs
@@ -0,0 +1,213 @@+// SPDX-License-Identifier: GPL-2.0++//! Page table walker implementation for NVIDIA GPUs.+//!+//! This module provides page table walking functionality for MMU v2 and v3.+//! The walker traverses the page table hierarchy to resolve virtual addresses+//! to physical addresses or to find PTE locations.+//!+//! # Page Table Hierarchy+//!+//! ## MMU v2 (Turing/Ampere/Ada) - 5 levels+//!+//! ```text+//! +-------+ +-------+ +-------+ +---------+ +-------++//! | PDB |---->| L1 |---->| L2 |---->| L3 Dual |---->| L4 |+//! | (L0) | | | | | | PDE | | (PTE) |+//! +-------+ +-------+ +-------+ +---------+ +-------++//! 64-bit 64-bit 64-bit 128-bit 64-bit+//! PDE PDE PDE (big+small) PTE+//! ```+//!+//! ## MMU v3 (Hopper+) - 6 levels+//!+//! ```text+//! +-------+ +-------+ +-------+ +-------+ +---------+ +-------++//! | PDB |---->| L1 |---->| L2 |---->| L3 |---->| L4 Dual |---->| L5 |+//! | (L0) | | | | | | | | PDE | | (PTE) |+//! +-------+ +-------+ +-------+ +-------+ +---------+ +-------++//! 64-bit 64-bit 64-bit 64-bit 128-bit 64-bit+//! PDE PDE PDE PDE (big+small) PTE+//! ```+//!+//! # Result of a page table walk+//!+//! The walker returns a [`WalkResult`] indicating the outcome.++usekernel::prelude::*;++usesuper::{+DualPde,+MmuVersion,+PageTableLevel,+Pde,+Pte,//+};+usecrate::mm::{+pramin,+GpuMm,+Pfn,+Vfn,+VirtualAddress,+VramAddress,//+};++/// Result of walking to a PTE.+#[derive(Debug, Clone, Copy)]+pub(crate)enumWalkResult{+/// Intermediate page tables are missing (only returned in lookup mode).+PageTableMissing,+/// PTE exists but is invalid (page not mapped).+Unmapped{pte_addr:VramAddress},+/// PTE exists and is valid (page is mapped).+Mapped{pte_addr:VramAddress,pfn:Pfn},+}++/// Result of walking PDE levels only.+///+/// Returned by [`PtWalk::walk_pde_levels()`] to indicate whether all PDE levels+/// resolved or a PDE is missing.+#[derive(Debug, Clone, Copy)]+pub(crate)enumWalkPdeResult{+/// All PDE levels resolved -- returns PTE page table address.+Complete{+/// VRAM address of the PTE-level page table.+pte_table:VramAddress,+},+/// A PDE is missing and no prepared page was provided by the closure.+Missing{+/// PDE slot address in the parent page table (where to install).+install_addr:VramAddress,+/// The page table level that is missing.+level:PageTableLevel,+},+}++/// Page table walker for NVIDIA GPUs.+///+/// Walks the page table hierarchy (5 levels for v2, 6 for v3) to find PTE+/// locations or resolve virtual addresses.+pub(crate)structPtWalk{+pdb_addr:VramAddress,+mmu_version:MmuVersion,+}++implPtWalk{+/// Calculate the VRAM address of an entry within a page table.+fnentry_addr(+table:VramAddress,+mmu_version:MmuVersion,+level:PageTableLevel,+index:u64,+)->VramAddress{+letentry_size=mmu_version.entry_size(level)asu64;+VramAddress::new(table.raw_u64()+index*entry_size)+}++/// Create a new page table walker.+pub(crate)fnnew(pdb_addr:VramAddress,mmu_version:MmuVersion)->Self{+Self{+pdb_addr,+mmu_version,+}+}++/// Walk PDE levels with closure-based resolution for missing PDEs.+///+/// Traverses all PDE levels for the MMU version. At each level, reads the PDE.+/// If valid, extracts the child table address and continues. If missing, calls+/// `resolve_prepared(install_addr)` to resolve the missing PDE.+pub(crate)fnwalk_pde_levels(+&self,+window:&mutpramin::PraminWindow<'_>,+vfn:Vfn,+resolve_prepared:implFn(VramAddress)->Option<VramAddress>,+)->Result<WalkPdeResult>{+letva=VirtualAddress::from(vfn);+letmutcur_table=self.pdb_addr;++for&levelinself.mmu_version.pde_levels(){+letidx=va.level_index(level.as_index());+letinstall_addr=Self::entry_addr(cur_table,self.mmu_version,level,idx);++iflevel==self.mmu_version.dual_pde_level(){+// 128-bit dual PDE with big+small page table pointers.+letdpde=DualPde::read(window,install_addr,self.mmu_version)?;+ifdpde.has_small(){+cur_table=dpde.small_vram_address();+continue;+}+}else{+// Regular 64-bit PDE.+letpde=Pde::read(window,install_addr,self.mmu_version)?;+ifpde.is_valid(){+cur_table=pde.table_vram_address();+continue;+}+}++// PDE missing in HW. Ask caller for resolution.+ifletSome(prepared_addr)=resolve_prepared(install_addr){+cur_table=prepared_addr;+continue;+}++returnOk(WalkPdeResult::Missing{+install_addr,+level,+});+}++Ok(WalkPdeResult::Complete{+pte_table:cur_table,+})+}++/// Walk to PTE for lookup only (no allocation).+///+/// Returns [`WalkResult::PageTableMissing`] if intermediate tables don't exist.+pub(crate)fnwalk_to_pte_lookup(&self,mm:&GpuMm,vfn:Vfn)->Result<WalkResult>{+letmutwindow=mm.pramin().window()?;+self.walk_to_pte_lookup_with_window(&mutwindow,vfn)+}++/// Walk to PTE using a caller-provided PRAMIN window (lookup only).+///+/// Uses [`PtWalk::walk_pde_levels()`] for the PDE traversal, then reads the PTE at+/// the leaf level. Useful when called for multiple VFNs with single PRAMIN window+/// acquisition. Used by [`Vmm::execute_map()`] and [`Vmm::unmap_pages()`].+pub(crate)fnwalk_to_pte_lookup_with_window(+&self,+window:&mutpramin::PraminWindow<'_>,+vfn:Vfn,+)->Result<WalkResult>{+matchself.walk_pde_levels(window,vfn,|_|None)?{+WalkPdeResult::Complete{pte_table}=>{+Self::read_pte_at_level(window,vfn,pte_table,self.mmu_version)+}+WalkPdeResult::Missing{..}=>Ok(WalkResult::PageTableMissing),+}+}++/// Read the PTE at the PTE level given the PTE table address.+fnread_pte_at_level(+window:&mutpramin::PraminWindow<'_>,+vfn:Vfn,+pte_table:VramAddress,+mmu_version:MmuVersion,+)->Result<WalkResult>{+letva=VirtualAddress::from(vfn);+letpte_level=mmu_version.pte_level();+letpte_idx=va.level_index(pte_level.as_index());+letpte_addr=Self::entry_addr(pte_table,mmu_version,pte_level,pte_idx);+letpte=Pte::read(window,pte_addr,mmu_version)?;++ifpte.is_valid(){+returnOk(WalkResult::Mapped{+pte_addr,+pfn:pte.frame_number(),+});+}+Ok(WalkResult::Unmapped{pte_addr})+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:36
Add virtual address range tracking to the VMM using a buddy allocator.
This enables contiguous virtual address range allocation for mappings.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/vmm.rs | 75 +++++++++++++++++++++++++++++++--
1 file changed, 71 insertions(+), 4 deletions(-)
@@ -21,7 +30,8 @@GpuMm,Pfn,Vfn,-VramAddress,//+VramAddress,+PAGE_SIZE,//};/// Virtual Memory Manager for a GPU address space.
@@ -34,23 +44,80 @@ pub(crate) struct Vmm {pub(crate)mmu_version:MmuVersion,/// Page table allocations required for mappings.page_table_allocs:KVec<Pin<KBox<AllocatedBlocks>>>,+/// Buddy allocator for virtual address range tracking.+virt_buddy:GpuBuddy,}implVmm{/// Create a new [`Vmm`] for the given Page Directory Base address.-pub(crate)fnnew(pdb_addr:VramAddress,mmu_version:MmuVersion)->Result<Self>{+/// The [`Vmm`] will manage a virtual address space of `va_size` bytes.+pub(crate)fnnew(+pdb_addr:VramAddress,+mmu_version:MmuVersion,+va_size:u64,+)->Result<Self>{// Only MMU v2 is supported for now.ifmmu_version!=MmuVersion::V2{returnErr(ENOTSUPP);}+letvirt_buddy=GpuBuddy::new(GpuBuddyParams{+base_offset_bytes:0,+physical_memory_size_bytes:va_size,+chunk_size_bytes:SZ_4Kasu64,+})?;+Ok(Self{pdb_addr,mmu_version,page_table_allocs:KVec::new(),+virt_buddy,})}+/// Allocate a contiguous virtual frame number range.+///+/// # Arguments+/// - `num_pages`: Number of pages to allocate.+/// - `va_range`: `None` = allocate anywhere,+/// `Some(range)` = constrain allocation to the given range.+pub(crate)fnalloc_vfn_range(+&self,+num_pages:usize,+va_range:Option<Range<u64>>,+)->Result<(Vfn,Pin<KBox<AllocatedBlocks>>)>{+letsize_bytes=(num_pagesasu64)+.checked_mul(PAGE_SIZEasu64)+.ok_or(EOVERFLOW)?;++let(start,end)=matchva_range{+Some(r)=>{+letrange_size=r.end.checked_sub(r.start).ok_or(EOVERFLOW)?;+ifrange_size!=size_bytes{+returnErr(EINVAL);+}+(r.start,r.end)+}+None=>(0,0),+};++letparams=GpuBuddyAllocParams{+start_range_address:start,+end_range_address:end,+size_bytes,+min_block_size_bytes:SZ_4Kasu64,+buddy_flags:BuddyFlags::try_new(BuddyFlags::CONTIGUOUS_ALLOCATION)?,+};++letalloc=KBox::pin_init(self.virt_buddy.alloc_blocks(¶ms),GFP_KERNEL)?;++// Get the starting offset of the first block (only block as range is contiguous).+letoffset=alloc.iter().next().ok_or(ENOMEM)?.offset();+letvfn=Vfn::new(offset/PAGE_SIZEasu64);++Ok((vfn,alloc))+}+/// Read the PFN for a mapped VFN if one is mapped.pub(crate)fnread_mapping(&self,mm:&GpuMm,vfn:Vfn)->Result<Option<Pfn>>{letwalker=PtWalk::new(self.pdb_addr,self.mmu_version);
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:36
Add the page table mapping and unmapping API to the Virtual Memory
Manager, implementing a two-phase prepare/execute model suitable for
use both inside and outside the DMA fence signalling critical path.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/vmm.rs | 347 +++++++++++++++++++++++++++++++-
1 file changed, 345 insertions(+), 2 deletions(-)
@@ -46,6 +56,74 @@ pub(crate) struct Vmm {page_table_allocs:KVec<Pin<KBox<AllocatedBlocks>>>,/// Buddy allocator for virtual address range tracking.virt_buddy:GpuBuddy,+/// Prepared PT pages pending PDE installation, keyed by `install_addr`.+///+/// Populated by `Vmm` mapping prepare phase and drained in the execute phase.+/// Shared by all pending maps in the `Vmm`, thus preventing races where 2+/// maps might be trying to install the same page table/directory entry pointer.+pt_pages:RBTree<VramAddress,PreparedPtPage>,+}++/// A pre-allocated and zeroed page table page.+///+/// Created during the mapping prepare phase and consumed during the mapping execute phase.+/// Stored in an [`RBTree`] keyed by the PDE slot address (`install_addr`).+structPreparedPtPage{+/// The allocated and zeroed page table page.+alloc:Pin<KBox<AllocatedBlocks>>,+/// Page table level -- needed to determine if this PT page is for a dual PDE.+level:PageTableLevel,+}++/// Multi-page prepared mapping -- VA range allocated, ready for execute.+///+/// Produced by [`Vmm::prepare_map()`], consumed by [`Vmm::execute_map()`].+/// The struct owns the VA space allocation between prepare and execute phases.+pub(crate)structPreparedMapping{+vfn_start:Vfn,+num_pages:usize,+vfn_alloc:Pin<KBox<AllocatedBlocks>>,+}++/// Result of a mapping operation -- tracks the active mapped range.+///+/// Returned by [`Vmm::execute_map()`] and [`Vmm::map_pages()`].+/// Owns the VA allocation; the VA range is freed when this is dropped.+/// Callers must call [`Vmm::unmap_pages()`] before dropping to invalidate+/// PTEs (dropping only frees the VA range, not the PTE entries).+pub(crate)structMappedRange{+pub(crate)vfn_start:Vfn,+pub(crate)num_pages:usize,+/// VA allocation -- freed when [`MappedRange`] is dropped.+_vfn_alloc:Pin<KBox<AllocatedBlocks>>,+/// Logs a warning if dropped without unmapping.+_drop_guard:MustUnmapGuard,+}++/// Guard that logs a warning once if a [`MappedRange`] is dropped without+/// calling [`Vmm::unmap_pages()`].+structMustUnmapGuard{+armed:Cell<bool>,+}++implMustUnmapGuard{+constfnnew()->Self{+Self{+armed:Cell::new(true),+}+}++fndisarm(&self){+self.armed.set(false);+}+}++implDropforMustUnmapGuard{+fndrop(&mutself){+ifself.armed.get(){+kernel::pr_warn_once!("MappedRange dropped without calling unmap_pages()\n");+}+}}implVmm{
@@ -127,4 +206,268 @@ pub(crate) fn read_mapping(&self, mm: &GpuMm, vfn: Vfn) -> Result<Option<Pfn>> {WalkResult::Unmapped{..}|WalkResult::PageTableMissing=>Ok(None),}}++/// Allocate and zero a physical page table page for a specific PDE slot.+/// Called during the map prepare phase.+fnalloc_and_zero_page_table(+&mutself,+mm:&GpuMm,+level:PageTableLevel,+)->Result<PreparedPtPage>{+letparams=GpuBuddyAllocParams{+start_range_address:0,+end_range_address:0,+size_bytes:SZ_4Kasu64,+min_block_size_bytes:SZ_4Kasu64,+buddy_flags:BuddyFlags::try_new(0)?,+};+letblocks=KBox::pin_init(mm.buddy().alloc_blocks(¶ms),GFP_KERNEL)?;++// Get page's VRAM address from the allocation.+letpage_vram=VramAddress::new(blocks.iter().next().ok_or(ENOMEM)?.offset());++// Zero via PRAMIN.+letmutwindow=mm.pramin().window()?;+letbase=page_vram.raw();+foroffin(0..PAGE_SIZE).step_by(8){+window.try_write64(base+off,0)?;+}++Ok(PreparedPtPage{+alloc:blocks,+level,+})+}++/// Ensure all intermediate page table pages are prepared for a [`Vfn`]. Just+/// finds out which PDE pages are missing, allocates pages for them, and defers+/// installation to the execute phase.+///+/// PRAMIN is released before each allocation and re-acquired after. Memory+/// allocations outside of holding this lock to prevent deadlocks with fence signalling+/// critical path.+fnensure_pte_path(&mutself,mm:&GpuMm,vfn:Vfn)->Result{+letwalker=PtWalk::new(self.pdb_addr,self.mmu_version);+letmax_iter=2*self.mmu_version.pde_level_count();++// Keep looping until all PDE levels are resolved.+for_in0..max_iter{+letmutwindow=mm.pramin().window()?;++// Walk PDE levels. The closure checks self.pt_pages for prepared-but-uninstalled+// pages, letting the walker continue through them as if they were installed in HW.+// The walker keeps calling the closure to get these "prepared but not installed" pages.+letresult=walker.walk_pde_levels(&mutwindow,vfn,|install_addr|{+self.pt_pages+.get(&install_addr)+.and_then(|p|Some(VramAddress::new(p.alloc.iter().next()?.offset())))+})?;++matchresult{+WalkPdeResult::Complete{..}=>{+// All PDE levels resolved.+returnOk(());+}+WalkPdeResult::Missing{+install_addr,+level,+}=>{+// Drop PRAMIN before allocation.+drop(window);+letpage=self.alloc_and_zero_page_table(mm,level)?;+letnode=RBTreeNode::new(install_addr,page,GFP_KERNEL)?;+letold=self.pt_pages.insert(node);+ifold.is_some(){+kernel::pr_warn_once!(+"VMM: duplicate install_addr in pt_pages (internal consistency error)\n"+);+returnErr(EIO);+}+// Loop: re-acquire PRAMIN and re-walk from root.+}+}+}++Err(EIO)+}++/// 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)fnprepare_map(+&mutself,+mm:&GpuMm,+num_pages:usize,+va_range:Option<Range<u64>>,+)->Result<PreparedMapping>{+ifnum_pages==0{+returnErr(EINVAL);+}++// Pre-reserve so execute_map() can use push_within_capacity (no alloc in+// fence signalling critical path).+// Upper bound on page table pages needed for the full tree (PTE pages + PDE+// pages at all levels).+letpt_upper_bound=self.mmu_version.pt_pages_upper_bound(num_pages);+self.page_table_allocs.reserve(pt_upper_bound,GFP_KERNEL)?;++// Allocate contiguous VA range.+let(vfn_start,vfn_alloc)=self.alloc_vfn_range(num_pages,va_range)?;++// Walk the hierarchy per-VFN to prepare pages for all missing PDEs.+foriin0..num_pages{+letvfn=Vfn::new(vfn_start.raw()+iasu64);+self.ensure_pte_path(mm,vfn)?;+}++Ok(PreparedMapping{+vfn_start,+num_pages,+vfn_alloc,+})+}++/// Execute a prepared multi-page mapping.+///+/// Drain prepared PT pages and install PDEs followed by single TLB flush.+pub(crate)fnexecute_map(+&mutself,+mm:&GpuMm,+prepared:PreparedMapping,+pfns:&[Pfn],+writable:bool,+)->Result<MappedRange>{+ifpfns.len()!=prepared.num_pages{+returnErr(EINVAL);+}++letPreparedMapping{+vfn_start,+num_pages,+vfn_alloc,+}=prepared;++letwalker=PtWalk::new(self.pdb_addr,self.mmu_version);+letmutwindow=mm.pramin().window()?;++// First, drain self.pt_pages, install all pending PDEs.+letmutcursor=self.pt_pages.cursor_front_mut();+whileletSome(c)=cursor{+let(next,node)=c.remove_current();+let(install_addr,page)=node.to_key_value();+letpage_vram=VramAddress::new(page.alloc.iter().next().ok_or(ENOMEM)?.offset());++ifpage.level==self.mmu_version.dual_pde_level(){+letnew_dpde=DualPde::new_small(self.mmu_version,Pfn::from(page_vram));+new_dpde.write(&mutwindow,install_addr)?;+}else{+letnew_pde=Pde::new_vram(self.mmu_version,Pfn::from(page_vram));+new_pde.write(&mutwindow,install_addr)?;+}++// Track the allocated pages in the `Vmm`.+self.page_table_allocs+.push_within_capacity(page.alloc)+.map_err(|_|ENOMEM)?;++cursor=next;+}++// Next, write PTEs (all PDEs now installed in HW).+for(i,&pfn)inpfns.iter().enumerate(){+letvfn=Vfn::new(vfn_start.raw()+iasu64);+letresult=walker.walk_to_pte_lookup_with_window(&mutwindow,vfn)?;++matchresult{+WalkResult::Unmapped{pte_addr}|WalkResult::Mapped{pte_addr,..}=>{+letpte=Pte::new_vram(self.mmu_version,pfn,writable);+pte.write(&mutwindow,pte_addr)?;+}+WalkResult::PageTableMissing=>{+kernel::pr_warn_once!("VMM: page table missing for VFN {vfn:?}\n");+returnErr(EIO);+}+}+}++drop(window);++// Finally, flush the TLB.+mm.tlb().flush(self.pdb_addr)?;++Ok(MappedRange{+vfn_start,+num_pages,+_vfn_alloc:vfn_alloc,+_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)fnmap_pages(+&mutself,+mm:&GpuMm,+pfns:&[Pfn],+va_range:Option<Range<u64>>,+writable:bool,+)->Result<MappedRange>{+ifpfns.is_empty(){+returnErr(EINVAL);+}++// Check if provided VA range is sufficient (if provided).+ifletSome(refrange)=va_range{+letrequired=pfns.len().checked_mul(PAGE_SIZE).ok_or(EOVERFLOW)?asu64;+letavailable=range.end.checked_sub(range.start).ok_or(EINVAL)?;+ifavailable<required{+returnErr(EINVAL);+}+}++letprepared=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.+///+/// Takes the range by value (consumes it), then invalidates PTEs for the range,+/// flushes the TLB, then drops the range (freeing the VA). PRAMIN lock is held.+pub(crate)fnunmap_pages(&mutself,mm:&GpuMm,range:MappedRange)->Result{+letwalker=PtWalk::new(self.pdb_addr,self.mmu_version);+letinvalid_pte=Pte::invalid(self.mmu_version);++letmutwindow=mm.pramin().window()?;+foriin0..range.num_pages{+letvfn=Vfn::new(range.vfn_start.raw()+iasu64);+letresult=walker.walk_to_pte_lookup_with_window(&mutwindow,vfn)?;++matchresult{+WalkResult::Mapped{pte_addr,..}|WalkResult::Unmapped{pte_addr}=>{+invalid_pte.write(&mutwindow,pte_addr)?;+}+WalkResult::PageTableMissing=>{+continue;+}+}+}+drop(window);++mm.tlb().flush(self.pdb_addr)?;++// 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.++range._drop_guard.disarm();// Unmap complete, Ok to drop MappedRange.+Ok(())+}}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:37
Add the BAR1 user interface for CPU access to GPU video memory through
the BAR1 aperture.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/driver.rs | 1 -
drivers/gpu/nova-core/mm/bar_user.rs | 154 +++++++++++++++++++++++++++
drivers/gpu/nova-core/mm/mod.rs | 1 +
3 files changed, 155 insertions(+), 1 deletion(-)
create mode 100644 drivers/gpu/nova-core/mm/bar_user.rs
@@ -0,0 +1,154 @@+// SPDX-License-Identifier: GPL-2.0++//! BAR1 user interface for CPU access to GPU virtual memory. Used for USERD+//! for GPU work submission, and applications to access GPU buffers via mmap().++usekernel::io::Io;+usekernel::prelude::*;++usecrate::{+driver::Bar1,+mm::{+pagetable::MmuVersion,+vmm::{+MappedRange,+Vmm,//+},+GpuMm,+Pfn,+Vfn,+VirtualAddress,+VramAddress,+PAGE_SIZE,//+},+};++/// BAR1 user interface for virtual memory mappings.+///+/// Owns a VMM instance with virtual address tracking and provides+/// BAR1-specific mapping and cleanup operations.+pub(crate)structBarUser{+vmm:Vmm,+}++implBarUser{+/// Create a new [`BarUser`] with virtual address tracking.+pub(crate)fnnew(+pdb_addr:VramAddress,+mmu_version:MmuVersion,+va_size:u64,+)->Result<Self>{+Ok(Self{+vmm:Vmm::new(pdb_addr,mmu_version,va_size)?,+})+}++/// Map physical pages to a contiguous BAR1 virtual range.+pub(crate)fnmap<'a>(+&'amutself,+mm:&'aGpuMm,+bar:&'aBar1,+pfns:&[Pfn],+writable:bool,+)->Result<BarAccess<'a>>{+ifpfns.is_empty(){+returnErr(EINVAL);+}++letmapped=self.vmm.map_pages(mm,pfns,None,writable)?;++Ok(BarAccess{+vmm:&mutself.vmm,+mm,+bar,+mapped:Some(mapped),+})+}+}++/// Access object for a mapped BAR1 region.+///+/// Wraps a [`MappedRange`] and provides BAR1 access. When dropped,+/// unmaps pages and releases the VA range (by passing the range to+/// [`Vmm::unmap_pages()`], which consumes it).+pub(crate)structBarAccess<'a>{+vmm:&'amutVmm,+mm:&'aGpuMm,+bar:&'aBar1,+/// Needs to be an `Option` so that we can `take()` it and call `Drop`+/// on it in [`Vmm::unmap_pages()`].+mapped:Option<MappedRange>,+}++impl<'a>BarAccess<'a>{+/// Returns the active mapping.+fnmapped(&self)->&MappedRange{+// SAFETY: unwrap() will never panic here because `mapped` is only+// `None` after `take()` in `Drop`, accessors are never called in `Drop`.+self.mapped.as_ref().unwrap()+}++/// Get the base virtual address of this mapping.+pub(crate)fnbase(&self)->VirtualAddress{+VirtualAddress::from(self.mapped().vfn_start)+}++/// Get the total size of the mapped region in bytes.+pub(crate)fnsize(&self)->usize{+self.mapped().num_pages*PAGE_SIZE+}++/// Get the starting virtual frame number.+pub(crate)fnvfn_start(&self)->Vfn{+self.mapped().vfn_start+}++/// Get the number of pages in this mapping.+pub(crate)fnnum_pages(&self)->usize{+self.mapped().num_pages+}++/// Translate an offset within this mapping to a BAR1 aperture offset.+fnbar_offset(&self,offset:usize)->Result<usize>{+ifoffset>=self.size(){+returnErr(EINVAL);+}++letbase=(self.mapped().vfn_start.raw()asusize)+.checked_mul(PAGE_SIZE)+.ok_or(EOVERFLOW)?;+base.checked_add(offset).ok_or(EOVERFLOW)+}++// Fallible accessors with runtime bounds checking.++/// Read a 32-bit value at the given offset.+pub(crate)fntry_read32(&self,offset:usize)->Result<u32>{+self.bar.try_read32(self.bar_offset(offset)?)+}++/// Write a 32-bit value at the given offset.+pub(crate)fntry_write32(&self,value:u32,offset:usize)->Result{+self.bar.try_write32(value,self.bar_offset(offset)?)+}++/// Read a 64-bit value at the given offset.+pub(crate)fntry_read64(&self,offset:usize)->Result<u64>{+self.bar.try_read64(self.bar_offset(offset)?)+}++/// Write a 64-bit value at the given offset.+pub(crate)fntry_write64(&self,value:u64,offset:usize)->Result{+self.bar.try_write64(value,self.bar_offset(offset)?)+}+}++implDropforBarAccess<'_>{+fndrop(&mutself){+ifletSome(mapped)=self.mapped.take(){+ifself.vmm.unmap_pages(self.mm,mapped).is_err(){+kernel::pr_warn_once!("BarAccess: unmap_pages failed.\n");+}+}+}+}
@@ -127,6 +130,12 @@ fn run_fwsec_frts(/// structures that the GSP will use at runtime.////// Upon return, the GSP is up and running, and its runtime object given as return value.+///+/// Returns a tuple containing:+/// - [`GetGspStaticInfoReply`]: Static GPU information from GSP, including the BAR1 page+/// directory base address needed for memory management.+/// - [`FbLayout`]: Frame buffer layout computed during boot, containing memory regions+/// required for [`GpuMm`] initialization.pub(crate)fnboot(mutself:Pin<&mutSelf>,pdev:&pci::Device<device::Bound>,
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:39
Add self-tests for BAR1 access during driver probe when
CONFIG_NOVA_MM_SELFTESTS is enabled (default disabled). This results in
testing the Vmm, GPU buddy allocator and BAR1 region all of which should
function correctly for the tests to pass.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/Kconfig | 10 ++
drivers/gpu/nova-core/driver.rs | 2 +
drivers/gpu/nova-core/gpu.rs | 48 ++++++++
drivers/gpu/nova-core/gsp/commands.rs | 1 -
drivers/gpu/nova-core/mm/bar_user.rs | 164 ++++++++++++++++++++++++++
5 files changed, 224 insertions(+), 1 deletion(-)
@@ -152,3 +152,167 @@ fn drop(&mut self) {}}}++/// Check if the PDB has valid, VRAM-backed page tables.+///+/// Returns `Err(ENOENT)` if page tables are missing or not in VRAM.+#[cfg(CONFIG_NOVA_MM_SELFTESTS)]+fncheck_valid_page_tables(mm:&GpuMm,pdb_addr:VramAddress)->Result{+usecrate::mm::pagetable::ver2::Pde;+usecrate::mm::pagetable::AperturePde;++letmutwindow=mm.pramin().window()?;+letpdb_entry_raw=window.try_read64(pdb_addr.raw())?;+letpdb_entry=Pde::new(pdb_entry_raw);++if!pdb_entry.is_valid(){+returnErr(ENOENT);+}++ifpdb_entry.aperture()!=AperturePde::VideoMemory{+returnErr(ENOENT);+}++Ok(())+}++/// Run MM subsystem self-tests during probe.+///+/// Tests page table infrastructure and `BAR1` MMIO access using the `BAR1`+/// address space. Uses the `GpuMm`'s buddy allocator / to allocate page tables+/// and test pages as needed.+#[cfg(CONFIG_NOVA_MM_SELFTESTS)]+pub(crate)fnrun_self_test(+dev:&kernel::device::Device,+mm:&GpuMm,+bar1:&crate::driver::Bar1,+bar1_pdb:u64,+mmu_version:MmuVersion,+)->Result{+usecrate::mm::vmm::Vmm;+usecrate::mm::PAGE_SIZE;+usekernel::gpu::buddy::BuddyFlags;+usekernel::gpu::buddy::GpuBuddyAllocParams;+usekernel::sizes::{+SZ_4K,+SZ_64K,//+};++// Self-tests only support MMU v2 for now.+ifmmu_version!=MmuVersion::V2{+dev_info!(+dev,+"MM: Skipping self-tests for MMU {:?} (only V2 supported)\n",+mmu_version+);+returnOk(());+}++// Test patterns.+constPATTERN_PRAMIN:u32=0xDEAD_BEEF;+constPATTERN_BAR1:u32=0xCAFE_BABE;++dev_info!(dev,"MM: Starting self-test...\n");++letpdb_addr=VramAddress::new(bar1_pdb);++// Check if initial page tables are in VRAM.+ifcheck_valid_page_tables(mm,pdb_addr).is_err(){+dev_info!(dev,"MM: Self-test SKIPPED - no valid VRAM page tables\n");+returnOk(());+}++// Setup a test page from the buddy allocator.+letalloc_params=GpuBuddyAllocParams{+start_range_address:0,+end_range_address:0,+size_bytes:SZ_4Kasu64,+min_block_size_bytes:SZ_4Kasu64,+buddy_flags:BuddyFlags::try_new(0)?,+};++lettest_page_blocks=KBox::pin_init(mm.buddy().alloc_blocks(&alloc_params),GFP_KERNEL)?;+lettest_vram_offset=test_page_blocks.iter().next().ok_or(ENOMEM)?.offset();+lettest_vram=VramAddress::new(test_vram_offset);+lettest_pfn=Pfn::from(test_vram);++// Create a VMM of size 64K to track virtual memory mappings.+letmutvmm=Vmm::new(pdb_addr,MmuVersion::V2,SZ_64Kasu64)?;++// Create a test mapping.+letmapped=vmm.map_pages(mm,&[test_pfn],None,true)?;+lettest_vfn=mapped.vfn_start;++// Pre-compute test addresses for each access path.+// Use distinct offsets within the page for read (0x100) and write (0x200) tests.+letbar1_base_offset=test_vfn.raw()asusize*PAGE_SIZE;+letbar1_read_offset:usize=bar1_base_offset+0x100;+letbar1_write_offset:usize=bar1_base_offset+0x200;+letvram_read_addr:usize=test_vram.raw()+0x100;+letvram_write_addr:usize=test_vram.raw()+0x200;++// Test 1: Write via PRAMIN, read via BAR1.+{+letmutwindow=mm.pramin().window()?;+window.try_write32(vram_read_addr,PATTERN_PRAMIN)?;+}++// Read back via BAR1 aperture.+letbar1_value=bar1.try_read32(bar1_read_offset)?;++lettest1_passed=ifbar1_value==PATTERN_PRAMIN{+true+}else{+dev_err!(+dev,+"MM: Test 1 FAILED - Expected {:#010x}, got {:#010x}\n",+PATTERN_PRAMIN,+bar1_value+);+false+};++// Test 2: Write via BAR1, read via PRAMIN.+bar1.try_write32(PATTERN_BAR1,bar1_write_offset)?;++// Read back via PRAMIN.+letpramin_value={+letmutwindow=mm.pramin().window()?;+window.try_read32(vram_write_addr)?+};++lettest2_passed=ifpramin_value==PATTERN_BAR1{+true+}else{+dev_err!(+dev,+"MM: Test 2 FAILED - Expected {:#010x}, got {:#010x}\n",+PATTERN_BAR1,+pramin_value+);+false+};++// Cleanup - invalidate PTE.+vmm.unmap_pages(mm,mapped)?;++// Test 3: Two-phase prepare/execute API.+letprepared=vmm.prepare_map(mm,1,None)?;+letmapped2=vmm.execute_map(mm,prepared,&[test_pfn],true)?;+letreadback=vmm.read_mapping(mm,mapped2.vfn_start)?;+lettest3_passed=ifreadback==Some(test_pfn){+true+}else{+dev_err!(dev,"MM: Test 3 FAILED - Two-phase map readback mismatch\n");+false+};+vmm.unmap_pages(mm,mapped2)?;++iftest1_passed&&test2_passed&&test3_passed{+dev_info!(dev,"MM: All self-tests PASSED\n");+Ok(())+}else{+dev_err!(dev,"MM: Self-tests FAILED\n");+Err(EIO)+}+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:41
Add self-tests for the PRAMIN aperture mechanism to verify correct
operation during GPU probe. The tests validate various alignment
requirements and corner cases.
The tests are default disabled and behind CONFIG_NOVA_PRAMIN_SELFTESTS.
When enabled, tests run after GSP boot during probe.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/Kconfig | 4 +-
drivers/gpu/nova-core/gpu.rs | 3 +
drivers/gpu/nova-core/mm/pramin.rs | 161 +++++++++++++++++++++++++++++
3 files changed, 166 insertions(+), 2 deletions(-)
@@ -291,3 +291,164 @@ fn drop(&mut self) {// MutexGuard drops automatically, releasing the lock.}}++/// Run PRAMIN self-tests during boot if self-tests are enabled.+#[cfg(CONFIG_NOVA_MM_SELFTESTS)]+pub(crate)fnrun_self_test(+dev:&kernel::device::Device,+bar:Arc<Devres<Bar0>>,+mmu_version:super::pagetable::MmuVersion,+)->Result{+usesuper::pagetable::MmuVersion;++// PRAMIN support is only for MMU v2 for now (Turing/Ampere/Ada).+ifmmu_version!=MmuVersion::V2{+dev_info!(+dev,+"PRAMIN: Skipping self-tests for MMU {:?} (only V2 supported)\n",+mmu_version+);+returnOk(());+}++dev_info!(dev,"PRAMIN: Starting self-test...\n");++letpramin=Arc::pin_init(Pramin::new(bar)?,GFP_KERNEL)?;+letmutwin=pramin.window()?;++// Use offset 0x1000 as test area.+letbase:usize=0x1000;++// Test 1: Read/write at byte-aligned locations.+foriin0u8..4{+letoffset=base+1+usize::from(i);// Offsets 0x1001, 0x1002, 0x1003, 0x1004+letval=0xA0+i;+win.try_write8(offset,val)?;+letread_val=win.try_read8(offset)?;+ifread_val!=val{+dev_err!(+dev,+"PRAMIN: FAIL - offset {:#x}: wrote {:#x}, read {:#x}\n",+offset,+val,+read_val+);+returnErr(EIO);+}+}++// Test 2: Write `u32` and read back as `u8`s.+lettest2_offset=base+0x10;+lettest2_val:u32=0xDEADBEEF;+win.try_write32(test2_offset,test2_val)?;++// Read back as individual bytes (little-endian: EF BE AD DE).+letexpected_bytes:[u8;4]=[0xEF,0xBE,0xAD,0xDE];+for(i,&expected)inexpected_bytes.iter().enumerate(){+letread_val=win.try_read8(test2_offset+i)?;+ifread_val!=expected{+dev_err!(+dev,+"PRAMIN: FAIL - offset {:#x}: expected {:#x}, read {:#x}\n",+test2_offset+i,+expected,+read_val+);+returnErr(EIO);+}+}++// Test 3: Window repositioning across 1MB boundaries.+// Write to offset > 1MB to trigger window slide, then verify.+lettest3_offset_a:usize=base;// First 1MB region.+lettest3_offset_b:usize=0x200000+base;// 2MB + base (different 1MB region).+letval_a:u32=0x11111111;+letval_b:u32=0x22222222;++// Write to first region.+win.try_write32(test3_offset_a,val_a)?;++// Write to second region (triggers window reposition).+win.try_write32(test3_offset_b,val_b)?;++// Read back from second region.+letread_b=win.try_read32(test3_offset_b)?;+ifread_b!=val_b{+dev_err!(+dev,+"PRAMIN: FAIL - offset {:#x}: expected {:#x}, read {:#x}\n",+test3_offset_b,+val_b,+read_b+);+returnErr(EIO);+}++// Read back from first region (triggers window reposition again).+letread_a=win.try_read32(test3_offset_a)?;+ifread_a!=val_a{+dev_err!(+dev,+"PRAMIN: FAIL - offset {:#x}: expected {:#x}, read {:#x}\n",+test3_offset_a,+val_a,+read_a+);+returnErr(EIO);+}++// Test 4: Invalid offset rejection (beyond 40-bit address space).+{+// 40-bit address space limit check.+letinvalid_offset:usize=MAX_VRAM_OFFSET+1;+letresult=win.try_read32(invalid_offset);+ifresult.is_ok(){+dev_err!(+dev,+"PRAMIN: FAIL - read at invalid offset {:#x} should have failed\n",+invalid_offset+);+returnErr(EIO);+}+}++// Test 5: Misaligned multi-byte access rejection.+// Verify that misaligned `u16`/`u32`/`u64` accesses are properly rejected.+{+// `u16` at odd offset (not 2-byte aligned).+letoffset_u16=base+0x21;+ifwin.try_write16(offset_u16,0xABCD).is_ok(){+dev_err!(+dev,+"PRAMIN: FAIL - misaligned u16 write at {:#x} should have failed\n",+offset_u16+);+returnErr(EIO);+}++// `u32` at 2-byte-aligned (not 4-byte-aligned) offset.+letoffset_u32=base+0x32;+ifwin.try_write32(offset_u32,0x12345678).is_ok(){+dev_err!(+dev,+"PRAMIN: FAIL - misaligned u32 write at {:#x} should have failed\n",+offset_u32+);+returnErr(EIO);+}++// `u64` read at 4-byte-aligned (not 8-byte-aligned) offset.+letoffset_u64=base+0x44;+ifwin.try_read64(offset_u64).is_ok(){+dev_err!(+dev,+"PRAMIN: FAIL - misaligned u64 read at {:#x} should have failed\n",+offset_u64+);+returnErr(EIO);+}+}++dev_info!(dev,"PRAMIN: All self-tests PASSED\n");+Ok(())+}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:43
Add first_usable_fb_region() to GspStaticConfigInfo to extract the first
usable FB region from GSP's fbRegionInfoParams. Usable regions are those
that are not reserved or protected.
The extracted region is stored in GetGspStaticInfoReply and exposed via
usable_fb_region() API for use by the memory subsystem.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/gsp/commands.rs | 13 +++++++++-
drivers/gpu/nova-core/gsp/fw/commands.rs | 30 ++++++++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
@@ -186,10 +186,13 @@ fn init(&self) -> impl Init<Self::Command, Self::InitError> {}}-/// The reply from the GSP to the [`GetGspInfo`] command.+/// The reply from the GSP to the [`GetGspStaticInfo`] command.pub(crate)structGetGspStaticInfoReply{gpu_name:[u8;64],bar1_pde_base:u64,+/// First usable FB region `(base, size)` for memory allocation.+#[expect(dead_code)]+usable_fb_region:Option<(u64,u64)>,}implMessageFromGspforGetGspStaticInfoReply{
@@ -235,6 +239,13 @@ pub(crate) fn gpu_name(&self) -> core::result::Result<&str, GpuNameError> {pub(crate)fnbar1_pde_base(&self)->u64{self.bar1_pde_base}++/// Returns the usable FB region `(base, size)` for driver allocation which is+/// already retrieved from the GSP.+#[expect(dead_code)]+pub(crate)fnusable_fb_region(&self)->Option<(u64,u64)>{+self.usable_fb_region+}}/// Send the [`GetGspInfo`] command and awaits for its reply.
@@ -122,6 +122,36 @@ impl GspStaticConfigInfo {pub(crate)fnbar1_pde_base(&self)->u64{self.0.bar1PdeBase}++/// Extract the first usable FB region from GSP firmware data.+///+/// Returns the first region suitable for driver memory allocation as a `(base, size)` tuple.+/// Usable regions are those that:+/// - Are not reserved for firmware internal use.+/// - Are not protected (hardware-enforced access restrictions).+/// - Support compression (can use GPU memory compression for bandwidth).+/// - Support ISO (isochronous memory for display requiring guaranteed bandwidth).+pub(crate)fnfirst_usable_fb_region(&self)->Option<(u64,u64)>{+letfb_info=&self.0.fbRegionInfoParams;+foriin0..fb_info.numFBRegionsasusize{+ifletSome(reg)=fb_info.fbRegion.get(i){+// Skip malformed regions where limit < base.+ifreg.limit<reg.base{+continue;+}+// Filter: not reserved, not protected, supports compression and ISO.+ifreg.reserved==0+&®.bProtected==0+&®.supportCompressed!=0+&®.supportISO!=0+{+letsize=reg.limit-reg.base+1;+returnSome((reg.base,size));+}+}+}+None+}}// SAFETY: Padding is explicit and will not contain uninitialized data.
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:45
Add usable_vram field to FbLayout to store the usable VRAM region for
driver allocations. This is populated after GSP boot with the region
extracted from GSP's fbRegionInfoParams.
FbLayout is now a two-phase structure:
1. new() computes firmware layout from hardware
2. set_usable_vram() populates usable region from GSP
The new usable_vram field represents the actual usable VRAM region
(~23.7GB on a 24GB GPU GA102 Ampere GPU).
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/fb.rs | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
@@ -97,6 +97,10 @@ pub(crate) fn unregister(&self, bar: &Bar0) {/// Layout of the GPU framebuffer memory.////// Contains ranges of GPU memory reserved for a given purpose during the GSP boot process.+///+/// This structure is populated in 2 steps:+/// 1. [`FbLayout::new()`] computes firmware layout from hardware.+/// 2. [`FbLayout::set_usable_vram()`] populates usable region from GSP response.#[derive(Debug)]pub(crate)structFbLayout{/// Range of the framebuffer. Starts at `0`.
@@ -111,10 +115,14 @@ pub(crate) struct FbLayout {pub(crate)elf:Range<u64>,/// WPR2 heap.pub(crate)wpr2_heap:Range<u64>,-/// WPR2 region range, starting with an instance of `GspFwWprMeta`.+/// WPR2 region range, starting with an instance of [`GspFwWprMeta`].pub(crate)wpr2:Range<u64>,+/// Non-WPR heap carved before WPR2, used by GSP firmware.pub(crate)heap:Range<u64>,pub(crate)vf_partition_count:u8,+/// Usable VRAM region for driver allocations (from GSP `fbRegionInfoParams`).+/// Initially [`None`], populated after GSP boot with usable region info.+pub(crate)usable_vram:Option<Range<u64>>,}implFbLayout{
@@ -212,6 +220,19 @@ pub(crate) fn new(chipset: Chipset, bar: &Bar0, gsp_fw: &GspFirmware) -> Result<wpr2,heap,vf_partition_count:0,+usable_vram:None,})}++/// Set the usable VRAM region from GSP response.+///+/// Called after GSP boot with the first usable region extracted from+/// GSP's `fbRegionInfoParams`. Usable regions are those that:+/// - Are not reserved for firmware internal use.+/// - Are not protected (hardware-enforced access restrictions).+/// - Support compression (can use GPU memory compression for bandwidth).+/// - Support ISO (isochronous memory for display requiring guaranteed bandwidth).+pub(crate)fnset_usable_vram(&mutself,base:u64,size:u64){+self.usable_vram=Some(base..base.saturating_add(size));+}}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:47
Add a BarUser field to struct Gpu and eagerly create it during GPU
initialization. The BarUser provides the BAR1 user interface for CPU
access to GPU virtual memory through the GPU's MMU.
The BarUser is initialized using BAR1 PDE base address from GSP static
info, MMU version and BAR1 size obtained from platform device.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/gpu.rs | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
@@ -272,6 +278,8 @@ pub(crate) struct Gpu {gsp:Gsp,/// Static GPU information from GSP.gsp_static_info:GetGspStaticInfoReply,+/// BAR1 user interface for CPU access to GPU virtual memory.+bar_user:BarUser,}implGpu{
@@ -345,6 +355,16 @@ pub(crate) fn new<'a>(})?},+// Create BAR1 user interface for CPU access to GPU virtual memory.+// Uses the BAR1 PDE base from GSP and full BAR1 size for VA space.+bar_user:{+letparams=boot_params.get();+letpdb_addr=VramAddress::new(params.bar1_pde_base);+letmmu_version=MmuVersion::from(spec.chipset.arch());+letbar1_size=pdev.resource_len(1)?;+BarUser::new(pdb_addr,mmu_version,bar1_size)?+},+bar:devres_bar,})}
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-18 21:21:47
The buddy allocator manages the actual usable VRAM. On my GA102 Ampere
with 24GB video memory, that is ~23.7GB on a 24GB GPU enabling proper
GPU memory allocation for driver use.
Cc: Nikola Djukic <redacted>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/gpu.rs | 62 ++++++++++++++++++++++-----
drivers/gpu/nova-core/gsp/boot.rs | 7 ++-
drivers/gpu/nova-core/gsp/commands.rs | 2 -
3 files changed, 57 insertions(+), 14 deletions(-)
@@ -28,6 +30,13 @@regs,};+/// Parameters extracted from GSP boot for initializing memory subsystems.+#[derive(Clone, Copy)]+structBootParams{+usable_vram_start:u64,+usable_vram_size:u64,+}+macro_rules!define_chipset{({$($variant:ident=$value:expr),*$(,)*})=>{
@@ -271,6 +280,13 @@ pub(crate) fn new<'a>(devres_bar:Arc<Devres<Bar0>>,bar:&'aBar0,)->implPinInit<Self,Error>+'a{+// Cell to share boot parameters between GSP boot and subsequent initializations.+// Contains usable VRAM region from FbLayout and BAR1 PDE base from GSP info.+letboot_params:Cell<BootParams>=Cell::new(BootParams{+usable_vram_start:0,+usable_vram_size:0,+});+try_pin_init!(Self{spec:Spec::new(pdev.as_ref(),bar).inspect(|spec|{dev_info!(pdev.as_ref(),"NVIDIA ({})\n",spec);
@@ -292,18 +308,42 @@ pub(crate) fn new<'a>(sec2_falcon:Falcon::new(pdev.as_ref(),spec.chipset)?,-// Create GPU memory manager owning memory management resources.-// This will be initialized with the usable VRAM region from GSP in a later-// patch. For now, we use a placeholder of 1MB.-mm<-GpuMm::new(devres_bar.clone(),&GpuBuddyParams{-base_offset_bytes:0,-physical_memory_size_bytes:SZ_1Masu64,-chunk_size_bytes:SZ_4Kasu64,-})?,-gsp<-Gsp::new(pdev),-gsp_static_info:{gsp.boot(pdev,bar,spec.chipset,gsp_falcon,sec2_falcon)?.0},+// Boot GSP and extract usable VRAM region for buddy allocator.+gsp_static_info:{+let(info,fb_layout)=gsp.boot(pdev,bar,spec.chipset,gsp_falcon,sec2_falcon)?;++letusable_vram=fb_layout.usable_vram.as_ref().ok_or_else(||{+dev_err!(pdev.as_ref(),"No usable FB regions found from GSP\n");+ENODEV+})?;++dev_info!(+pdev.as_ref(),+"Using FB region: {:#x}..{:#x}\n",+usable_vram.start,+usable_vram.end+);++boot_params.set(BootParams{+usable_vram_start:usable_vram.start,+usable_vram_size:usable_vram.end-usable_vram.start,+});++info+},++// Create GPU memory manager owning memory management resources.+// Uses the usable VRAM region from GSP for buddy allocator.+mm<-{+letparams=boot_params.get();+GpuMm::new(devres_bar.clone(),GpuBuddyParams{+base_offset_bytes:params.usable_vram_start,+physical_memory_size_bytes:params.usable_vram_size,+chunk_size_bytes:SZ_4Kasu64,+})?+},bar:devres_bar,})
@@ -191,7 +191,6 @@ pub(crate) struct GetGspStaticInfoReply {gpu_name:[u8;64],bar1_pde_base:u64,/// First usable FB region `(base, size)` for memory allocation.-#[expect(dead_code)]usable_fb_region:Option<(u64,u64)>,}
@@ -242,7 +241,6 @@ pub(crate) fn bar1_pde_base(&self) -> u64 {/// Returns the usable FB region `(base, size)` for driver allocation which is/// already retrieved from the GSP.-#[expect(dead_code)]pub(crate)fnusable_fb_region(&self)->Option<(u64,u64)>{self.usable_fb_region}
Hi Joel,
On Thu Feb 19, 2026 at 6:19 AM JST, Joel Fernandes wrote:
This series adds support for nova-core memory management, including VRAM
allocation, PRAMIN, VMM, page table walking, and BAR 1 read/writes.
These are critical for channel management, vGPU, and all memory
management uses.
These patches depend on the following preparatory patches:
https://lore.kernel.org/all/20260218205507.689429-1-joelagnelf@nvidia.com/T/#t
All patches (including the preparatory patches from the other series) can
be found at:
git://git.kernel.org/pub/scm/linux/kernel/git/jfern/linux.git (branch nova/mm or tag: nova-mm-v7-20260218)
Earlier series of these patches:
https://lore.kernel.org/linux-fbdev/DG0MRL6T7ACW.25G3GLJMP7PN1@kernel.org/T/#thttps://lore.kernel.org/linux-fbdev/20260210233204.790524-1-joelagnelf@nvidia.com/
Joel Fernandes (23):
nova-core: mm: Add support to use PRAMIN windows to write to VRAM
docs: gpu: nova-core: Document the PRAMIN aperture mechanism
nova-core: Add BAR1 aperture type and size constant
nova-core: gsp: Add BAR1 PDE base accessors
nova-core: mm: Add common memory management types
nova-core: mm: Add common types for all page table formats
nova-core: mm: Add MMU v2 page table types
nova-core: mm: Add MMU v3 page table types
nova-core: mm: Add unified page table entry wrapper enums
nova-core: mm: Add TLB flush support
nova-core: mm: Add GpuMm centralized memory manager
nova-core: mm: Add page table walker for MMU v2/v3
nova-core: mm: Add Virtual Memory Manager
nova-core: mm: Add virtual address range tracking to VMM
nova-core: mm: Add multi-page mapping API to VMM
nova-core: mm: Add BAR1 user interface
nova-core: gsp: Return GspStaticInfo and FbLayout from boot()
nova-core: mm: Add BAR1 memory management self-tests
nova-core: mm: Add PRAMIN aperture self-tests
nova-core: gsp: Extract usable FB region from GSP
nova-core: fb: Add usable_vram field to FbLayout
nova-core: mm: Use usable VRAM region for buddy allocator
nova-core: mm: Add BarUser to struct Gpu and create at boot
Nice series, a few overall remarks before I dive deeper into each patch:
- Use `gpu: nova-core:` (not just `nova-core:`) as the patch prefix.
- There were a few clippy warnings/rustfmt diffs when I built it.
- There are build failures introduced in patches 11 and 18 - basically
the series doesn't build from 11 until the last patch.
- This patchset is using the `module/mod.rs` pattern instead of
`module.rs` for new modules. The latter is the preferred norm IIUC.
The end result of the series looks coherent, but the ordering of patches
makes it more tedious to review than it needs to be. We start with
PRAMIN, then add some BAR1 types that are not used until 10+ patches
later, switch to getting the framebuffer size from the GSP, add tests,
go back to GSP, etc. It is difficult to follow all these intertwined
topics (even though they eventually converge) and to know which of the
previous patches are relevant to the current one.
But there is a clear path, and after moving patches around (with only
very trivial conflicts) I got the following ordering which is more
logical IMHO:
Phase 1: GSP plumbing
- nova-core: gsp: Return GspStaticInfo and FbLayout from boot()
- nova-core: gsp: Extract usable FB region from GSP
- nova-core: fb: Add usable_vram field to FbLayout
These constitute a logical change by themselves, by getting more
information from the GSP to know how much VRAM we have. You could even
display the result as a dev_info of dev_dbg to remove the only remaining
`dead_code`.
Phase 2: PRAMIN support
- nova-core: mm: Add support to use PRAMIN windows to write to VRAM
- docs: gpu: nova-core: Document the PRAMIN aperture mechanism
PRAMIN is needed by everything that follows.
Phase 3: GpuMm
- nova-core: mm: Add common memory management types
- nova-core: mm: Add TLB flush support
- nova-core: mm: Add GpuMm centralized memory manager
- nova-core: mm: Use usable VRAM region for buddy allocator
The common memory management types patch and TLB give us all we need to
introduce GpuMm, which makes it more accessible that after going through
all the page table types which it doesn't depend on. This culminates
with using the result of phase 1, which also allows you to get rid of
the temporary 1MB window hack if you rearrange the code a bit.
Phase 4: page tables / VMM
- nova-core: mm: Add common types for all page table formats
- nova-core: mm: Add MMU v2 page table types
- nova-core: mm: Add MMU v3 page table types
- nova-core: mm: Add unified page table entry wrapper enums
- nova-core: mm: Add page table walker for MMU v2/v3
- nova-core: mm: Add Virtual Memory Manager
- nova-core: mm: Add virtual address range tracking to VMM
- nova-core: mm: Add multi-page mapping API to VMM
The main course, required for BAR1 - these follow the original order.
Phase 5: BAR1
- nova-core: Add BAR1 aperture type and size constant
- nova-core: gsp: Add BAR1 PDE base accessors
- nova-core: mm: Add BAR1 user interface
- nova-core: mm: Add BarUser to struct Gpu and create at boot
All the BAR1 stuff now happens in one place. There is certainly room for
merging a few patches to avoid introducing dead code and eliminating
just after.
Phase 6: tests
- nova-core: mm: Add PRAMIN aperture self-tests
- nova-core: mm: Add BAR1 memory management self-tests
I have done this reordering locally and it seems to build fine.
I'll do a patch-by-patch review following this order, but I wanted to
share it first for other reviewers of this revision as it makes the
series more accessible IMHO.
From: Joel Fernandes <joelagnelf@nvidia.com> Date: 2026-02-19 19:48:44
Hi Alex,
Thanks for taking the time to go through the series and for the effort
of doing the reordering. Just to clarify, do you mean I should be
sending each of the phases separately for review instead of in one
series?
By any chance, do you have the tree after doing the rearrangement that I
could take a look at? That would be very helpful so I don't repeat your
rearrangement effort.
Some comments below:
Nice series, a few overall remarks before I dive deeper into each patch:
- Use `gpu: nova-core:` (not just `nova-core:`) as the patch prefix.
Done.
- There were a few clippy warnings/rustfmt diffs when I built it.
- There are build failures introduced in patches 11 and 18 - basically
the series doesn't build from 11 until the last patch.
- This patchset is using the `module/mod.rs` pattern instead of
`module.rs` for new modules. The latter is the preferred norm IIUC.
I will work on fixing these based on the reordered patches for the next
spin.
Phase 1: GSP plumbing
- nova-core: gsp: Return GspStaticInfo and FbLayout from boot()
- nova-core: gsp: Extract usable FB region from GSP
- nova-core: fb: Add usable_vram field to FbLayout
These constitute a logical change by themselves, by getting more
information from the GSP to know how much VRAM we have. You could even
display the result as a dev_info of dev_dbg to remove the only remaining
`dead_code`.
This looks good to me.
Phase 2: PRAMIN support
- nova-core: mm: Add support to use PRAMIN windows to write to VRAM
- docs: gpu: nova-core: Document the PRAMIN aperture mechanism
PRAMIN is needed by everything that follows.
This looks good to me.
Phase 3: GpuMm
- nova-core: mm: Add common memory management types
- nova-core: mm: Add TLB flush support
- nova-core: mm: Add GpuMm centralized memory manager
- nova-core: mm: Use usable VRAM region for buddy allocator
The common memory management types patch and TLB give us all we need to
introduce GpuMm, which makes it more accessible that after going through
all the page table types which it doesn't depend on. This culminates
with using the result of phase 1, which also allows you to get rid of
the temporary 1MB window hack if you rearrange the code a bit.
Yeah, that is a nice advantage!
Phase 4: page tables / VMM
- nova-core: mm: Add common types for all page table formats
- nova-core: mm: Add MMU v2 page table types
- nova-core: mm: Add MMU v3 page table types
- nova-core: mm: Add unified page table entry wrapper enums
- nova-core: mm: Add page table walker for MMU v2/v3
- nova-core: mm: Add Virtual Memory Manager
- nova-core: mm: Add virtual address range tracking to VMM
- nova-core: mm: Add multi-page mapping API to VMM
The main course, required for BAR1 - these follow the original order.
Sounds good.
Phase 5: BAR1
- nova-core: Add BAR1 aperture type and size constant
- nova-core: gsp: Add BAR1 PDE base accessors
- nova-core: mm: Add BAR1 user interface
- nova-core: mm: Add BarUser to struct Gpu and create at boot
These sound good to me.
All the BAR1 stuff now happens in one place. There is certainly room for
merging a few patches to avoid introducing dead code and eliminating
just after.
Yeah, I tried to keep the commits at a reasonable size to make review
easier. I will look into merging a little more to see where it is possible.
I have done this reordering locally and it seems to build fine.
Please share your reorder tree if you still have it. That would likely save
me a lot of effort. Thanks.
I'll do a patch-by-patch review following this order, but I wanted to
share it first for other reviewers of this revision as it makes the
series more accessible IMHO.
On Fri Feb 20, 2026 at 4:48 AM JST, Joel Fernandes wrote:
Hi Alex,
Thanks for taking the time to go through the series and for the effort
of doing the reordering. Just to clarify, do you mean I should be
sending each of the phases separately for review instead of in one
series?
By any chance, do you have the tree after doing the rearrangement that I
could take a look at? That would be very helpful so I don't repeat your
rearrangement effort.
Sure: https://github.com/Gnurou/linux/tree/review/nova-mm-v10
I have rebased it on top of your `nova-mm-v7-20260218` tag, hopefully
that makes it easier to pick.
The top-of-trees of both my branch and your tag are identical, and I've
tried to limit the changes in each patch, but you will want to quickly
check them. I have fixed the transient build errors, but not the
formatting or clippy warnings to make sure my top-of-tree stayed
identical to yours and I haven't introduced any regression.
On Fri Feb 20, 2026 at 4:48 AM JST, Joel Fernandes wrote:
Hi Alex,
Thanks for taking the time to go through the series and for the effort
of doing the reordering. Just to clarify, do you mean I should be
sending each of the phases separately for review instead of in one
series?
Sorry, forgot to reply to this. I think one series is fine now that
CList/buddy have been moved out, as it only spans one component
(nova-core) and forces us to keep the big picture in mind. The
reordering is just to enable a more granular review process and identify
the moving pieces more clearly.