[PATCH v11 12/20] gpu: nova-core: mm: Add unified page table entry wrapper enums

Joel Fernandes
2026-04-15T21:06:38Z

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 <ndjukic@nvidia.com>
Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
---
drivers/gpu/nova-core/mm/pagetable.rs | 223 +++++++++++++++++++++
drivers/gpu/nova-core/mm/pagetable/ver2.rs | 150 ++++++++------
drivers/gpu/nova-core/mm/pagetable/ver3.rs | 120 +++++++----
3 files changed, 396 insertions(+), 97 deletions(-)

diff --git a/drivers/gpu/nova-core/mm/pagetable.rs b/drivers/gpu/nova-core/mm/pagetable.rs
index 9897818b3b07..764b9e71ae41 100644
--- a/drivers/gpu/nova-core/mm/pagetable.rs
+++ b/drivers/gpu/nova-core/mm/pagetable.rs
@@ -14,6 +14,13 @@
use kernel::num::Bounded;

use crate::gpu::Architecture;
+use crate::mm::{
+ pramin,
+ Pfn,
+ VirtualAddress,
+ VramAddress, //
+};
+use kernel::prelude::*;

/// Extracts the page table index at a given level from a virtual address.
pub(super) trait VaLevelIndex {
@@ -86,6 +93,222 @@ pub(super) const fn as_index(&self) -> u64 {
}
}

+// Trait abstractions for page table operations.
+
+/// Operations on Page Table Entries (`PTE`s).
+pub(super) trait PteOps: Copy + core::fmt::Debug {
+ /// Create a `PTE` from a raw `u64` value.
+ fn new(val: u64) -> Self;
+
+ /// Create an invalid `PTE`.
+ fn invalid() -> Self;
+
+ /// Create a valid `PTE` for video memory.
+ fn new_vram(pfn: Pfn, writable: bool) -> Self;
+
+ /// Check if this `PTE` is valid.
+ fn is_valid(&self) -> bool;
+
+ /// Get the physical frame number.
+ fn frame_number(&self) -> Pfn;
+
+ /// Get the raw `u64` value.
+ fn raw_u64(&self) -> u64;
+
+ /// Read a `PTE` from VRAM.
+ fn read(window: &mut pramin::PraminWindow<'_>, addr: VramAddress) -> Result<Self> {
+ let val = window.try_read64(addr.raw())?;
+ Ok(Self::new(val))
+ }
+
+ /// Write this `PTE` to VRAM.
+ fn write(&self, window: &mut pramin::PraminWindow<'_>, addr: VramAddress) -> Result {
+ window.try_write64(addr.raw(), self.raw_u64())
+ }
+}
+
+/// Operations on Page Directory Entries (`PDE`s).
+pub(super) trait PdeOps: Copy + core::fmt::Debug {
+ /// Create a `PDE` from a raw `u64` value.
+ fn new(val: u64) -> Self;
+
+ /// Create a valid `PDE` pointing to a page table in video memory.
+ fn new_vram(table_pfn: Pfn) -> Self;
+
+ /// Create an invalid `PDE`.
+ fn invalid() -> Self;
+
+ /// Check if this `PDE` is valid.
+ fn is_valid(&self) -> bool;
+
+ /// Get the memory aperture of this `PDE`.
+ fn aperture(&self) -> AperturePde;
+
+ /// Get the VRAM address of the page table.
+ fn table_vram_address(&self) -> VramAddress;
+
+ /// Get the raw `u64` value.
+ fn raw_u64(&self) -> u64;
+
+ /// Read a `PDE` from VRAM.
+ fn read(window: &mut pramin::PraminWindow<'_>, addr: VramAddress) -> Result<Self> {
+ let val = window.try_read64(addr.raw())?;
+ Ok(Self::new(val))
+ }
+
+ /// Write this `PDE` to VRAM.
+ fn write(&self, window: &mut pramin::PraminWindow<'_>, addr: VramAddress) -> Result {
+ window.try_write64(addr.raw(), self.raw_u64())
+ }
+
+ /// Check if this `PDE` is valid and points to video memory.
+ fn is_valid_vram(&self) -> bool {
+ self.is_valid() && self.aperture() == AperturePde::VideoMemory
+ }
+}
+
+/// Operations on Dual Page Directory Entries (128-bit `DualPde`s).
+pub(super) trait DualPdeOps: Copy + core::fmt::Debug {
+ /// Create a `DualPde` from raw 128-bit value (two `u64`s).
+ fn new(big: u64, small: u64) -> Self;
+
+ /// Create a `DualPde` with only the small page table pointer set.
+ fn new_small(table_pfn: Pfn) -> Self;
+
+ /// Check if the small page table pointer is valid.
+ fn has_small(&self) -> bool;
+
+ /// Get the small page table VRAM address.
+ fn small_vram_address(&self) -> VramAddress;
+
+ /// Get the raw `u64` value of the big PDE.
+ fn big_raw_u64(&self) -> u64;
+
+ /// Get the raw `u64` value of the small PDE.
+ fn small_raw_u64(&self) -> u64;
+
+ /// Read a dual PDE (128-bit) from VRAM.
+ fn read(window: &mut pramin::PraminWindow<'_>, addr: VramAddress) -> Result<Self> {
+ let lo = window.try_read64(addr.raw())?;
+ let hi = window.try_read64(addr.raw() + 8)?;
+ Ok(Self::new(lo, hi))
+ }
+
+ /// Write this dual PDE (128-bit) to VRAM.
+ fn write(&self, window: &mut pramin::PraminWindow<'_>, addr: VramAddress) -> Result {
+ window.try_write64(addr.raw(), self.big_raw_u64())?;
+ window.try_write64(addr.raw() + 8, self.small_raw_u64())
+ }
+}
+
+/// MMU configuration trait -- encodes version-specific constants and types.
+pub(super) trait MmuConfig: 'static {
+ /// Page Table Entry type.
+ type Pte: PteOps;
+ /// Page Directory Entry type.
+ type Pde: PdeOps;
+ /// Dual Page Directory Entry type (128-bit).
+ type DualPde: DualPdeOps;
+
+ /// PDE levels (excluding PTE level) for page table walking.
+ const PDE_LEVELS: &'static [PageTableLevel];
+ /// PTE level for this MMU version.
+ const PTE_LEVEL: PageTableLevel;
+ /// Dual PDE level (128-bit entries) for this MMU version.
+ const DUAL_PDE_LEVEL: PageTableLevel;
+
+ /// Get the number of entries per page table page for a given level.
+ fn entries_per_page(level: PageTableLevel) -> usize;
+
+ /// Extract the page table index at `level` from `va`.
+ fn level_index(va: VirtualAddress, level: u64) -> u64;
+
+ /// Get the entry size in bytes for a given level.
+ fn entry_size(level: PageTableLevel) -> usize {
+ if level == Self::DUAL_PDE_LEVEL {
+ 16 // 128-bit dual PDE
+ } else {
+ 8 // 64-bit PDE/PTE
+ }
+ }
+
+ /// Compute upper bound on page table pages needed for `num_virt_pages`.
+ ///
+ /// Walks from PTE level up through PDE levels, accumulating the tree.
+ fn pt_pages_upper_bound(num_virt_pages: usize) -> usize {
+ let mut total = 0;
+
+ // PTE pages at the leaf level.
+ let pte_epp = Self::entries_per_page(Self::PTE_LEVEL);
+ let mut pages_at_level = num_virt_pages.div_ceil(pte_epp);
+ total += pages_at_level;
+
+ // Walk PDE levels bottom-up (reverse of PDE_LEVELS).
+ for &level in Self::PDE_LEVELS.iter().rev() {
+ let epp = Self::entries_per_page(level);
+
+ // How many pages at this level do we need to point to
+ // the previous pages_at_level?
+ pages_at_level = pages_at_level.div_ceil(epp);
+ total += pages_at_level;
+ }
+
+ total
+ }
+}
+
+/// Marker struct for MMU v2 (Turing/Ampere/Ada).
+pub(super) struct MmuV2;
+
+impl MmuConfig for MmuV2 {
+ type Pte = ver2::Pte;
+ type Pde = ver2::Pde;
+ type DualPde = ver2::DualPde;
+
+ const PDE_LEVELS: &'static [PageTableLevel] = ver2::PDE_LEVELS;
+ const PTE_LEVEL: PageTableLevel = ver2::PTE_LEVEL;
+ const DUAL_PDE_LEVEL: PageTableLevel = ver2::DUAL_PDE_LEVEL;
+
+ fn entries_per_page(level: PageTableLevel) -> usize {
+ // TODO: Calculate these values from the bitfield dynamically
+ // instead of hardcoding them.
+ match level {
+ PageTableLevel::Pdb => 4, // PD3 root: bits [48:47] = 2 bits
+ PageTableLevel::L3 => 256, // PD0 dual: bits [28:21] = 8 bits
+ _ => 512, // PD2, PD1, PT: 9 bits each
+ }
+ }
+
+ fn level_index(va: VirtualAddress, level: u64) -> u64 {
+ ver2::VirtualAddressV2::new(va).level_index(level)
+ }
+}
+
+/// Marker struct for MMU v3 (Hopper and later).
+pub(super) struct MmuV3;
+
+impl MmuConfig for MmuV3 {
+ type Pte = ver3::Pte;
+ type Pde = ver3::Pde;
+ type DualPde = ver3::DualPde;
+
+ const PDE_LEVELS: &'static [PageTableLevel] = ver3::PDE_LEVELS;
+ const PTE_LEVEL: PageTableLevel = ver3::PTE_LEVEL;
+ const DUAL_PDE_LEVEL: PageTableLevel = ver3::DUAL_PDE_LEVEL;
+
+ fn entries_per_page(level: PageTableLevel) -> usize {
+ match level {
+ PageTableLevel::Pdb => 2, // PDE4 root: bit [56] = 1 bit, 2 entries
+ PageTableLevel::L4 => 256, // PDE0 dual: bits [28:21] = 8 bits
+ _ => 512, // PDE3, PDE2, PDE1, PT: 9 bits each
+ }
+ }
+
+ fn level_index(va: VirtualAddress, level: u64) -> u64 {
+ ver3::VirtualAddressV3::new(va).level_index(level)
+ }
+}
+
/// Memory aperture for Page Table Entries (`PTE`s).
///
/// Determines which memory region the `PTE` points to.
diff --git a/drivers/gpu/nova-core/mm/pagetable/ver2.rs b/drivers/gpu/nova-core/mm/pagetable/ver2.rs
index 37066688b5f1..b4ee91766a4f 100644
--- a/drivers/gpu/nova-core/mm/pagetable/ver2.rs
+++ b/drivers/gpu/nova-core/mm/pagetable/ver2.rs
@@ -16,7 +16,10 @@
use super::{
AperturePde,
AperturePte,
+ DualPdeOps,
PageTableLevel,
+ PdeOps,
+ PteOps,
VaLevelIndex, //
};
use crate::mm::{
@@ -116,12 +119,12 @@ pub(in crate::mm) struct Pte(u64) {

impl Pte {
/// Create a `PTE` from a `u64` value.
- pub(super) fn new(val: u64) -> Self {
+ pub(super) fn new_raw(val: u64) -> Self {
Self::from_raw(val)
}

/// Create a valid `PTE` for video memory.
- pub(super) fn new_vram(pfn: Pfn, writable: bool) -> Self {
+ fn new_vram_inner(pfn: Pfn, writable: bool) -> Self {
Self::zeroed()
.with_valid(true)
.with_aperture(AperturePte::VideoMemory)
@@ -129,21 +132,37 @@ pub(super) fn new_vram(pfn: Pfn, writable: bool) -> Self {
.with_read_only(!writable)
}

- /// Create an invalid `PTE`.
- pub(super) fn invalid() -> Self {
- Self::zeroed()
- }
-
/// Get the frame number based on aperture type.
- pub(super) fn frame_number(&self) -> Pfn {
+ fn frame_number_by_aperture(&self) -> Pfn {
match self.aperture() {
AperturePte::VideoMemory => self.frame_number_vid(),
_ => self.frame_number_sys(),
}
}
+}

- /// Get the raw `u64` value.
- pub(super) fn raw_u64(&self) -> u64 {
+impl PteOps for Pte {
+ fn new(val: u64) -> Self {
+ Self::from_raw(val)
+ }
+
+ fn invalid() -> Self {
+ Self::zeroed()
+ }
+
+ fn new_vram(pfn: Pfn, writable: bool) -> Self {
+ Self::new_vram_inner(pfn, writable)
+ }
+
+ fn is_valid(&self) -> bool {
+ self.valid().into_bool()
+ }
+
+ fn frame_number(&self) -> Pfn {
+ self.frame_number_by_aperture()
+ }
+
+ fn raw_u64(&self) -> u64 {
self.into_raw()
}
}
@@ -171,30 +190,18 @@ pub(in crate::mm) struct Pde(u64) {

impl Pde {
/// Create a `PDE` from a `u64` value.
- pub(super) fn new(val: u64) -> Self {
+ pub(super) fn new_raw(val: u64) -> Self {
Self::from_raw(val)
}

/// Create a valid `PDE` pointing to a page table in video memory.
- pub(super) fn new_vram(table_pfn: Pfn) -> Self {
+ fn new_vram_inner(table_pfn: Pfn) -> Self {
Self::zeroed()
.with_valid_inverted(false) // 0 = valid
.with_aperture(AperturePde::VideoMemory)
.with_table_frame_vid(table_pfn)
}

- /// Create an invalid `PDE`.
- pub(super) fn invalid() -> Self {
- Self::zeroed()
- .with_valid_inverted(true)
- .with_aperture(AperturePde::Invalid)
- }
-
- /// Check if this `PDE` is valid.
- pub(super) fn is_valid(&self) -> bool {
- !self.valid_inverted().into_bool() && self.aperture() != AperturePde::Invalid
- }
-
/// Get the table frame number based on aperture type.
fn table_frame(&self) -> Pfn {
match self.aperture() {
@@ -202,19 +209,42 @@ fn table_frame(&self) -> Pfn {
_ => self.table_frame_sys(),
}
}
+}

- /// Get the `VRAM` address of the page table.
- pub(super) fn table_vram_address(&self) -> VramAddress {
+impl PdeOps for Pde {
+ fn new(val: u64) -> Self {
+ Self::from_raw(val)
+ }
+
+ fn new_vram(table_pfn: Pfn) -> Self {
+ Self::new_vram_inner(table_pfn)
+ }
+
+ fn invalid() -> Self {
+ Self::zeroed()
+ .with_valid_inverted(true)
+ .with_aperture(AperturePde::Invalid)
+ }
+
+ fn is_valid(&self) -> bool {
+ !self.valid_inverted().into_bool() && self.aperture() != AperturePde::Invalid
+ }
+
+ fn aperture(&self) -> AperturePde {
+ // Delegate to bitfield getter (takes self by value, Copy).
+ Pde::aperture(*self)
+ }
+
+ fn table_vram_address(&self) -> VramAddress {
debug_assert!(
- self.aperture() == AperturePde::VideoMemory,
+ Pde::aperture(*self) == AperturePde::VideoMemory,
"table_vram_address called on non-VRAM PDE (aperture: {:?})",
- self.aperture()
+ Pde::aperture(*self)
);
VramAddress::from(self.table_frame_vid())
}

- /// Get the raw `u64` value of the `PDE`.
- pub(super) fn raw_u64(&self) -> u64 {
+ fn raw_u64(&self) -> u64 {
self.into_raw()
}
}
@@ -233,35 +263,9 @@ pub(in crate::mm) struct DualPde {

impl DualPde {
- /// Create a dual `PDE` from raw 128-bit value (two `u64`s).
- pub(super) fn new(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(super) fn new_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(super) fn has_small(&self) -> bool {
- self.small.is_valid()
- }
-
/// Check if the big page table pointer is valid.
fn has_big(&self) -> bool {
- self.big.is_valid()
+ PdeOps::is_valid(&self.big)
}

/// Get the small page table `Pfn`.
@@ -269,3 +273,35 @@ fn small_pfn(&self) -> Pfn {
self.small.table_frame()
}
}
+
+impl DualPdeOps for DualPde {
+ fn new(big: u64, small: u64) -> Self {
+ Self {
+ big: PdeOps::new(big),
+ small: PdeOps::new(small),
+ }
+ }
+
+ fn new_small(table_pfn: Pfn) -> Self {
+ Self {
+ big: PdeOps::new(0),
+ small: PdeOps::new_vram(table_pfn),
+ }
+ }
+
+ fn has_small(&self) -> bool {
+ PdeOps::is_valid(&self.small)
+ }
+
+ fn small_vram_address(&self) -> VramAddress {
+ PdeOps::table_vram_address(&self.small)
+ }
+
+ fn big_raw_u64(&self) -> u64 {
+ PdeOps::raw_u64(&self.big)
+ }
+
+ fn small_raw_u64(&self) -> u64 {
+ PdeOps::raw_u64(&self.small)
+ }
+}
diff --git a/drivers/gpu/nova-core/mm/pagetable/ver3.rs b/drivers/gpu/nova-core/mm/pagetable/ver3.rs
index 2f9e762c4667..1c52013e498d 100644
--- a/drivers/gpu/nova-core/mm/pagetable/ver3.rs
+++ b/drivers/gpu/nova-core/mm/pagetable/ver3.rs
@@ -25,7 +25,10 @@
use super::{
AperturePde,
AperturePte,
+ DualPdeOps,
PageTableLevel,
+ PdeOps,
+ PteOps,
VaLevelIndex, //
};
use crate::mm::{
@@ -194,12 +197,12 @@ pub(in crate::mm) struct Pte(u64) {

impl Pte {
/// Create a PTE from a `u64` value.
- pub(super) fn new(val: u64) -> Self {
+ pub(super) fn new_raw(val: u64) -> Self {
Self::from_raw(val)
}

/// Create a valid PTE for video memory.
- pub(super) fn new_vram(frame: Pfn, writable: bool) -> Self {
+ fn new_vram_inner(frame: Pfn, writable: bool) -> Self {
let pcf = if writable { PtePcf::rw() } else { PtePcf::ro() };
Self::zeroed()
.with_valid(true)
@@ -207,14 +210,30 @@ pub(super) fn new_vram(frame: Pfn, writable: bool) -> Self {
.with_pcf(pcf)
.with_frame_number(frame)
}
+}

- /// Create an invalid PTE.
- pub(super) fn invalid() -> Self {
+impl PteOps for Pte {
+ fn new(val: u64) -> Self {
+ Self::from_raw(val)
+ }
+
+ fn invalid() -> Self {
Self::zeroed()
}

- /// Get the raw `u64` value.
- pub(super) fn raw_u64(&self) -> u64 {
+ fn new_vram(pfn: Pfn, writable: bool) -> Self {
+ Self::new_vram_inner(pfn, writable)
+ }
+
+ fn is_valid(&self) -> bool {
+ self.valid().into_bool()
+ }
+
+ fn frame_number(&self) -> Pfn {
+ Pte::frame_number(*self)
+ }
+
+ fn raw_u64(&self) -> u64 {
self.into_raw()
}
}
@@ -237,40 +256,50 @@ pub(in crate::mm) struct Pde(u64) {

impl Pde {
/// Create a PDE from a `u64` value.
- pub(super) fn new(val: u64) -> Self {
+ pub(super) fn new_raw(val: u64) -> Self {
Self::from_raw(val)
}

/// Create a valid PDE pointing to a page table in video memory.
- pub(super) fn new_vram(table_pfn: Pfn) -> Self {
+ fn new_vram_inner(table_pfn: Pfn) -> Self {
Self::zeroed()
.with_is_pte(false)
.with_aperture(AperturePde::VideoMemory)
.with_table_frame(table_pfn)
}
+}

- /// Create an invalid PDE.
- pub(super) fn invalid() -> Self {
+impl PdeOps for Pde {
+ fn new(val: u64) -> Self {
+ Self::from_raw(val)
+ }
+
+ fn new_vram(table_pfn: Pfn) -> Self {
+ Self::new_vram_inner(table_pfn)
+ }
+
+ fn invalid() -> Self {
Self::zeroed().with_aperture(AperturePde::Invalid)
}

- /// Check if this PDE is valid.
- pub(super) fn is_valid(&self) -> bool {
- self.aperture() != AperturePde::Invalid
+ fn is_valid(&self) -> bool {
+ Pde::aperture(*self) != AperturePde::Invalid
}

- /// Get the VRAM address of the page table.
- pub(super) fn table_vram_address(&self) -> VramAddress {
+ fn aperture(&self) -> AperturePde {
+ Pde::aperture(*self)
+ }
+
+ fn table_vram_address(&self) -> VramAddress {
debug_assert!(
- self.aperture() == AperturePde::VideoMemory,
+ Pde::aperture(*self) == AperturePde::VideoMemory,
"table_vram_address called on non-VRAM PDE (aperture: {:?})",
- self.aperture()
+ Pde::aperture(*self)
);
VramAddress::from(self.table_frame())
}

- /// Get the raw `u64` value.
- pub(super) fn raw_u64(&self) -> u64 {
+ fn raw_u64(&self) -> u64 {
self.into_raw()
}
}
@@ -363,29 +392,40 @@ pub(in crate::mm) struct DualPde {
unsafe impl Zeroable for DualPde {}

impl DualPde {
- /// Create a dual PDE from raw 128-bit value (two `u64`s).
- pub(super) fn new(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(super) fn new_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(super) fn has_small(&self) -> bool {
- self.small.is_valid()
- }
-
/// Check if the big page table pointer is valid.
fn has_big(&self) -> bool {
self.big.is_valid()
}
}
+
+impl DualPdeOps for DualPde {
+ fn new(big: u64, small: u64) -> Self {
+ Self {
+ big: DualPdeBig::new(big),
+ small: PdeOps::new(small),
+ }
+ }
+
+ fn new_small(table_pfn: Pfn) -> Self {
+ Self {
+ big: DualPdeBig::invalid(),
+ small: PdeOps::new_vram(table_pfn),
+ }
+ }
+
+ fn has_small(&self) -> bool {
+ PdeOps::is_valid(&self.small)
+ }
+
+ fn small_vram_address(&self) -> VramAddress {
+ PdeOps::table_vram_address(&self.small)
+ }
+
+ fn big_raw_u64(&self) -> u64 {
+ self.big.raw_u64()
+ }
+
+ fn small_raw_u64(&self) -> u64 {
+ PdeOps::raw_u64(&self.small)
+ }
+}
--
2.34.1