[PATCH 02/13] gpu: nova-core: mm: add VramBlock and Bar1Map

From: Zhi Wang

Date: Sat Sep 05 2026 - 04:12:35 EST


GPU page table setup and VRAM-backed control structures require the
driver to allocate physical VRAM and map it into the BAR1 aperture for
CPU access. These operations are common to both the base driver and
vGPU paths.

VramBlock owns a buddy allocator allocation. Shared VramRegion views
keep that allocation alive while callers select byte ranges within a
larger preallocated block. Bar1Map retains one such region while mapping
the containing pages and bounds all CPU accesses to the requested view.
The mapping must be explicitly destroyed to release GPU VA resources and
invalidate PTEs.

Keep BarUser inline in Gpu and let short-lived BarUserAccess objects
borrow it. Bar1Map owns its mapped VA range and borrows the driver-owned
BAR1 mapping; explicit destruction returns the VA through BarUser and
GpuMm. Its MMIO accessors remain runtime checked because BAR1 and the
logical mapping have runtime sizes.

Signed-off-by: Zhi Wang <zhiw@xxxxxxxxxx>
---
drivers/gpu/nova-core/gpu.rs | 20 ++-
drivers/gpu/nova-core/mm.rs | 6 +-
drivers/gpu/nova-core/mm/bar_user.rs | 143 ++++++++++++++++++--
drivers/gpu/nova-core/mm/vram.rs | 187 +++++++++++++++++++++++++++
4 files changed, 329 insertions(+), 27 deletions(-)
create mode 100644 drivers/gpu/nova-core/mm/vram.rs

diff --git a/drivers/gpu/nova-core/gpu.rs b/drivers/gpu/nova-core/gpu.rs
index a2189589422a..430d0cc12546 100644
--- a/drivers/gpu/nova-core/gpu.rs
+++ b/drivers/gpu/nova-core/gpu.rs
@@ -317,7 +317,8 @@ pub(crate) struct Gpu<'gpu> {
/// the GSP is still operational.
mm: GpuMm<'gpu>,
/// BAR1 user interface for CPU access to GPU virtual memory.
- bar_user: Arc<BarUser<'gpu>>,
+ #[pin]
+ bar_user: BarUser<'gpu>,
/// GSP and its resources.
#[pin]
gsp_resources: GspResources<'gpu>,
@@ -508,19 +509,16 @@ pub(crate) fn new(
},

// Create BAR1 user interface for CPU access to GPU virtual memory.
- bar_user: {
+ bar_user <- {
let info = &gsp_resources.boot_result.static_info;
let pdb_addr = VramAddress::from_raw(info.bar1_pde_base);
let bar1_idx = crate::driver::bar1_resource_index(pdev)?;
let bar1_size = pdev.resource_len(bar1_idx)?;
- Arc::pin_init(
- BarUser::new(
- pdb_addr,
- gsp_resources.spec.chipset,
- bar1_size,
- bar1,
- )?,
- GFP_KERNEL,
+ BarUser::new(
+ pdb_addr,
+ gsp_resources.spec.chipset,
+ bar1_size,
+ bar1,
)?
},
})
@@ -543,7 +541,7 @@ pub(crate) fn run_selftests(self: Pin<&mut Self>, pdev: &pci::Device<device::Bou
dev,
this.mm,
regions,
- this.bar_user,
+ this.bar_user.as_ref().get_ref(),
info.bar1_pde_base,
this.spec.chipset,
) {
diff --git a/drivers/gpu/nova-core/mm.rs b/drivers/gpu/nova-core/mm.rs
index a5bc4042577b..d1cad5ad52fc 100644
--- a/drivers/gpu/nova-core/mm.rs
+++ b/drivers/gpu/nova-core/mm.rs
@@ -67,6 +67,7 @@ macro_rules! impl_pfn_bounded {
mod regs;
pub(super) mod tlb;
pub(super) mod vmm;
+pub(crate) mod vram;

/// GPU Memory Manager - owns all core MM components.
///
@@ -298,8 +299,7 @@ pub(crate) mod selftest {

use kernel::{
device,
- sizes::SizeConstants,
- sync::Arc, //
+ sizes::SizeConstants, //
};

use super::*;
@@ -309,7 +309,7 @@ pub(crate) fn run(
dev: &device::Device<device::Bound>,
mm: &mut GpuMm<'_>,
usable_fb_regions: &[Range<u64>],
- bar_user: &Arc<bar_user::BarUser<'_>>,
+ bar_user: &bar_user::BarUser<'_>,
bar1_pdb: u64,
chipset: Chipset,
) -> Result {
diff --git a/drivers/gpu/nova-core/mm/bar_user.rs b/drivers/gpu/nova-core/mm/bar_user.rs
index fb2129c47f47..adc23ac6d467 100644
--- a/drivers/gpu/nova-core/mm/bar_user.rs
+++ b/drivers/gpu/nova-core/mm/bar_user.rs
@@ -7,15 +7,13 @@
io::Io,
new_mutex,
prelude::*,
- sync::{
- Arc,
- Mutex, //
- },
+ sync::Mutex, //
};

use crate::{
driver::Bar1,
gpu::Chipset,
+ mm::vram::VramRegion,
mm::{
vmm::{
MappedRange,
@@ -60,12 +58,12 @@ pub(crate) fn new(
}

/// Map physical pages to a contiguous BAR1 virtual range.
- pub(crate) fn map(
- self: &Arc<Self>,
+ pub(crate) fn map<'access>(
+ &'access self,
mm: &mut GpuMm<'_>,
pfns: &[Pfn],
writable: bool,
- ) -> Result<BarUserAccess<'gpu>> {
+ ) -> Result<BarUserAccess<'access, 'gpu>> {
if pfns.is_empty() {
return Err(EINVAL);
}
@@ -73,22 +71,22 @@ pub(crate) fn map(
let mapped = vmm.map_pages(mm, pfns, None, writable)?;

Ok(BarUserAccess {
- bar_user: self.clone(),
+ bar_user: self,
mapped: Some(mapped),
})
}
}

/// Access object for a mapped BAR1 region.
-pub(crate) struct BarUserAccess<'gpu> {
- bar_user: Arc<BarUser<'gpu>>,
+pub(crate) struct BarUserAccess<'access, 'gpu> {
+ bar_user: &'access BarUser<'gpu>,
/// [`BarUserAccess::release`] [`Option::take`]s this; `Some` at
/// drop time means `release()` was never called.
mapped: Option<MappedRange>,
}

#[expect(dead_code)]
-impl BarUserAccess<'_> {
+impl BarUserAccess<'_, '_> {
/// Tear down the BAR1 mapping.
pub(crate) fn release(mut self, mm: &mut GpuMm<'_>) -> Result {
let mapped = self.mapped.take().ok_or(EINVAL)?;
@@ -162,7 +160,7 @@ pub(crate) fn try_write64(&self, value: u64, offset: usize) -> Result {
}
}

-impl Drop for BarUserAccess<'_> {
+impl Drop for BarUserAccess<'_, '_> {
fn drop(&mut self) {
if self.mapped.is_some() {
kernel::pr_warn!(
@@ -174,6 +172,125 @@ fn drop(&mut self) {
}
}

+/// An owned BAR1 mapping of a region within a live VRAM allocation.
+///
+/// The mapping retains the region's backing allocation until its PTEs have been removed. A
+/// logical region may begin or end within a page; the containing pages are mapped while CPU
+/// access remains bounded to the requested byte range.
+pub(crate) struct Bar1Map<'gpu> {
+ bar1: &'gpu Bar1<'gpu>,
+ mapped: MappedRange,
+ region: VramRegion,
+ page_bias: usize,
+ logical_size: usize,
+}
+
+impl<'gpu> Bar1Map<'gpu> {
+ /// Maps a VRAM region through BAR1.
+ pub(crate) fn new(
+ bar_user: &BarUser<'gpu>,
+ mm: &mut GpuMm<'_>,
+ region: VramRegion,
+ writable: bool,
+ ) -> Result<Self> {
+ let page_size = u64::try_from(PAGE_SIZE).map_err(|_| EOVERFLOW)?;
+ let region_start = region.address();
+ let region_end = region_start.checked_add(region.size()).ok_or(EOVERFLOW)?;
+ let map_start = region_start - region_start % page_size;
+ let map_end =
+ region_end.checked_add(page_size - 1).ok_or(EOVERFLOW)? / page_size * page_size;
+ let map_size = map_end.checked_sub(map_start).ok_or(EINVAL)?;
+ let num_pages = usize::try_from(map_size / page_size).map_err(|_| EOVERFLOW)?;
+ if num_pages == 0 {
+ return Err(EINVAL);
+ }
+
+ let page_bias = usize::try_from(region_start - map_start).map_err(|_| EOVERFLOW)?;
+ let logical_size = usize::try_from(region.size()).map_err(|_| EOVERFLOW)?;
+ let mut pfns = KVec::new();
+ for page in 0..num_pages {
+ let byte_offset = u64::try_from(page)
+ .map_err(|_| EOVERFLOW)?
+ .checked_mul(page_size)
+ .ok_or(EOVERFLOW)?;
+ let address = map_start.checked_add(byte_offset).ok_or(EOVERFLOW)?;
+ pfns.push(Pfn::from(VramAddress::from_raw(address)), GFP_KERNEL)?;
+ }
+
+ let mut vmm = bar_user.vmm.lock();
+ let mapped = vmm.map_pages(mm, &pfns, None, writable)?;
+
+ Ok(Self {
+ bar1: bar_user.bar1,
+ mapped,
+ region,
+ page_bias,
+ logical_size,
+ })
+ }
+
+ /// Returns the mapped physical VRAM region.
+ pub(crate) fn region(&self) -> &VramRegion {
+ &self.region
+ }
+
+ /// Returns the logical GPU virtual address visible through BAR1.
+ pub(crate) fn gpu_va_addr(&self) -> Result<u64> {
+ VirtualAddress::from(self.mapped.vfn_start)
+ .into_raw()
+ .checked_add(u64::try_from(self.page_bias).map_err(|_| EOVERFLOW)?)
+ .ok_or(EOVERFLOW)
+ }
+
+ /// Returns the requested logical mapping size.
+ pub(crate) const fn size(&self) -> usize {
+ self.logical_size
+ }
+
+ fn bar_offset(&self, offset: usize, width: usize) -> Result<usize> {
+ let logical_end = offset.checked_add(width).ok_or(EOVERFLOW)?;
+ if logical_end > self.logical_size {
+ return Err(EINVAL);
+ }
+
+ let access_offset = self.page_bias.checked_add(offset).ok_or(EOVERFLOW)?;
+ if !access_offset.is_multiple_of(width) {
+ return Err(EINVAL);
+ }
+
+ let base_vfn: usize = self.mapped.vfn_start.raw().into_safe_cast();
+ let base = base_vfn.checked_mul(PAGE_SIZE).ok_or(EOVERFLOW)?;
+ base.checked_add(access_offset).ok_or(EOVERFLOW)
+ }
+
+ // BAR1 and the logical mapping have runtime sizes, so these accessors
+ // validate the offset, width, and alignment before performing MMIO.
+ pub(crate) fn try_read32(&self, offset: usize) -> Result<u32> {
+ self.bar1
+ .try_read32(self.bar_offset(offset, size_of::<u32>())?)
+ }
+
+ pub(crate) fn try_write32(&self, value: u32, offset: usize) -> Result {
+ self.bar1
+ .try_write32(value, self.bar_offset(offset, size_of::<u32>())?)
+ }
+
+ pub(crate) fn try_write64(&self, value: u64, offset: usize) -> Result {
+ self.bar1
+ .try_write64(value, self.bar_offset(offset, size_of::<u64>())?)
+ }
+
+ /// Invalidates the PTEs and releases the BAR1 virtual address.
+ ///
+ /// The backing VRAM region remains alive until unmapping completes.
+ pub(crate) fn destroy(self, bar_user: &BarUser<'gpu>, mm: &mut GpuMm<'_>) -> Result {
+ let mut vmm = bar_user.vmm.lock();
+ let result = vmm.unmap_pages(mm, self.mapped);
+ drop(self.region);
+ result
+ }
+}
+
/// Run MM subsystem self-tests during probe.
///
/// Tests page table infrastructure and `BAR1` MMIO access using the `BAR1`
@@ -183,7 +300,7 @@ fn drop(&mut self) {
pub(crate) fn run_self_test(
dev: &device::Device<device::Bound>,
mm: &mut GpuMm<'_>,
- bar_user: &Arc<BarUser<'_>>,
+ bar_user: &BarUser<'_>,
bar1_pdb: u64,
chipset: Chipset,
) -> Result {
diff --git a/drivers/gpu/nova-core/mm/vram.rs b/drivers/gpu/nova-core/mm/vram.rs
new file mode 100644
index 000000000000..4a4bd42c9f18
--- /dev/null
+++ b/drivers/gpu/nova-core/mm/vram.rs
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+//! VRAM allocation and allocation-relative region helpers.
+
+use core::ops::Range;
+
+use kernel::{
+ gpu::buddy::{
+ AllocatedBlocks,
+ GpuBuddyAllocFlags,
+ GpuBuddyAllocMode, //
+ },
+ prelude::*,
+ ptr::Alignment,
+ sync::Arc, //
+};
+
+use super::{
+ GpuMm,
+ PAGE_SIZE, //
+};
+
+/// A physically contiguous VRAM allocation shared by its regions.
+///
+/// The buddy allocation is returned only after the block and every region or
+/// BAR1 mapping backed by it have been dropped.
+pub(crate) struct VramBlock {
+ _blocks: Pin<KBox<AllocatedBlocks>>,
+ address: u64,
+ size: u64,
+}
+
+impl VramBlock {
+ /// Return the physical start address of the allocation.
+ pub(crate) const fn address(&self) -> u64 {
+ self.address
+ }
+
+ /// Return the allocation size in bytes.
+ pub(crate) const fn size(&self) -> u64 {
+ self.size
+ }
+
+ /// Create a checked allocation-relative region.
+ pub(crate) fn region(self: &Arc<Self>, range: Range<u64>) -> Result<VramRegion> {
+ VramRegion::new(self.clone(), range)
+ }
+
+ /// Create a region spanning the complete allocation.
+ pub(crate) fn full_region(self: &Arc<Self>) -> VramRegion {
+ VramRegion {
+ backing: self.clone(),
+ address: self.address,
+ size: self.size,
+ }
+ }
+}
+
+/// A byte range within a shared [`VramBlock`].
+#[derive(Clone)]
+pub(crate) struct VramRegion {
+ backing: Arc<VramBlock>,
+ address: u64,
+ size: u64,
+}
+
+impl VramRegion {
+ fn new(backing: Arc<VramBlock>, range: Range<u64>) -> Result<Self> {
+ let size = range
+ .end
+ .checked_sub(range.start)
+ .filter(|size| *size != 0)
+ .ok_or(EINVAL)?;
+ if range.end > backing.size {
+ return Err(EINVAL);
+ }
+ let address = backing.address.checked_add(range.start).ok_or(EOVERFLOW)?;
+ backing.address.checked_add(range.end).ok_or(EOVERFLOW)?;
+
+ Ok(Self {
+ backing,
+ address,
+ size,
+ })
+ }
+
+ /// Return the physical address of the first byte in this region.
+ pub(crate) const fn address(&self) -> u64 {
+ self.address
+ }
+
+ /// Return the region size in bytes.
+ pub(crate) const fn size(&self) -> u64 {
+ self.size
+ }
+
+ /// Return a checked subregion relative to this region.
+ pub(crate) fn subregion(&self, range: Range<u64>) -> Result<Self> {
+ let size = range
+ .end
+ .checked_sub(range.start)
+ .filter(|size| *size != 0)
+ .ok_or(EINVAL)?;
+ if range.end > self.size {
+ return Err(EINVAL);
+ }
+ let address = self.address.checked_add(range.start).ok_or(EOVERFLOW)?;
+ address.checked_add(size).ok_or(EOVERFLOW)?;
+
+ Ok(Self {
+ backing: self.backing.clone(),
+ address,
+ size,
+ })
+ }
+}
+
+/// Allocate an exact VRAM range relative to a usable region's buddy base.
+pub(crate) fn alloc_vram_range(
+ mm: &GpuMm<'_>,
+ range: Range<u64>,
+ align: u64,
+) -> Result<Arc<VramBlock>> {
+ let page_size = u64::try_from(PAGE_SIZE).map_err(|_| EOVERFLOW)?;
+ let size = range
+ .end
+ .checked_sub(range.start)
+ .filter(|size| *size != 0)
+ .ok_or(EINVAL)?;
+ if !range.start.is_multiple_of(page_size) || !size.is_multiple_of(page_size) {
+ return Err(EINVAL);
+ }
+
+ let align = align.max(page_size);
+ let align_usize = usize::try_from(align).map_err(|_| EOVERFLOW)?;
+ let min_block_size = Alignment::new_checked(align_usize).ok_or(EINVAL)?;
+ let buddy = mm.buddy();
+ if range.end > buddy.size() {
+ return Err(ENOSPC);
+ }
+
+ let blocks = KBox::pin_init(
+ buddy.alloc_blocks(
+ GpuBuddyAllocMode::Range(range.clone()),
+ size,
+ min_block_size,
+ GpuBuddyAllocFlags::default(),
+ ),
+ GFP_KERNEL,
+ )?;
+
+ let mut address = None;
+ let mut allocation_end = None;
+ let mut covered = 0u64;
+ for block in blocks.as_ref().iter() {
+ let block_address = block.offset();
+ let block_size = block.size();
+ let block_end = block_address.checked_add(block_size).ok_or(EOVERFLOW)?;
+ address = Some(address.map_or(block_address, |start: u64| start.min(block_address)));
+ allocation_end = Some(allocation_end.map_or(block_end, |end: u64| end.max(block_end)));
+ covered = covered.checked_add(block_size).ok_or(EOVERFLOW)?;
+ }
+
+ let address = address.ok_or(ENOMEM)?;
+ let allocation_end = allocation_end.ok_or(ENOMEM)?;
+ let expected_address = buddy
+ .base_offset()
+ .checked_add(range.start)
+ .ok_or(EOVERFLOW)?;
+ if address != expected_address
+ || covered != size
+ || allocation_end.checked_sub(address).ok_or(EIO)? != size
+ || !address.is_multiple_of(align)
+ {
+ return Err(EIO);
+ }
+
+ Ok(Arc::new(
+ VramBlock {
+ _blocks: blocks,
+ address,
+ size,
+ },
+ GFP_KERNEL,
+ )?)
+}
--
2.53.0