[PATCH v1 12/12] gpu: nova-core: mm: Add PRAMIN aperture self-tests

From: Joel Fernandes

Date: Mon May 18 2026 - 14:14:01 EST


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_MM_SELFTESTS.
When enabled, tests run after GSP boot during probe.

Signed-off-by: Joel Fernandes <joelagnelf@xxxxxxxxxx>
---
drivers/gpu/nova-core/Kconfig | 10 ++
drivers/gpu/nova-core/driver.rs | 2 +
drivers/gpu/nova-core/gpu.rs | 9 ++
drivers/gpu/nova-core/mm.rs | 16 +++
drivers/gpu/nova-core/mm/pramin.rs | 214 +++++++++++++++++++++++++++++
5 files changed, 251 insertions(+)

diff --git a/drivers/gpu/nova-core/Kconfig b/drivers/gpu/nova-core/Kconfig
index f918f69e0599..abf10e82647b 100644
--- a/drivers/gpu/nova-core/Kconfig
+++ b/drivers/gpu/nova-core/Kconfig
@@ -15,3 +15,13 @@ config NOVA_CORE
This driver is work in progress and may not be functional.

If M is selected, the module will be called nova-core.
+
+config NOVA_MM_SELFTESTS
+ bool "Memory management self-tests"
+ depends on NOVA_CORE
+ help
+ Enable self-tests for the memory management subsystem. When enabled,
+ tests are run during GPU probe to verify PRAMIN aperture access,
+ page table walking, and BAR1 virtual memory mapping functionality.
+
+ This is a testing option and is default-disabled.
diff --git a/drivers/gpu/nova-core/driver.rs b/drivers/gpu/nova-core/driver.rs
index 84b0e1703150..77746d6949d7 100644
--- a/drivers/gpu/nova-core/driver.rs
+++ b/drivers/gpu/nova-core/driver.rs
@@ -96,6 +96,8 @@ fn probe(pdev: &pci::Device<Core>, _info: &Self::IdInfo) -> impl PinInit<Self, E

Ok(try_pin_init!(Self {
gpu <- Gpu::new(pdev, bar.clone(), bar.access(pdev.as_ref())?),
+ // Run optional GPU selftests.
+ _: { gpu.run_selftests(pdev)? },
_reg <- auxiliary::Registration::new(
pdev.as_ref(),
c"nova-drm",
diff --git a/drivers/gpu/nova-core/gpu.rs b/drivers/gpu/nova-core/gpu.rs
index 38544c38d660..aa047fe91054 100644
--- a/drivers/gpu/nova-core/gpu.rs
+++ b/drivers/gpu/nova-core/gpu.rs
@@ -342,4 +342,13 @@ pub(crate) fn unbind(&self, dev: &device::Device<device::Core>) {
.inspect(|bar| self.sysmem_flush.unregister(bar))
.is_err());
}
+
+ /// Run selftests on the constructed [`Gpu`].
+ pub(crate) fn run_selftests(
+ self: Pin<&mut Self>,
+ pdev: &pci::Device<device::Bound>,
+ ) -> Result {
+ crate::mm::run_mm_selftests(pdev, &self.mm, self.spec.chipset)?;
+ Ok(())
+ }
}
diff --git a/drivers/gpu/nova-core/mm.rs b/drivers/gpu/nova-core/mm.rs
index 5c1941d20d1b..08d74710f790 100644
--- a/drivers/gpu/nova-core/mm.rs
+++ b/drivers/gpu/nova-core/mm.rs
@@ -40,6 +40,7 @@ macro_rules! impl_pfn_bounded {
device,
devres::Devres,
num::Bounded,
+ pci,
prelude::*,
sync::Arc, //
};
@@ -83,6 +84,21 @@ pub(crate) fn pramin(&self) -> &pramin::Pramin {
}
}

+/// Run MM subsystem self-tests during probe.
+///
+/// No-op when `CONFIG_NOVA_MM_SELFTESTS` is not enabled.
+#[cfg_attr(not(CONFIG_NOVA_MM_SELFTESTS), allow(unused_variables))]
+pub(crate) fn run_mm_selftests(
+ pdev: &pci::Device<device::Bound>,
+ mm: &Arc<GpuMm>,
+ chipset: Chipset,
+) -> Result {
+ #[cfg(CONFIG_NOVA_MM_SELFTESTS)]
+ pramin::run_self_test(pdev.as_ref(), mm.pramin(), chipset)?;
+
+ Ok(())
+}
+
bitfield! {
/// Physical VRAM address in GPU video memory.
pub(crate) struct VramAddress(u64) {
diff --git a/drivers/gpu/nova-core/mm/pramin.rs b/drivers/gpu/nova-core/mm/pramin.rs
index 38758ca971be..73d516c91c15 100644
--- a/drivers/gpu/nova-core/mm/pramin.rs
+++ b/drivers/gpu/nova-core/mm/pramin.rs
@@ -296,3 +296,217 @@ fn compute_window(
define_pramin_write!(try_write32, u32);
define_pramin_write!(try_write64, u64);
}
+
+#[cfg(CONFIG_NOVA_MM_SELFTESTS)]
+mod selftest {
+ use super::*;
+ use crate::{
+ mm::VramAddress,
+ num::IntoSafeCast, //
+ };
+ use kernel::{
+ device,
+ prelude::*, //
+ };
+
+ /// Offset within the VRAM region to use as the self-test area.
+ const SELFTEST_REGION_OFFSET: u64 = 0x1000;
+
+ /// Test read/write at byte-aligned locations.
+ fn test_byte_readwrite(
+ dev: &kernel::device::Device,
+ win: &mut PraminWindow<'_>,
+ base: VramAddress,
+ ) -> Result {
+ for i in 0u8..4 {
+ let offset = base + 1 + u64::from(i);
+ let val = 0xA0 + i;
+ win.try_write8(offset, val)?;
+ let read_val = win.try_read8(offset)?;
+ if read_val != val {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - offset {:#x}: wrote {:#x}, read {:#x}\n",
+ offset,
+ val,
+ read_val
+ );
+ return Err(EIO);
+ }
+ }
+ Ok(())
+ }
+
+ /// Test writing a `u32` and reading back as individual `u8`s.
+ fn test_u32_as_bytes(
+ dev: &kernel::device::Device,
+ win: &mut PraminWindow<'_>,
+ base: VramAddress,
+ ) -> Result {
+ let offset = base + 0x10;
+ let val: u32 = 0xDEADBEEF;
+ win.try_write32(offset, val)?;
+
+ // Read back as individual bytes (little-endian: EF BE AD DE).
+ let expected_bytes: [u8; 4] = [0xEF, 0xBE, 0xAD, 0xDE];
+ for (i, &expected) in expected_bytes.iter().enumerate() {
+ let i_u64: u64 = i.into_safe_cast();
+ let read_val = win.try_read8(offset + i_u64)?;
+ if read_val != expected {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - offset {:#x}: expected {:#x}, read {:#x}\n",
+ offset + i_u64,
+ expected,
+ read_val
+ );
+ return Err(EIO);
+ }
+ }
+ Ok(())
+ }
+
+ /// Test window repositioning across 1MB boundaries.
+ fn test_window_reposition(
+ dev: &kernel::device::Device,
+ win: &mut PraminWindow<'_>,
+ base: VramAddress,
+ ) -> Result {
+ let offset_a = base;
+ let offset_b = base + 0x200000; // base + 2MB (different 1MB region).
+ let val_a: u32 = 0x11111111;
+ let val_b: u32 = 0x22222222;
+
+ win.try_write32(offset_a, val_a)?;
+ win.try_write32(offset_b, val_b)?;
+
+ let read_b = win.try_read32(offset_b)?;
+ if read_b != val_b {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - offset {:#x}: expected {:#x}, read {:#x}\n",
+ offset_b,
+ val_b,
+ read_b
+ );
+ return Err(EIO);
+ }
+
+ let read_a = win.try_read32(offset_a)?;
+ if read_a != val_a {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - offset {:#x}: expected {:#x}, read {:#x}\n",
+ offset_a,
+ val_a,
+ read_a
+ );
+ return Err(EIO);
+ }
+ Ok(())
+ }
+
+ /// Test that offsets outside the VRAM region are rejected.
+ fn test_invalid_offset(
+ dev: &kernel::device::Device,
+ win: &mut PraminWindow<'_>,
+ vram_end: VramAddress,
+ ) -> Result {
+ let result = win.try_read32(vram_end);
+ if result.is_ok() {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - read at invalid offset {:#x} should have failed\n",
+ vram_end
+ );
+ return Err(EIO);
+ }
+ Ok(())
+ }
+
+ /// Test that misaligned multi-byte accesses are rejected.
+ fn test_misaligned_access(
+ dev: &kernel::device::Device,
+ win: &mut PraminWindow<'_>,
+ base: VramAddress,
+ ) -> Result {
+ // `u16` at odd offset (not 2-byte aligned).
+ let offset_u16 = base + 0x21;
+ if win.try_write16(offset_u16, 0xABCD).is_ok() {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - misaligned u16 write at {:#x} should have failed\n",
+ offset_u16
+ );
+ return Err(EIO);
+ }
+
+ // `u32` at 2-byte-aligned (not 4-byte-aligned) offset.
+ let offset_u32 = base + 0x32;
+ if win.try_write32(offset_u32, 0x12345678).is_ok() {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - misaligned u32 write at {:#x} should have failed\n",
+ offset_u32
+ );
+ return Err(EIO);
+ }
+
+ // `u64` read at 4-byte-aligned (not 8-byte-aligned) offset.
+ let offset_u64 = base + 0x44;
+ if win.try_read64(offset_u64).is_ok() {
+ dev_err!(
+ dev,
+ "PRAMIN: FAIL - misaligned u64 read at {:#x} should have failed\n",
+ offset_u64
+ );
+ return Err(EIO);
+ }
+ Ok(())
+ }
+
+ /// Run PRAMIN self-tests during boot if self-tests are enabled.
+ pub(crate) fn run_self_test(
+ pdev: &device::Device<device::Bound>,
+ pramin: &Pramin,
+ chipset: crate::gpu::Chipset,
+ ) -> Result {
+ use crate::gpu::Architecture;
+
+ let dev = pdev;
+
+ // PRAMIN uses NV_PBUS_BAR0_WINDOW which is only available on pre-Hopper GPUs.
+ // Hopper+ uses NV_XAL_EP_BAR0_WINDOW instead, requiring a separate HAL that
+ // has not been implemented yet.
+ if !matches!(
+ chipset.arch(),
+ Architecture::Turing | Architecture::Ampere | Architecture::Ada
+ ) {
+ dev_info!(
+ dev,
+ "PRAMIN: Skipping self-tests for {:?} (only pre-Hopper supported)\n",
+ chipset
+ );
+ return Ok(());
+ }
+
+ dev_info!(dev, "PRAMIN: Starting self-test...\n");
+
+ let vram_region = pramin.vram_region();
+ let base = vram_region.start + SELFTEST_REGION_OFFSET;
+ let vram_end = vram_region.end;
+ let mut win = pramin.get_window(pdev)?;
+
+ test_byte_readwrite(dev, &mut win, base)?;
+ test_u32_as_bytes(dev, &mut win, base)?;
+ test_window_reposition(dev, &mut win, base)?;
+ test_invalid_offset(dev, &mut win, vram_end)?;
+ test_misaligned_access(dev, &mut win, base)?;
+
+ dev_info!(dev, "PRAMIN: All self-tests PASSED\n");
+ Ok(())
+ }
+}
+
+#[cfg(CONFIG_NOVA_MM_SELFTESTS)]
+pub(crate) use selftest::run_self_test;
--
2.34.1