[PATCH v4 16/17] gpu: nova-core: add KUnit tests for the interrupt tree and HALs
From: John Hubbard
Date: Sat Sep 12 2026 - 00:47:09 EST
The vector arithmetic and the per-architecture interrupt properties
touch no hardware, so KUnit can cover both without a GPU. A wrong leaf
count or rearm method for one family would otherwise show up only on
that family's hardware.
Add three suites:
* nova_core_gin_tree covers the vector types. It checks that a leaf
index stops at the widest supported tree, that a leaf count implies
the right number of subtrees and vectors and enumerates every leaf in
order, that a vector maps to the right leaf, bit and subtree, and
that a vector beyond an 8-leaf tree is rejected there and accepted in
a 16-leaf tree. It also exercises the subtree set operations and
checks that every supported chipset implements the subtree that
carries the GSP event.
* nova_core_gin_hal covers the CPU interrupt HAL: the 8-leaf tree on
Turing through Ada and the 16-leaf tree on Hopper and later, the
configuration-space rearm for pre-Hopper MSI, the TOP-enable rearm
for Hopper-plus MSI, and the single-subtree rearm for MSI-X on every
family.
* nova_core_falcon_hal covers the falcon interrupt HAL: which chipsets
have the retrigger register, and which routing offsets each uses.
GA100 appears on the Turing side of one split and the Ampere side of
the other, because it has the retrigger register but keeps the Turing
routing offsets.
Assisted-by: LLM
Signed-off-by: John Hubbard <jhubbard@xxxxxxxxxx>
---
drivers/gpu/nova-core/falcon/hal.rs | 48 +++++++
drivers/gpu/nova-core/irq/hal.rs | 64 ++++++++++
drivers/gpu/nova-core/irq/interrupt_tree.rs | 135 +++++++++++++++++++-
3 files changed, 246 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/nova-core/falcon/hal.rs b/drivers/gpu/nova-core/falcon/hal.rs
index 3f1f509eccbd..70bddcaf4266 100644
--- a/drivers/gpu/nova-core/falcon/hal.rs
+++ b/drivers/gpu/nova-core/falcon/hal.rs
@@ -171,3 +171,51 @@ pub(super) fn falcon_hal<E: FalconEngine + 'static>(
Ok(hal)
}
+
+#[kunit_tests(nova_core_falcon_hal)]
+mod tests {
+ use super::*;
+
+ /// Turing falcons have no retrigger register. GA100 and every later chipset have it.
+ #[test]
+ fn intr_retrigger_gate_per_arch() {
+ for chipset in [Chipset::TU102, Chipset::TU116] {
+ assert!(!falcon_intr_hal(chipset).has_intr_retrigger());
+ }
+
+ for chipset in [
+ Chipset::GA100,
+ Chipset::GA102,
+ Chipset::AD102,
+ Chipset::GH100,
+ Chipset::GB100,
+ Chipset::GB202,
+ ] {
+ assert!(falcon_intr_hal(chipset).has_intr_retrigger());
+ }
+ }
+
+ /// The RISC-V routing offsets change at GA102, so GA100 still uses the Turing ones.
+ #[test]
+ fn riscv_routing_offsets_split_at_ga102() {
+ for chipset in [Chipset::TU102, Chipset::TU116, Chipset::GA100] {
+ assert_eq!(
+ falcon_intr_hal(chipset).riscv_routing(),
+ RiscvRouting::Tu102
+ );
+ }
+
+ for chipset in [
+ Chipset::GA102,
+ Chipset::AD102,
+ Chipset::GH100,
+ Chipset::GB100,
+ Chipset::GB202,
+ ] {
+ assert_eq!(
+ falcon_intr_hal(chipset).riscv_routing(),
+ RiscvRouting::Ga102
+ );
+ }
+ }
+}
diff --git a/drivers/gpu/nova-core/irq/hal.rs b/drivers/gpu/nova-core/irq/hal.rs
index ede9a10ccda6..03852918013d 100644
--- a/drivers/gpu/nova-core/irq/hal.rs
+++ b/drivers/gpu/nova-core/irq/hal.rs
@@ -88,3 +88,67 @@ pub(super) fn cpu_interrupt_hal(chipset: Chipset) -> &'static dyn CpuInterruptHa
}
}
}
+
+#[kunit_tests(nova_core_gin_hal)]
+mod tests {
+ use super::*;
+
+ use crate::gpu::Chipset;
+
+ /// Turing through Ada implement an 8-leaf tree.
+ #[test]
+ fn pre_hopper_tree_size() {
+ for chipset in [Chipset::TU102, Chipset::GA102, Chipset::AD102] {
+ assert_eq!(cpu_interrupt_hal(chipset).leaf_count(), LeafCount::Eight);
+ }
+ }
+
+ /// Hopper and later implement a 16-leaf tree.
+ #[test]
+ fn hopper_plus_tree_size() {
+ for chipset in [Chipset::GH100, Chipset::GB100, Chipset::GB202] {
+ assert_eq!(cpu_interrupt_hal(chipset).leaf_count(), LeafCount::Sixteen);
+ }
+ }
+
+ /// MSI rearms through the configuration-space mirror only before Hopper. Hopper and later
+ /// cycle the `TOP` enables of every serviced subtree.
+ #[test]
+ fn msi_rearm_method_per_arch() {
+ for chipset in [Chipset::TU102, Chipset::GA102, Chipset::AD102] {
+ let hal = cpu_interrupt_hal(chipset);
+ assert_eq!(
+ hal.pci_irq_rearm_method(MsiType::Msi),
+ PciIrqRearmMethod::ConfigMirrorEoi
+ );
+ }
+
+ for chipset in [Chipset::GH100, Chipset::GB100, Chipset::GB202] {
+ let hal = cpu_interrupt_hal(chipset);
+ assert_eq!(
+ hal.pci_irq_rearm_method(MsiType::Msi),
+ PciIrqRearmMethod::TopEnableCycleServiced
+ );
+ }
+ }
+
+ /// MSI-X rearms one subtree on every architecture, since each subtree has its own table
+ /// entry.
+ #[test]
+ fn msix_rearms_one_subtree_on_every_arch() {
+ for chipset in [
+ Chipset::TU102,
+ Chipset::GA102,
+ Chipset::AD102,
+ Chipset::GH100,
+ Chipset::GB100,
+ Chipset::GB202,
+ ] {
+ let hal = cpu_interrupt_hal(chipset);
+ assert_eq!(
+ hal.pci_irq_rearm_method(MsiType::MsiX),
+ PciIrqRearmMethod::TopEnableCycleSubtree
+ );
+ }
+ }
+}
diff --git a/drivers/gpu/nova-core/irq/interrupt_tree.rs b/drivers/gpu/nova-core/irq/interrupt_tree.rs
index eae1a1d4b933..66b7d2b16454 100644
--- a/drivers/gpu/nova-core/irq/interrupt_tree.rs
+++ b/drivers/gpu/nova-core/irq/interrupt_tree.rs
@@ -122,7 +122,10 @@ pub(super) const fn from_raw(raw: u32) -> Self {
Self(raw)
}
- #[cfg_attr(not(CONFIG_NOVA_CORE_SELFTESTS), expect(dead_code))]
+ #[cfg_attr(
+ not(any(CONFIG_NOVA_CORE_SELFTESTS, CONFIG_KUNIT = "y")),
+ expect(dead_code)
+ )]
pub(super) const fn into_raw(self) -> u32 {
self.0
}
@@ -490,3 +493,133 @@ fn drop(&mut self) {
clear_top_enables(self.bar, self.serviced);
}
}
+
+#[kunit_tests(nova_core_gin_tree)]
+mod tests {
+ use super::*;
+
+ /// A leaf index cannot name a leaf beyond the widest supported tree.
+ #[test]
+ fn leaf_index_bounds() {
+ assert!(LeafIndex::try_new(0).is_some());
+ assert!(LeafIndex::try_new(15).is_some());
+ assert!(LeafIndex::try_new(16).is_none());
+ }
+
+ /// The subtree count, the implemented-subtree set, and the vector count follow the leaf count.
+ #[test]
+ fn leaf_count_derives_subtrees_and_vectors() {
+ assert_eq!(LeafCount::Eight.subtree_count(), 4);
+ assert_eq!(
+ Bounded::<u32, 32>::from(LeafCount::Eight.subtree_set()).get(),
+ 0x0f
+ );
+ assert_eq!(LeafCount::Eight.vector_count(), 256);
+
+ assert_eq!(LeafCount::Sixteen.subtree_count(), 8);
+ assert_eq!(
+ Bounded::<u32, 32>::from(LeafCount::Sixteen.subtree_set()).get(),
+ 0xff
+ );
+ assert_eq!(LeafCount::Sixteen.vector_count(), 512);
+ }
+
+ /// A tree enumerates every leaf that it implements, in order, and no more.
+ #[test]
+ fn leaf_count_iter_covers_the_tree() {
+ for (count, expected) in [(LeafCount::Eight, 8usize), (LeafCount::Sixteen, 16)] {
+ let mut seen = 0;
+
+ for (index, leaf) in count.iter().enumerate() {
+ assert_eq!(leaf.get(), index);
+ seen += 1;
+ }
+
+ assert_eq!(seen, expected);
+ }
+ }
+
+ /// A vector maps to its leaf, its bit within that leaf, and its subtree. The doorbell (129)
+ /// and the GSP event (155) share a subtree.
+ #[test]
+ fn vector_maps_to_leaf_bit_and_subtree() {
+ let doorbell = GinVector::new::<129>();
+ let gsp = GinVector::new::<155>();
+
+ assert_eq!(doorbell.leaf_index().get(), 4);
+ assert_eq!(doorbell.leaf_mask().into_raw(), 1 << 1);
+ assert_eq!(doorbell.subtree().index(), 2);
+
+ assert_eq!(gsp.leaf_index().get(), 4);
+ assert_eq!(gsp.leaf_mask().into_raw(), 1 << 27);
+ assert_eq!(gsp.subtree().index(), 2);
+
+ assert_eq!(doorbell.subtree(), gsp.subtree());
+ }
+
+ /// Both fixed vectors are within the 8-leaf tree, so every supported part implements them.
+ #[test]
+ fn fixed_vectors_fit_the_narrowest_tree() {
+ assert!(GinVector::new::<129>().validate(LeafCount::Eight).is_ok());
+ assert!(GinVector::new::<155>().validate(LeafCount::Eight).is_ok());
+
+ // The first vector beyond an 8-leaf tree.
+ assert!(GinVector::new::<256>().validate(LeafCount::Eight).is_err());
+ assert!(GinVector::new::<256>().validate(LeafCount::Sixteen).is_ok());
+ }
+
+ /// A subtree set reports membership, intersection, and its span from subtree 0.
+ #[test]
+ fn subtree_set_operations() {
+ let gsp = GinVector::new::<155>().subtree();
+
+ assert!(LeafCount::Eight.subtree_set().contains(gsp));
+ assert!(!LeafCount::Eight.subtree_set().is_empty());
+
+ // The GSP needs no subtree above 2, so an MSI-X request covers entries 0 through 2.
+ assert_eq!(SubtreeSet::from(gsp).span(), 3);
+
+ // A 16-leaf tree implements every subtree that an 8-leaf tree does.
+ assert_eq!(
+ LeafCount::Sixteen
+ .subtree_set()
+ .intersection(LeafCount::Eight.subtree_set()),
+ LeafCount::Eight.subtree_set()
+ );
+ }
+
+ /// Iterating a subtree set yields each subtree once, lowest index first, and nothing for an
+ /// empty set.
+ #[test]
+ fn subtree_set_iterates_its_members() {
+ assert!(LeafCount::Eight
+ .subtree_set()
+ .iter()
+ .map(Subtree::index)
+ .eq([0u32, 1, 2, 3]));
+
+ let gsp = SubtreeSet::from(GinVector::new::<155>().subtree());
+ assert!(gsp.iter().map(Subtree::index).eq([2u32]));
+
+ let empty = SubtreeSet::from(Bounded::<u32, 32>::new::<0>());
+ assert_eq!(empty.iter().count(), 0);
+ }
+
+ /// Every supported chipset implements the subtree that carries the GSP event.
+ #[test]
+ fn gsp_subtree_is_implemented_everywhere() {
+ for chipset in [
+ Chipset::TU102,
+ Chipset::GA102,
+ Chipset::AD102,
+ Chipset::GH100,
+ Chipset::GB100,
+ Chipset::GB202,
+ ] {
+ assert!(cpu_interrupt_hal(chipset)
+ .leaf_count()
+ .subtree_set()
+ .contains(crate::irq::gsp::GSP_SUBTREE));
+ }
+ }
+}
--
2.55.0