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

From: Eliot Courtney

Date: Mon Aug 10 2026 - 10:03:16 EST


From: Joel Fernandes <joelagnelf@xxxxxxxxxx>

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

Signed-off-by: Joel Fernandes <joelagnelf@xxxxxxxxxx>
[ecourtney: convert the tests to window_at(), macros, and the new types]
[ecourtney: cfg-gate the tests and expect(dead_code), not a runtime no-op]
[ecourtney: run the self-tests on all architectures, drop the chipset arg]
[ecourtney: test within a usable FB region, skip when none is large enough]
[ecourtney: report failures without failing probe, start banner at dev_dbg]
[ecourtney: removed the mm-specific Kconfig option]
Signed-off-by: Eliot Courtney <ecourtney@xxxxxxxxxx>
---
drivers/gpu/nova-core/driver.rs | 3 +
drivers/gpu/nova-core/gpu.rs | 12 ++++
drivers/gpu/nova-core/mm.rs | 40 ++++++++++-
drivers/gpu/nova-core/mm/pramin.rs | 134 +++++++++++++++++++++++++++++++++++++
4 files changed, 188 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/nova-core/driver.rs b/drivers/gpu/nova-core/driver.rs
index eddd5e7aeec8..f012bea2b5bf 100644
--- a/drivers/gpu/nova-core/driver.rs
+++ b/drivers/gpu/nova-core/driver.rs
@@ -87,6 +87,9 @@ fn probe<'bound>(
// (`try_pin_init!()` initializes fields in declaration order), lives at a pinned
// stable address, and is dropped after `gpu` (struct field drop order).
gpu <- Gpu::new(pdev, unsafe { &*core::ptr::from_ref(bar) }),
+ // Run optional GPU selftests.
+ #[cfg(CONFIG_NOVA_CORE_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 e29e07488e78..04e1e30eeed7 100644
--- a/drivers/gpu/nova-core/gpu.rs
+++ b/drivers/gpu/nova-core/gpu.rs
@@ -429,4 +429,16 @@ pub(crate) fn new(
)?,
})
}
+
+ /// Runs self-tests on the constructed [`Gpu`], logging failures without failing probe.
+ #[cfg(CONFIG_NOVA_CORE_SELFTESTS)]
+ pub(crate) fn run_selftests(self: Pin<&mut Self>, pdev: &pci::Device<device::Bound>) {
+ let this = self.project();
+ let dev = pdev.as_ref();
+ let regions = &this.gsp_static_info.usable_fb_regions;
+
+ if let Err(err) = crate::mm::selftest::run(dev, this.mm, regions) {
+ dev_err!(dev, "self-tests failed: {:?}\n", err);
+ }
+ }
}
diff --git a/drivers/gpu/nova-core/mm.rs b/drivers/gpu/nova-core/mm.rs
index 58dce211a337..9e4338c7c393 100644
--- a/drivers/gpu/nova-core/mm.rs
+++ b/drivers/gpu/nova-core/mm.rs
@@ -3,7 +3,7 @@

//! Memory management subsystems.

-#![expect(dead_code)]
+#![cfg_attr(not(CONFIG_NOVA_CORE_SELFTESTS), expect(dead_code))]

use core::{
fmt::LowerHex,
@@ -123,3 +123,41 @@ fn sub(self, rhs: Self) -> Self::Output {
self.into_raw() - rhs.into_raw()
}
}
+
+#[cfg(CONFIG_NOVA_CORE_SELFTESTS)]
+pub(crate) mod selftest {
+ use core::ops::Range;
+
+ use kernel::{
+ device,
+ sizes::SizeConstants, //
+ };
+
+ use super::*;
+
+ /// Run MM subsystem self-tests during probe.
+ pub(crate) fn run(
+ dev: &device::Device<device::Bound>,
+ mm: &mut GpuMm<'_>,
+ usable_fb_regions: &[Range<u64>],
+ ) -> Result {
+ // VRAM span the self-tests are free to overwrite, from the chosen test base.
+ const SELFTEST_SPAN: u64 = u64::SZ_64M;
+
+ let base = usable_fb_regions.iter().find_map(|region| {
+ // Tests rely on this being 8 byte aligned for checking misalignment handling.
+ let base = region.start.align_up(Alignment::new::<8>())?;
+ (base.checked_add(SELFTEST_SPAN)? <= region.end).then_some(base)
+ });
+ let Some(base) = base else {
+ dev_warn!(
+ dev,
+ "PRAMIN: skipping self-tests, no usable VRAM region of {:#x} bytes\n",
+ SELFTEST_SPAN
+ );
+ return Ok(());
+ };
+
+ pramin::selftest::run(dev, mm.pramin_mut(), VramAddress::from_raw(base))
+ }
+}
diff --git a/drivers/gpu/nova-core/mm/pramin.rs b/drivers/gpu/nova-core/mm/pramin.rs
index 20be3fc471ba..7f89c093d591 100644
--- a/drivers/gpu/nova-core/mm/pramin.rs
+++ b/drivers/gpu/nova-core/mm/pramin.rs
@@ -176,3 +176,137 @@ pub(super) fn window_at<'a, T>(
Ok(PraminAccess { view })
}
}
+
+#[cfg(CONFIG_NOVA_CORE_SELFTESTS)]
+pub(super) mod selftest {
+ use kernel::{
+ device,
+ io::io_read,
+ sizes::SizeConstants, //
+ };
+
+ use super::*;
+ use crate::{
+ selftest_assert,
+ selftest_assert_eq, //
+ };
+
+ /// Test read/write at byte granularity, at unaligned addresses.
+ fn test_byte_readwrite(
+ dev: &device::Device<device::Bound>,
+ pramin: &mut Pramin<'_>,
+ base: VramAddress,
+ ) -> Result {
+ for i in 0u8..4 {
+ let addr = base + 1 + u64::from(i);
+ pramin.window_at::<u8>(addr)?.view().write_val(0xA0 + i);
+ }
+
+ for i in 0u8..4 {
+ let addr = base + 1 + u64::from(i);
+ selftest_assert_eq!(
+ dev,
+ pramin.window_at::<u8>(addr)?.view().read_val(),
+ 0xA0 + i
+ );
+ }
+ Ok(())
+ }
+
+ /// Test writing a `u32` and reading back as individual `u8`s.
+ fn test_u32_as_bytes(
+ dev: &device::Device<device::Bound>,
+ pramin: &mut Pramin<'_>,
+ base: VramAddress,
+ ) -> Result {
+ let addr = base + 0x10;
+ let val: u32 = 0xDEADBEEF;
+ pramin.window_at::<u32>(addr)?.view().write_val(val);
+
+ let window = pramin.window_at::<[u8; 4]>(addr)?;
+ for (i, &expected) in val.to_le_bytes().iter().enumerate() {
+ selftest_assert_eq!(dev, io_read!(window.view(), [build: i]), expected);
+ }
+ Ok(())
+ }
+
+ /// Test window repositioning across 1 MiB boundaries.
+ fn test_window_reposition(
+ dev: &device::Device<device::Bound>,
+ pramin: &mut Pramin<'_>,
+ base: VramAddress,
+ ) -> Result {
+ let addr_a = base;
+ let addr_b = base + u64::SZ_2M; // base + 2 MiB (different 1 MiB region).
+ let val_a: u32 = 0x11111111;
+ let val_b: u32 = 0x22222222;
+
+ pramin.window_at::<u32>(addr_a)?.view().write_val(val_a);
+ pramin.window_at::<u32>(addr_b)?.view().write_val(val_b);
+
+ selftest_assert_eq!(
+ dev,
+ pramin.window_at::<u32>(addr_a)?.view().read_val(),
+ val_a
+ );
+ selftest_assert_eq!(
+ dev,
+ pramin.window_at::<u32>(addr_b)?.view().read_val(),
+ val_b
+ );
+ Ok(())
+ }
+
+ /// Test that offsets outside the VRAM region are rejected.
+ fn test_invalid_offset(
+ dev: &device::Device<device::Bound>,
+ pramin: &mut Pramin<'_>,
+ vram_end: VramAddress,
+ ) -> Result {
+ selftest_assert!(dev, pramin.window_at::<u32>(vram_end).is_err());
+ Ok(())
+ }
+
+ /// Test that misaligned accesses are rejected.
+ fn test_misaligned_access(
+ dev: &device::Device<device::Bound>,
+ pramin: &mut Pramin<'_>,
+ base: VramAddress,
+ ) -> Result {
+ // `u16` at odd offset (not 2-byte aligned).
+ selftest_assert!(dev, pramin.window_at::<u16>(base + 0x21).is_err());
+
+ // `u32` at 2-byte-aligned (not 4-byte-aligned) offset.
+ selftest_assert!(dev, pramin.window_at::<u32>(base + 2).is_err());
+
+ // `u64` at a 4-byte-aligned (not 8-byte-aligned) address.
+ selftest_assert!(dev, pramin.window_at::<u64>(base + 0x44).is_err());
+
+ // A `u16` view at an even address is allowed.
+ pramin.window_at::<u16>(base + 0x22)?;
+ Ok(())
+ }
+
+ /// Run PRAMIN self-tests during probe.
+ ///
+ /// `base` is the start of a driver-usable VRAM span that the tests are free to
+ /// overwrite.
+ pub(crate) fn run(
+ dev: &device::Device<device::Bound>,
+ pramin: &mut Pramin<'_>,
+ base: VramAddress,
+ ) -> Result {
+ dev_dbg!(dev, "PRAMIN: starting self-tests\n");
+
+ let vram_end = pramin.vram_range.end;
+
+ test_byte_readwrite(dev, pramin, base)?;
+ test_u32_as_bytes(dev, pramin, base)?;
+ test_window_reposition(dev, pramin, base)?;
+ test_invalid_offset(dev, pramin, vram_end)?;
+ test_misaligned_access(dev, pramin, base)?;
+
+ dev_info!(dev, "PRAMIN: self-tests passed\n");
+ Ok(())
+ }
+}

--
2.55.0