* [PATCH 0/12] drm/tyr: firmware loading and MCU boot support
@ 2026-02-12 1:37 Deborah Brouwer
2026-02-12 1:37 ` [PATCH 01/12] drm/tyr: select DRM abstractions in Kconfig Deborah Brouwer
` (11 more replies)
0 siblings, 12 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
This series adds firmware loading and MCU boot support to the Tyr DRM
driver. It includes:
- A parser for the Mali CSF firmware binary format
- A kernel-managed BO type (KernelBo) for internal driver allocations
- GPU virtual memory (VM) integration using drm_gpuvm
- An MMU module and a generic slot manager
- Shmem-backed GEM support for Tyr
- Loading firmware, VM activation, and MCU boot at probe()
Base:
drm-rust-next-2026-01-26
commit cea7b66a8041 ("Documentation: nova: update pending tasks")
Dependencies:
- [PATCH v7 0/7] Rust bindings for gem shmem + iosys_map
https://lore.kernel.org/rust-for-linux/20260206223431.693765-1-lyude@redhat.com/
- [PATCH v4 0/6] Rust GPUVM immediate mode
https://lore.kernel.org/rust-for-linux/20260130-gpuvm-rust-v4-0-8364d104ff40@google.com/
- [PATCH v5 0/4] Introduce DeviceContext
https://lore.kernel.org/rust-for-linux/20260131001602.2095470-1-lyude@redhat.com/
Other Prerequisites:
This series also depends on additional prerequisite fixes not included in
this posting. The full stack (base + prerequisites + this series) is
available here:
https://gitlab.freedesktop.org/dbrouwer/linux/-/tree/dbrouwer/fw-boot
Development history / discussion:
https://gitlab.freedesktop.org/panfrost/linux/-/merge_requests/56
Beata Michalska (1):
drm/tyr: set DMA mask using GPU physical address
Boris Brezillon (6):
drm/tyr: select DRM abstractions in Kconfig
drm/tyr: rename TyrObject to BoData
drm/tyr: add MMU address space registers
drm/tyr: Add generic slot manager
drm/tyr: add MMU module
drm/tyr: add GPU virtual memory module
Daniel Almeida (1):
drm/tyr: add parser for firmware binary
Deborah Brouwer (4):
drm/tyr: move clock cleanup into Clocks Drop impl
drm/tyr: add shmem backing for GEM objects
drm/tyr: add a kernel buffer object
drm/tyr: add firmware loading and MCU boot support
drivers/gpu/drm/tyr/Kconfig | 14 +-
drivers/gpu/drm/tyr/driver.rs | 55 +-
drivers/gpu/drm/tyr/fw.rs | 263 ++++++++
drivers/gpu/drm/tyr/fw/parser.rs | 469 ++++++++++++++
drivers/gpu/drm/tyr/gem.rs | 117 +++-
drivers/gpu/drm/tyr/gpu.rs | 2 -
drivers/gpu/drm/tyr/mmu.rs | 90 +++
drivers/gpu/drm/tyr/mmu/address_space.rs | 322 ++++++++++
drivers/gpu/drm/tyr/regs.rs | 101 ++-
drivers/gpu/drm/tyr/slot.rs | 358 +++++++++++
drivers/gpu/drm/tyr/tyr.rs | 4 +
drivers/gpu/drm/tyr/vm.rs | 782 +++++++++++++++++++++++
12 files changed, 2548 insertions(+), 29 deletions(-)
create mode 100644 drivers/gpu/drm/tyr/fw.rs
create mode 100644 drivers/gpu/drm/tyr/fw/parser.rs
create mode 100644 drivers/gpu/drm/tyr/mmu.rs
create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
create mode 100644 drivers/gpu/drm/tyr/slot.rs
create mode 100644 drivers/gpu/drm/tyr/vm.rs
--
2.52.0
^ permalink raw reply [flat|nested] 63+ messages in thread
* [PATCH 01/12] drm/tyr: select DRM abstractions in Kconfig
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl Deborah Brouwer
` (10 subsequent siblings)
11 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Boris Brezillon <boris.brezillon@collabora.com>
When Tyr uses GEM_SHMEM_HELPER and GPUVM, these helpers must be enabled
or the build will fail with undefined symbol errors like:
"ld.lld: error: undefined symbol: drm_gem_shmem_free"
Introduce DRM_TYR_STATIC_DEPS and have Tyr select the required
abstractions to ensure that they are enabled when Tyr is built.
Also add MMU and IOMMU dependencies that will be required to boot
the firmware.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/Kconfig | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/tyr/Kconfig b/drivers/gpu/drm/tyr/Kconfig
index 4b55308fd2eb..c521fbd950ea 100644
--- a/drivers/gpu/drm/tyr/Kconfig
+++ b/drivers/gpu/drm/tyr/Kconfig
@@ -1,11 +1,23 @@
# SPDX-License-Identifier: GPL-2.0 or MIT
+config DRM_TYR_STATIC_DEPS
+ bool
+ select DRM_GEM_SHMEM_HELPER
+ select DRM_GPUVM
+ help
+ Ensure required DRM infrastructure is built-in when enabling Tyr
+ even if Tyr is =m
+
config DRM_TYR
tristate "Tyr (Rust DRM support for ARM Mali CSF-based GPUs)"
depends on DRM=y
depends on RUST
depends on ARM || ARM64 || COMPILE_TEST
depends on !GENERIC_ATOMIC64 # for IOMMU_IO_PGTABLE_LPAE
+ depends on MMU
+ select DRM_TYR_STATIC_DEPS
+ select IOMMU_IO_PGTABLE_LPAE
+ depends on IOMMU_SUPPORT
default n
help
Rust DRM driver for ARM Mali CSF-based GPUs.
@@ -15,5 +27,5 @@ config DRM_TYR
Note that the Mali-G68 and Mali-G78, while Valhall architecture, will
be supported with the panfrost driver as they are not CSF GPUs.
- if M is selected, the module will be called tyr. This driver is work
+ If M is selected, the module will be called tyr. This driver is work
in progress and may not be functional.
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-02-12 1:37 ` [PATCH 01/12] drm/tyr: select DRM abstractions in Kconfig Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 8:12 ` Boris Brezillon
` (2 more replies)
2026-02-12 1:37 ` [PATCH 03/12] drm/tyr: rename TyrObject to BoData Deborah Brouwer
` (9 subsequent siblings)
11 siblings, 3 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
Currently Tyr disables its clocks from TyrDrmDeviceData::drop(), which
causes them to be shut down before any other fields in TyrDrmDeviceData
are dropped. This prevents us from using the clocks when dropping the
other fields in TyrDrmDeviceData.
In order to better control when the clocks are dropped, move this cleanup
logic into a Drop implementation on the Clocks struct itself.
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/driver.rs | 23 +++++++++--------------
1 file changed, 9 insertions(+), 14 deletions(-)
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index ae4daa12b3e5..9bc6ed56c45e 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -54,7 +54,7 @@ pub(crate) struct TyrPlatformDeviceData {
_device: ARef<TyrDrmDevice>,
}
-#[pin_data(PinnedDrop)]
+#[pin_data]
pub(crate) struct TyrDrmDeviceData {
pub(crate) pdev: ARef<platform::Device>,
@@ -168,17 +168,6 @@ impl PinnedDrop for TyrPlatformDeviceData {
fn drop(self: Pin<&mut Self>) {}
}
-#[pinned_drop]
-impl PinnedDrop for TyrDrmDeviceData {
- fn drop(self: Pin<&mut Self>) {
- // TODO: the type-state pattern for Clks will fix this.
- let clks = self.clks.lock();
- clks.core.disable_unprepare();
- clks.stacks.disable_unprepare();
- clks.coregroup.disable_unprepare();
- }
-}
-
// We need to retain the name "panthor" to achieve drop-in compatibility with
// the C driver in the userspace stack.
const INFO: drm::DriverInfo = drm::DriverInfo {
@@ -202,14 +191,20 @@ impl drm::Driver for TyrDrmDriver {
}
}
-#[pin_data]
struct Clocks {
core: Clk,
stacks: OptionalClk,
coregroup: OptionalClk,
}
-#[pin_data]
+impl Drop for Clocks {
+ fn drop(&mut self) {
+ self.core.disable_unprepare();
+ self.stacks.disable_unprepare();
+ self.coregroup.disable_unprepare();
+ }
+}
+
struct Regulators {
_mali: Regulator<regulator::Enabled>,
_sram: Regulator<regulator::Enabled>,
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 03/12] drm/tyr: rename TyrObject to BoData
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-02-12 1:37 ` [PATCH 01/12] drm/tyr: select DRM abstractions in Kconfig Deborah Brouwer
2026-02-12 1:37 ` [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-20 14:04 ` Daniel Almeida
2026-02-21 9:01 ` Alice Ryhl
2026-02-12 1:37 ` [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address Deborah Brouwer
` (8 subsequent siblings)
11 siblings, 2 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Boris Brezillon <boris.brezillon@collabora.com>
Currently the GEM inner driver data object is called `TyrObject` which
is a fairly generic name. To make the code easier to understand,
rename `TyrObject` to `BoData` so that the name better reflects its
role.
No functional change is intended.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/driver.rs | 4 ++--
drivers/gpu/drm/tyr/gem.rs | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index 9bc6ed56c45e..e5eae5a73371 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -36,7 +36,7 @@
use crate::{
file::TyrDrmFileData,
- gem::TyrObject,
+ gem::BoData,
gpu,
gpu::GpuInfo,
regs, //
@@ -182,7 +182,7 @@ fn drop(self: Pin<&mut Self>) {}
impl drm::Driver for TyrDrmDriver {
type Data = TyrDrmDeviceData;
type File = TyrDrmFileData;
- type Object<R: drm::DeviceContext> = drm::gem::Object<TyrObject, R>;
+ type Object<R: drm::DeviceContext> = drm::gem::Object<BoData, R>;
const INFO: drm::DriverInfo = INFO;
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index c59214e3d0ef..c1208d332dea 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -12,9 +12,9 @@
/// GEM Object inner driver data
#[pin_data]
-pub(crate) struct TyrObject {}
+pub(crate) struct BoData {}
-impl gem::DriverObject for TyrObject {
+impl gem::DriverObject for BoData {
type Driver = TyrDrmDriver;
type Args = ();
@@ -23,6 +23,6 @@ fn new<Ctx: DeviceContext>(
_size: usize,
_args: (),
) -> impl PinInit<Self, Error> {
- try_pin_init!(TyrObject {})
+ try_pin_init!(BoData {})
}
}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (2 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 03/12] drm/tyr: rename TyrObject to BoData Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 10:16 ` Boris Brezillon
` (2 more replies)
2026-02-12 1:37 ` [PATCH 05/12] drm/tyr: add MMU address space registers Deborah Brouwer
` (7 subsequent siblings)
11 siblings, 3 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Beata Michalska <beata.michalska@arm.com>
Configure the device DMA mask during probe using the GPU's physical
address capability reported in GpuInfo. This ensures DMA allocations
use an appropriate address mask.
Signed-off-by: Beata Michalska <beata.michalska@arm.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/driver.rs | 11 +++++++++++
drivers/gpu/drm/tyr/gpu.rs | 1 -
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index e5eae5a73371..2973a8b3cc09 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -11,6 +11,10 @@
Device, //
},
devres::Devres,
+ dma::{
+ Device as DmaDevice,
+ DmaMask, //
+ },
drm,
drm::{
driver::Registration,
@@ -134,6 +138,13 @@ fn probe(
let gpu_info = GpuInfo::new(pdev.as_ref(), &iomem)?;
gpu_info.log(pdev);
+ // SAFETY: No concurrent DMA allocations or mappings can be made because
+ // the device is still being probed and therefore isn't being used by
+ // other threads of execution.
+ unsafe {
+ pdev.dma_set_mask_and_coherent(DmaMask::try_new(gpu_info.pa_bits())?)?;
+ }
+
let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
let platform: ARef<platform::Device> = pdev.into();
diff --git a/drivers/gpu/drm/tyr/gpu.rs b/drivers/gpu/drm/tyr/gpu.rs
index affca5b0dc6c..b5f11bc96fa0 100644
--- a/drivers/gpu/drm/tyr/gpu.rs
+++ b/drivers/gpu/drm/tyr/gpu.rs
@@ -141,7 +141,6 @@ pub(crate) fn va_bits(&self) -> u32 {
}
/// Returns the number of physical address bits supported by the GPU.
- #[expect(dead_code)]
pub(crate) fn pa_bits(&self) -> u32 {
(self.mmu_features >> 8) & genmask_u32(0..=7)
}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (3 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 8:16 ` Boris Brezillon
` (2 more replies)
2026-02-12 1:37 ` [PATCH 06/12] drm/tyr: add shmem backing for GEM objects Deborah Brouwer
` (6 subsequent siblings)
11 siblings, 3 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Boris Brezillon <boris.brezillon@collabora.com>
Add register definitions and constants for managing MMU address space,
including:
- Address space translation configuration (page table format, attributes)
- Memory attributes (cacheability, shareability)
- Address space commands (update, lock, flush)
- AsRegister helper for per-AS register access
These will be used by the MMU/VM manager to configure page tables and
control address space operations.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/regs.rs | 101 +++++++++++++++++++++++++++++++++++-
1 file changed, 100 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/tyr/regs.rs b/drivers/gpu/drm/tyr/regs.rs
index 611870c2e6af..9cb7ab0c806a 100644
--- a/drivers/gpu/drm/tyr/regs.rs
+++ b/drivers/gpu/drm/tyr/regs.rs
@@ -8,7 +8,10 @@
#![allow(dead_code)]
use kernel::{
- bits::bit_u32,
+ bits::{
+ bit_u32,
+ bit_u64, //
+ },
device::{
Bound,
Device, //
@@ -111,3 +114,99 @@ pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u3
pub(crate) const MMU_IRQ_CLEAR: Register<0x2004> = Register;
pub(crate) const MMU_IRQ_MASK: Register<0x2008> = Register;
pub(crate) const MMU_IRQ_STAT: Register<0x200c> = Register;
+
+pub(crate) const AS_TRANSCFG_ADRMODE_UNMAPPED: u64 = bit_u64(0);
+pub(crate) const AS_TRANSCFG_ADRMODE_AARCH64_4K: u64 = bit_u64(2) | bit_u64(1);
+pub(crate) const AS_TRANSCFG_PTW_MEMATTR_WB: u64 = bit_u64(25);
+pub(crate) const AS_TRANSCFG_PTW_RA: u64 = bit_u64(30);
+
+pub(crate) const fn as_transcfg_ina_bits(x: u64) -> u64 {
+ x << 6
+}
+
+pub(crate) const AS_MEMATTR_AARCH64_SH_MIDGARD_INNER: u32 = 0 << 4;
+pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_NC: u32 = 1 << 6;
+pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_WB: u32 = 2 << 6;
+
+pub(crate) fn as_memattr_aarch64_inner_alloc_expl(w: bool, r: bool) -> u32 {
+ (3 << 2) | (u32::from(w)) | ((u32::from(r)) << 1)
+}
+
+pub(crate) const AS_COMMAND_UPDATE: u32 = 1;
+pub(crate) const AS_COMMAND_LOCK: u32 = 2;
+pub(crate) const AS_COMMAND_FLUSH_PT: u32 = 4;
+pub(crate) const AS_COMMAND_FLUSH_MEM: u32 = 5;
+
+pub(crate) const AS_STATUS_ACTIVE: u32 = bit_u32(0);
+
+pub(crate) const AS_LOCK_REGION_MIN_SIZE: u32 = bit_u32(15);
+
+/// Maximum number of hardware address space slots.
+/// The actual number of slots available is usually much lower.
+pub(crate) const MAX_AS_REGISTERS: usize = 32;
+
+const MMU_BASE: usize = 0x2400;
+const MMU_AS_SHIFT: usize = 6;
+
+const fn mmu_as(as_nr: usize) -> usize {
+ MMU_BASE + (as_nr << MMU_AS_SHIFT)
+}
+
+pub(crate) struct AsRegister(usize);
+
+impl AsRegister {
+ fn new(as_nr: usize, offset: usize) -> Result<Self> {
+ Ok(AsRegister(mmu_as(as_nr) + offset))
+ }
+
+ #[inline]
+ pub(crate) fn read(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>) -> Result<u32> {
+ let value = (*iomem).access(dev)?.try_read32(self.0)?;
+ Ok(value)
+ }
+
+ #[inline]
+ pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u32) -> Result {
+ (*iomem).access(dev)?.try_write32(value, self.0)?;
+ Ok(())
+ }
+}
+
+pub(crate) fn as_transtab_lo(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x0)
+}
+
+pub(crate) fn as_transtab_hi(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x4)
+}
+
+pub(crate) fn as_memattr_lo(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x8)
+}
+
+pub(crate) fn as_memattr_hi(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0xc)
+}
+
+pub(crate) fn as_lockaddr_lo(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x10)
+}
+
+pub(crate) fn as_lockaddr_hi(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x14)
+}
+
+pub(crate) fn as_command(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x18)
+}
+
+pub(crate) fn as_status(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x28)
+}
+
+pub(crate) fn as_transcfg_lo(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x30)
+}
+pub(crate) fn as_transcfg_hi(as_nr: usize) -> Result<AsRegister> {
+ AsRegister::new(as_nr, 0x34)
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (4 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 05/12] drm/tyr: add MMU address space registers Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 8:17 ` Boris Brezillon
2026-02-20 14:25 ` Daniel Almeida
2026-02-12 1:37 ` [PATCH 07/12] drm/tyr: Add generic slot manager Deborah Brouwer
` (5 subsequent siblings)
11 siblings, 2 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
Add support for GEM buffer objects backed by shared memory.
This introduces the BoCreateArgs structure for passing creation parameters
including flags, and adds a flags field to BoData. A new_dummy_object()
helper is provided to create a dummy GEM object for use as a GPUVM root.
The Bo type alias is added to simplify working with Tyr's shmem-backed
GEM objects throughout the driver.
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
1 file changed, 44 insertions(+), 8 deletions(-)
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index c1208d332dea..6a58f2da88d3 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -1,28 +1,64 @@
// SPDX-License-Identifier: GPL-2.0 or MIT
+//! GEM buffer object management for the Tyr driver.
+//!
+//! This module provides buffer object (BO) management functionality using
+//! DRM's GEM subsystem with shmem backing.
use kernel::{
drm::{
gem,
+ gem::shmem,
DeviceContext, //
},
- prelude::*, //
+ prelude::*,
+ sync::aref::ARef, //
};
-use crate::driver::TyrDrmDriver;
+use crate::driver::{
+ TyrDrmDevice,
+ TyrDrmDriver, //
+};
-/// GEM Object inner driver data
+/// Tyr's DriverObject type for GEM objects.
#[pin_data]
-pub(crate) struct BoData {}
+pub(crate) struct BoData {
+ flags: u32,
+}
+
+/// Provides a way to pass arguments when creating BoData
+/// as required by the gem::DriverObject trait.
+pub(crate) struct BoCreateArgs {
+ flags: u32,
+}
impl gem::DriverObject for BoData {
type Driver = TyrDrmDriver;
- type Args = ();
+ type Args = BoCreateArgs;
fn new<Ctx: DeviceContext>(
- _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
+ _dev: &TyrDrmDevice<Ctx>,
_size: usize,
- _args: (),
+ args: BoCreateArgs,
) -> impl PinInit<Self, Error> {
- try_pin_init!(BoData {})
+ try_pin_init!(Self { flags: args.flags })
}
}
+
+/// Type alias for Tyr GEM buffer objects.
+pub(crate) type Bo = gem::shmem::Object<BoData>;
+
+/// Creates a dummy GEM object to serve as the root of a GPUVM.
+#[expect(dead_code)]
+pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
+ let bo = gem::shmem::Object::<BoData>::new(
+ ddev,
+ 4096,
+ shmem::ObjectConfig {
+ map_wc: true,
+ parent_resv_obj: None,
+ },
+ BoCreateArgs { flags: 0 },
+ )?;
+
+ Ok(bo)
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (5 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 06/12] drm/tyr: add shmem backing for GEM objects Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 10:11 ` Boris Brezillon
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
` (4 subsequent siblings)
11 siblings, 1 reply; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Boris Brezillon <boris.brezillon@collabora.com>
Introduce a generic slot manager to dynamically allocate limited hardware
slots to software "seats". It can be used for both address space (AS) and
command stream group (CSG) slots.
The slot manager initially assigns seats to its free slots. It then
continues to reuse the same slot for a seat, as long as another seat
did not start to use the slot in the interim.
When contention arises because all of the slots are allocated, the slot
manager will lazily evict and reuse slots that have become idle (if any).
The seat state is protected using the LockedBy pattern with the same lock
that guards the SlotManager. This ensures the seat state stays consistent
across slot operations.
Hardware specific behaviour can be customized using the slot manager's
`SlotOperations` trait.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/slot.rs | 359 ++++++++++++++++++++++++++++++++++++
drivers/gpu/drm/tyr/tyr.rs | 1 +
2 files changed, 360 insertions(+)
create mode 100644 drivers/gpu/drm/tyr/slot.rs
diff --git a/drivers/gpu/drm/tyr/slot.rs b/drivers/gpu/drm/tyr/slot.rs
new file mode 100644
index 000000000000..37bf8800a225
--- /dev/null
+++ b/drivers/gpu/drm/tyr/slot.rs
@@ -0,0 +1,359 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Slot management abstraction for limited hardware resources.
+//!
+//! This module provides a generic [`SlotManager`] that assigns limited hardware
+//! slots to logical "seats". A seat represents an entity (such as a vm address
+//! space) that needs access to a hardware slot.
+//!
+//! The [`SlotManager`] tracks slot allocation using sequence numbers to detect
+//! when a seat's binding has been invalidated. When a seat requests activation,
+//! the manager will either reuse the seat's existing slot (if still valid),
+//! allocate a free slot (if any are available), or evict the oldest idle slot if any
+//! slots are idle.
+//!
+//! Hardware-specific behavior is customized by implementing the [`SlotOperations`]
+//! trait, which allows callbacks when slots are activated or evicted.
+//!
+//! This is primarily used for managing address space slots in the GPU, where
+//! the number of hardware address space slots is limited.
+//!
+//! [SlotOperations]: crate::slot::SlotOperations
+//! [SlotManager]: crate::slot::SlotManager
+#![allow(dead_code)]
+
+use core::{
+ mem::take,
+ ops::{
+ Deref,
+ DerefMut, //
+ }, //
+};
+
+use kernel::{
+ prelude::*,
+ sync::LockedBy, //
+};
+
+pub(crate) struct SeatInfo {
+ /// Slot used by this seat
+ slot: u8,
+
+ /// Sequence number encoding the last time this seat was active.
+ /// We also use it to check if a slot is still bound to a seat.
+ seqno: u64,
+}
+
+#[derive(Default)]
+pub(crate) enum Seat {
+ #[expect(clippy::enum_variant_names)]
+ #[default]
+ NoSeat,
+ Active(SeatInfo),
+ Idle(SeatInfo),
+}
+
+impl Seat {
+ pub(super) fn slot(&self) -> Option<u8> {
+ match self {
+ Self::Active(info) => Some(info.slot),
+ _ => None,
+ }
+ }
+}
+
+pub(crate) trait SlotOperations {
+ type SlotData;
+
+ /// Called when a slot is being activated for a seat.
+ ///
+ /// This callback allows hardware-specific actions to be performed when a slot
+ /// becomes active, such as updating hardware registers or invalidating caches.
+ fn activate(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
+ Ok(())
+ }
+
+ /// Called when a slot is being evicted and freed.
+ ///
+ /// This callback allows hardware-specific cleanup when a slot is being
+ /// completely freed, either explicitly or when an idle slot is being
+ /// reused for a different seat. Any hardware state should be invalidated.
+ fn evict(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
+ Ok(())
+ }
+}
+
+struct SlotInfo<T> {
+ /// Type specific data attached to a slot
+ slot_data: T,
+
+ /// Sequence number from when this slot was last activated
+ seqno: u64,
+}
+
+#[derive(Default)]
+enum Slot<T> {
+ #[default]
+ Free,
+ Active(SlotInfo<T>),
+ Idle(SlotInfo<T>),
+}
+
+pub(crate) struct SlotManager<T: SlotOperations, const MAX_SLOTS: usize> {
+ /// Manager specific data
+ manager: T,
+
+ /// Number of slots actually available
+ slot_count: usize,
+
+ /// Slots
+ slots: [Slot<T::SlotData>; MAX_SLOTS],
+
+ /// Sequence number incremented each time a Seat is successfully activated
+ use_seqno: u64,
+}
+
+// A `Seat` protected by the same lock that is used to wrap the `SlotManager`.
+type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
+
+impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
+
+impl<T: SlotOperations, const MAX_SLOTS: usize> SlotManager<T, MAX_SLOTS> {
+ pub(crate) fn new(manager: T, slot_count: usize) -> Result<Self> {
+ if slot_count == 0 {
+ return Err(EINVAL);
+ }
+ if slot_count > MAX_SLOTS {
+ return Err(EINVAL);
+ }
+ Ok(Self {
+ manager,
+ slot_count,
+ slots: [const { Slot::Free }; MAX_SLOTS],
+ use_seqno: 1,
+ })
+ }
+
+ fn record_active_slot(
+ &mut self,
+ slot_idx: usize,
+ locked_seat: &LockedSeat<T, MAX_SLOTS>,
+ slot_data: T::SlotData,
+ ) -> Result {
+ let cur_seqno = self.use_seqno;
+
+ *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
+ slot: slot_idx as u8,
+ seqno: cur_seqno,
+ });
+
+ self.slots[slot_idx] = Slot::Active(SlotInfo {
+ slot_data,
+ seqno: cur_seqno,
+ });
+
+ self.use_seqno += 1;
+ Ok(())
+ }
+
+ fn activate_slot(
+ &mut self,
+ slot_idx: usize,
+ locked_seat: &LockedSeat<T, MAX_SLOTS>,
+ slot_data: T::SlotData,
+ ) -> Result {
+ self.manager.activate(slot_idx, &slot_data)?;
+ self.record_active_slot(slot_idx, locked_seat, slot_data)
+ }
+
+ fn allocate_slot(
+ &mut self,
+ locked_seat: &LockedSeat<T, MAX_SLOTS>,
+ slot_data: T::SlotData,
+ ) -> Result {
+ let slots = &self.slots[..self.slot_count];
+
+ let mut idle_slot_idx = None;
+ let mut idle_slot_seqno: u64 = 0;
+
+ for (slot_idx, slot) in slots.iter().enumerate() {
+ match slot {
+ Slot::Free => {
+ return self.activate_slot(slot_idx, locked_seat, slot_data);
+ }
+ Slot::Idle(slot_info) => {
+ if idle_slot_idx.is_none() || slot_info.seqno < idle_slot_seqno {
+ idle_slot_idx = Some(slot_idx);
+ idle_slot_seqno = slot_info.seqno;
+ }
+ }
+ Slot::Active(_) => (),
+ }
+ }
+
+ match idle_slot_idx {
+ Some(slot_idx) => {
+ // Lazily evict idle slot just before it is reused
+ if let Slot::Idle(slot_info) = &self.slots[slot_idx] {
+ self.manager.evict(slot_idx, &slot_info.slot_data)?;
+ }
+ self.activate_slot(slot_idx, locked_seat, slot_data)
+ }
+ None => {
+ pr_err!(
+ "Slot allocation failed: all {} slots in use\n",
+ self.slot_count
+ );
+ Err(EBUSY)
+ }
+ }
+ }
+
+ fn idle_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ let slot = take(&mut self.slots[slot_idx]);
+
+ if let Slot::Active(slot_info) = slot {
+ self.slots[slot_idx] = Slot::Idle(SlotInfo {
+ slot_data: slot_info.slot_data,
+ seqno: slot_info.seqno,
+ })
+ };
+
+ *locked_seat.access_mut(self) = match locked_seat.access(self) {
+ Seat::Active(seat_info) | Seat::Idle(seat_info) => Seat::Idle(SeatInfo {
+ slot: seat_info.slot,
+ seqno: seat_info.seqno,
+ }),
+ Seat::NoSeat => Seat::NoSeat,
+ };
+ Ok(())
+ }
+
+ fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ match &self.slots[slot_idx] {
+ Slot::Active(slot_info) | Slot::Idle(slot_info) => {
+ self.manager.evict(slot_idx, &slot_info.slot_data)?;
+ take(&mut self.slots[slot_idx]);
+ }
+ _ => (),
+ }
+
+ *locked_seat.access_mut(self) = Seat::NoSeat;
+ Ok(())
+ }
+
+ // Checks and updates the seat state based on the slot it points to
+ // (if any). Returns a Seat with a value matching the slot state.
+ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
+ let new_seat = match locked_seat.access(self) {
+ Seat::Active(seat_info) => {
+ let old_slot_idx = seat_info.slot as usize;
+ let slot = &self.slots[old_slot_idx];
+
+ if kernel::warn_on!(
+ !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
+ ) {
+ Seat::NoSeat
+ } else {
+ Seat::Active(SeatInfo {
+ slot: seat_info.slot,
+ seqno: seat_info.seqno,
+ })
+ }
+ }
+
+ Seat::Idle(seat_info) => {
+ let old_slot_idx = seat_info.slot as usize;
+ let slot = &self.slots[old_slot_idx];
+
+ if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
+ Seat::NoSeat
+ } else {
+ Seat::Idle(SeatInfo {
+ slot: seat_info.slot,
+ seqno: seat_info.seqno,
+ })
+ }
+ }
+
+ _ => Seat::NoSeat,
+ };
+
+ // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
+ // so that only slot.rs can change the seat state, but there might be better solutions
+ // to prevent that.
+ match &new_seat {
+ Seat::Active(seat_info) => {
+ *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
+ slot: seat_info.slot,
+ seqno: seat_info.seqno,
+ })
+ }
+ Seat::Idle(seat_info) => {
+ *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
+ slot: seat_info.slot,
+ seqno: seat_info.seqno,
+ })
+ }
+ _ => *locked_seat.access_mut(self) = Seat::NoSeat,
+ }
+
+ new_seat
+ }
+
+ pub(crate) fn activate(
+ &mut self,
+ locked_seat: &LockedSeat<T, MAX_SLOTS>,
+ slot_data: T::SlotData,
+ ) -> Result {
+ let seat = self.check_seat(locked_seat);
+ match seat {
+ Seat::Active(seat_info) | Seat::Idle(seat_info) => {
+ // With lazy eviction, if seqno matches, the hardware state is still
+ // valid for both Active and Idle slots, so just update our records
+ self.record_active_slot(seat_info.slot as usize, locked_seat, slot_data)
+ }
+ _ => self.allocate_slot(locked_seat, slot_data),
+ }
+ }
+
+ // The idle() method will be used when we start adding support for user VMs
+ #[expect(dead_code)]
+ pub(crate) fn idle(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ let seat = self.check_seat(locked_seat);
+ if let Seat::Active(seat_info) = seat {
+ self.idle_slot(seat_info.slot as usize, locked_seat)?;
+ }
+ Ok(())
+ }
+
+ /// Evict a seat from its slot, freeing up the hardware resource.
+ pub(crate) fn evict(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
+ let seat = self.check_seat(locked_seat);
+
+ match seat {
+ Seat::Active(seat_info) | Seat::Idle(seat_info) => {
+ let slot_idx = seat_info.slot as usize;
+
+ self.evict_slot(slot_idx, locked_seat)?;
+ }
+ _ => (),
+ }
+
+ Ok(())
+ }
+}
+
+impl<T: SlotOperations, const MAX_SLOTS: usize> Deref for SlotManager<T, MAX_SLOTS> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.manager
+ }
+}
+
+impl<T: SlotOperations, const MAX_SLOTS: usize> DerefMut for SlotManager<T, MAX_SLOTS> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.manager
+ }
+}
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index 6eaa2135fe07..f54b997355e0 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -12,6 +12,7 @@
mod gem;
mod gpu;
mod regs;
+mod slot;
kernel::module_platform_driver! {
type: TyrPlatformDeviceData,
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (6 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 07/12] drm/tyr: Add generic slot manager Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 10:44 ` Boris Brezillon
` (3 more replies)
2026-02-12 1:37 ` [PATCH 09/12] drm/tyr: add GPU virtual memory module Deborah Brouwer
` (3 subsequent siblings)
11 siblings, 4 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Boris Brezillon <boris.brezillon@collabora.com>
Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
SlotManager for allocating hardware address space slots. The underlying
AddressSpaceManager performs MMU operations including enabling/disabling
address spaces, flushing page tables, and locking regions for page table
updates.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/driver.rs | 3 +
drivers/gpu/drm/tyr/mmu.rs | 91 +++++++
drivers/gpu/drm/tyr/mmu/address_space.rs | 322 +++++++++++++++++++++++
drivers/gpu/drm/tyr/tyr.rs | 1 +
4 files changed, 417 insertions(+)
create mode 100644 drivers/gpu/drm/tyr/mmu.rs
create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index 2973a8b3cc09..ad5a765a6c2a 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -43,6 +43,7 @@
gem::BoData,
gpu,
gpu::GpuInfo,
+ mmu::Mmu,
regs, //
};
@@ -148,6 +149,8 @@ fn probe(
let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
let platform: ARef<platform::Device> = pdev.into();
+ let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
+
let data = try_pin_init!(TyrDrmDeviceData {
pdev: platform.clone(),
clks <- new_mutex!(Clocks {
diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
new file mode 100644
index 000000000000..8e076c35f342
--- /dev/null
+++ b/drivers/gpu/drm/tyr/mmu.rs
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Memory Management Unit (MMU) driver for the Tyr GPU.
+//!
+//! This module manages GPU address spaces and virtual memory operations through
+//! hardware MMU slots. It provides functionality for flushing page tables and
+//! managing VM updates for active address spaces.
+//!
+//! The MMU coordinates with the [`AddressSpaceManager`] to handle hardware
+//! address space allocation and page table operations, using [`SlotManager`]
+//! to track which address spaces are currently active in hardware slots.
+//!
+//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
+//! [`SlotManager`]: crate::slot::SlotManager
+#![allow(dead_code)]
+
+use core::ops::Range;
+
+use kernel::{
+ devres::Devres,
+ new_mutex,
+ platform,
+ prelude::*,
+ sync::{
+ Arc,
+ ArcBorrow,
+ Mutex, //
+ }, //
+};
+
+use crate::{
+ driver::IoMem,
+ gpu::GpuInfo,
+ mmu::address_space::{
+ AddressSpaceManager,
+ VmAsData, //
+ },
+ regs::MAX_AS_REGISTERS,
+ slot::{
+ SlotManager, //
+ }, //
+};
+
+pub(crate) mod address_space;
+
+pub(crate) type AsSlotManager = SlotManager<AddressSpaceManager, MAX_AS_REGISTERS>;
+
+#[pin_data]
+pub(crate) struct Mmu {
+ /// Manages the allocation of hardware MMU slots to GPU address spaces.
+ ///
+ /// Tracks which address spaces are currently active in hardware slots and
+ /// coordinates address space operations like flushing and VM updates.
+ #[pin]
+ pub(crate) as_manager: Mutex<AsSlotManager>,
+}
+
+impl Mmu {
+ pub(crate) fn new(
+ pdev: &platform::Device,
+ iomem: ArcBorrow<'_, Devres<IoMem>>,
+ gpu_info: &GpuInfo,
+ ) -> Result<Arc<Mmu>> {
+ let slot_count = gpu_info.as_present.count_ones().try_into()?;
+ let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
+ let mmu_init = try_pin_init!(Self{
+ as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
+ });
+ Arc::pin_init(mmu_init, GFP_KERNEL)
+ }
+
+ pub(crate) fn activate_vm(&self, vm: ArcBorrow<'_, VmAsData>) -> Result {
+ self.as_manager.lock().activate_vm(vm)
+ }
+
+ pub(crate) fn deactivate_vm(&self, vm: &VmAsData) -> Result {
+ self.as_manager.lock().deactivate_vm(vm)
+ }
+
+ pub(crate) fn flush_vm(&self, vm: &VmAsData) -> Result {
+ self.as_manager.lock().flush_vm(vm)
+ }
+
+ pub(crate) fn start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result {
+ self.as_manager.lock().start_vm_update(vm, region)
+ }
+
+ pub(crate) fn end_vm_update(&self, vm: &VmAsData) -> Result {
+ self.as_manager.lock().end_vm_update(vm)
+ }
+}
diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
new file mode 100644
index 000000000000..60e9a79112f0
--- /dev/null
+++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! GPU address space management and hardware operations.
+//!
+//! This module manages GPU hardware address spaces, including configuration,
+//! command submission, and page table update regions. It handles the hardware
+//! interaction for MMU operations through MMIO register access.
+//!
+//! The [`AddressSpaceManager`] implements [`SlotOperations`] to integrate with
+//! the slot management system, enabling and configuring address spaces in the
+//! hardware slots as needed.
+//!
+//! [`SlotOperations`]: crate::slot::SlotOperations
+
+use core::ops::Range;
+
+use kernel::{
+ bits::*,
+ device::{
+ Bound,
+ Device, //
+ },
+ devres::Devres,
+ error::Result,
+ io,
+ iommu::pgtable::{
+ IoPageTable,
+ ARM64LPAES1, //
+ },
+ platform,
+ prelude::*,
+ sync::{
+ aref::ARef,
+ Arc,
+ ArcBorrow,
+ LockedBy, //
+ },
+ time::Delta, //
+};
+
+use crate::{
+ driver::IoMem,
+ mmu::{
+ AsSlotManager,
+ Mmu, //
+ },
+ regs::*,
+ slot::{
+ Seat,
+ SlotOperations, //
+ }, //
+};
+
+/// Hardware address space configuration registers.
+///
+/// Contains the values to be written to the GPU's AS registers when
+/// activating this address space.
+#[derive(Clone, Copy)]
+pub(crate) struct AddressSpaceConfig {
+ pub(crate) transcfg: u64,
+ pub(crate) transtab: u64,
+ pub(crate) memattr: u64,
+}
+
+/// Any resource/information that will be used by the AddressSpaceManager
+/// to make a VM active is present in VmAsData.
+///
+/// On activation, we will pass an Arc<VmAsData> that will be stored in
+/// the slot to make sure the page table and the underlying resources
+/// (pages) used by the AS slot won't go away while the MMU points to
+/// those.
+pub(crate) struct VmAsData {
+ /// Tracks this VM's binding to a hardware address space slot.
+ as_seat: LockedBy<Seat, AsSlotManager>,
+ /// Hardware configuration for this address space.
+ as_config: AddressSpaceConfig,
+ /// Page table (managed by devres).
+ pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
+}
+
+impl VmAsData {
+ pub(crate) fn new(
+ mmu: &Mmu,
+ as_config: AddressSpaceConfig,
+ page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
+ ) -> VmAsData {
+ Self {
+ as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
+ as_config,
+ page_table,
+ }
+ }
+}
+
+/// Manages GPU hardware address spaces via MMIO register operations.
+pub(crate) struct AddressSpaceManager {
+ pdev: ARef<platform::Device>,
+ iomem: Arc<Devres<IoMem>>,
+ /// Bitmask of available address space slots from GPU_AS_PRESENT register
+ as_present: u32,
+}
+
+impl SlotOperations for AddressSpaceManager {
+ type SlotData = Arc<VmAsData>;
+
+ fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) -> Result {
+ self.as_enable(slot_idx, &slot_data.as_config)
+ }
+
+ fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
+ if self.iomem.try_access().is_some() {
+ let _ = self.as_flush(slot_idx);
+ let _ = self.as_disable(slot_idx);
+ }
+ Ok(())
+ }
+}
+
+impl AddressSpaceManager {
+ pub(super) fn new(
+ pdev: &platform::Device,
+ iomem: ArcBorrow<'_, Devres<IoMem>>,
+ as_present: u32,
+ ) -> Result<AddressSpaceManager> {
+ Ok(Self {
+ pdev: pdev.into(),
+ iomem: iomem.into(),
+ as_present,
+ })
+ }
+
+ fn dev(&self) -> &Device<Bound> {
+ // SAFETY: pdev is a bound device.
+ unsafe { self.pdev.as_ref().as_bound() }
+ }
+
+ fn validate_as_slot(&self, as_nr: usize) -> Result {
+ if as_nr >= MAX_AS_REGISTERS {
+ pr_err!(
+ "AS slot {} out of valid range (max {})\n",
+ as_nr,
+ MAX_AS_REGISTERS
+ );
+ return Err(EINVAL);
+ }
+
+ if (self.as_present & (1 << as_nr)) == 0 {
+ pr_err!(
+ "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
+ as_nr,
+ self.as_present
+ );
+ return Err(EINVAL);
+ }
+
+ Ok(())
+ }
+
+ fn as_wait_ready(&self, as_nr: usize) -> Result {
+ let op = || as_status(as_nr)?.read(self.dev(), &self.iomem);
+ let cond = |status: &u32| -> bool { *status & AS_STATUS_ACTIVE == 0 };
+ let _ =
+ io::poll::read_poll_timeout(op, cond, Delta::from_millis(0), Delta::from_millis(10))?;
+
+ Ok(())
+ }
+
+ fn as_send_cmd(&mut self, as_nr: usize, cmd: u32) -> Result {
+ self.as_wait_ready(as_nr)?;
+ as_command(as_nr)?.write(self.dev(), &self.iomem, cmd)?;
+ Ok(())
+ }
+
+ fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: u32) -> Result {
+ self.as_send_cmd(as_nr, cmd)?;
+ self.as_wait_ready(as_nr)?;
+ Ok(())
+ }
+
+ fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig) -> Result {
+ self.validate_as_slot(as_nr)?;
+
+ let transtab = as_config.transtab;
+ let transcfg = as_config.transcfg;
+ let memattr = as_config.memattr;
+
+ let transtab_lo = (transtab & 0xffffffff) as u32;
+ let transtab_hi = (transtab >> 32) as u32;
+
+ let transcfg_lo = (transcfg & 0xffffffff) as u32;
+ let transcfg_hi = (transcfg >> 32) as u32;
+
+ let memattr_lo = (memattr & 0xffffffff) as u32;
+ let memattr_hi = (memattr >> 32) as u32;
+
+ let dev = self.dev();
+ as_transtab_lo(as_nr)?.write(dev, &self.iomem, transtab_lo)?;
+ as_transtab_hi(as_nr)?.write(dev, &self.iomem, transtab_hi)?;
+
+ as_transcfg_lo(as_nr)?.write(dev, &self.iomem, transcfg_lo)?;
+ as_transcfg_hi(as_nr)?.write(dev, &self.iomem, transcfg_hi)?;
+
+ as_memattr_lo(as_nr)?.write(dev, &self.iomem, memattr_lo)?;
+ as_memattr_hi(as_nr)?.write(dev, &self.iomem, memattr_hi)?;
+
+ self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
+
+ Ok(())
+ }
+
+ fn as_disable(&mut self, as_nr: usize) -> Result {
+ self.validate_as_slot(as_nr)?;
+
+ // Flush AS before disabling
+ self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_MEM)?;
+
+ let dev = self.dev();
+ as_transtab_lo(as_nr)?.write(dev, &self.iomem, 0)?;
+ as_transtab_hi(as_nr)?.write(dev, &self.iomem, 0)?;
+
+ as_memattr_lo(as_nr)?.write(dev, &self.iomem, 0)?;
+ as_memattr_hi(as_nr)?.write(dev, &self.iomem, 0)?;
+
+ as_transcfg_lo(as_nr)?.write(dev, &self.iomem, AS_TRANSCFG_ADRMODE_UNMAPPED as u32)?;
+ as_transcfg_hi(as_nr)?.write(dev, &self.iomem, 0)?;
+
+ self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
+
+ Ok(())
+ }
+
+ fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
+ self.validate_as_slot(as_nr)?;
+
+ // The locked region is a naturally aligned power of 2 block encoded as
+ // log2 minus(1).
+ //
+ // Calculate the desired start/end and look for the highest bit which
+ // differs. The smallest naturally aligned block must include this bit
+ // change, the desired region starts with this bit (and subsequent bits)
+ // zeroed and ends with the bit (and subsequent bits) set to one.
+ let region_width = core::cmp::max(
+ 64 - (region.start ^ (region.end - 1)).leading_zeros() as u8,
+ AS_LOCK_REGION_MIN_SIZE.trailing_zeros() as u8,
+ ) - 1;
+
+ // Mask off the low bits of region.start, which would be ignored by the
+ // hardware anyways.
+ let region_start =
+ region.start & genmask_checked_u64(u32::from(region_width)..=63).ok_or(EINVAL)?;
+
+ let region = (u64::from(region_width)) | region_start;
+
+ let region_lo = (region & 0xffffffff) as u32;
+ let region_hi = (region >> 32) as u32;
+
+ // Lock the region that needs to be updated.
+ let dev = self.dev();
+ as_lockaddr_lo(as_nr)?.write(dev, &self.iomem, region_lo)?;
+ as_lockaddr_hi(as_nr)?.write(dev, &self.iomem, region_hi)?;
+
+ self.as_send_cmd(as_nr, AS_COMMAND_LOCK)
+ }
+
+ fn as_end_update(&mut self, as_nr: usize) -> Result {
+ self.validate_as_slot(as_nr)?;
+ self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_PT)
+ }
+
+ fn as_flush(&mut self, as_nr: usize) -> Result {
+ self.validate_as_slot(as_nr)?;
+ self.as_send_cmd(as_nr, AS_COMMAND_FLUSH_PT)
+ }
+}
+
+impl AsSlotManager {
+ /// Locks a region for page table updates if the VM has an active slot.
+ pub(super) fn start_vm_update(&mut self, vm: &VmAsData, region: &Range<u64>) -> Result {
+ let seat = vm.as_seat.access(self);
+ match seat.slot() {
+ Some(slot) => {
+ let as_nr = slot as usize;
+ self.as_start_update(as_nr, region)
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Flushes page table updates for a VM if it has an active slot.
+ pub(super) fn end_vm_update(&mut self, vm: &VmAsData) -> Result {
+ let seat = vm.as_seat.access(self);
+ match seat.slot() {
+ Some(slot) => {
+ let as_nr = slot as usize;
+ self.as_end_update(as_nr)
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Flushes page tables for a VM if it has an active slot.
+ pub(super) fn flush_vm(&mut self, vm: &VmAsData) -> Result {
+ let seat = vm.as_seat.access(self);
+ match seat.slot() {
+ Some(slot) => {
+ let as_nr = slot as usize;
+ self.as_flush(as_nr)
+ }
+ _ => Ok(()),
+ }
+ }
+
+ /// Flushes page tables for a VM if it has an active slot.
+ pub(super) fn activate_vm(&mut self, vm: ArcBorrow<'_, VmAsData>) -> Result {
+ self.activate(&vm.as_seat, vm.into())
+ }
+
+ /// Flushes page tables for a VM if it has an active slot.
+ pub(super) fn deactivate_vm(&mut self, vm: &VmAsData) -> Result {
+ self.evict(&vm.as_seat)
+ }
+}
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index f54b997355e0..ae435c7e80b1 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -11,6 +11,7 @@
mod file;
mod gem;
mod gpu;
+mod mmu;
mod regs;
mod slot;
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 09/12] drm/tyr: add GPU virtual memory module
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (7 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 10:54 ` Boris Brezillon
2026-02-12 1:37 ` [PATCH 10/12] drm/tyr: add a kernel buffer object Deborah Brouwer
` (2 subsequent siblings)
11 siblings, 1 reply; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Boris Brezillon <boris.brezillon@collabora.com>
Add GPU virtual address space management using the DRM GPUVM framework.
Each virtual memory (VM) space is backed by ARM64 LPAE Stage 1 page tables
and can be mapped into hardware address space (AS) slots for GPU execution.
The implementation provides memory isolation and virtual address
allocation. VMs support mapping GEM buffer objects with configurable
protection flags (readonly, noexec, uncached) and handle both 4KB and 2MB
page sizes.
The vm module integrates with the MMU for address space activation and
provides map/unmap/remap operations with page table synchronization.
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Daniel Almeida <daniel.almeida@collabora.com>
Signed-off-by: Daniel Almeida <daniel.almeida@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/gem.rs | 1 -
drivers/gpu/drm/tyr/gpu.rs | 1 -
drivers/gpu/drm/tyr/tyr.rs | 1 +
drivers/gpu/drm/tyr/vm.rs | 783 +++++++++++++++++++++++++++++++++++++
4 files changed, 784 insertions(+), 2 deletions(-)
create mode 100644 drivers/gpu/drm/tyr/vm.rs
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index 6a58f2da88d3..111acf33993f 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -48,7 +48,6 @@ fn new<Ctx: DeviceContext>(
pub(crate) type Bo = gem::shmem::Object<BoData>;
/// Creates a dummy GEM object to serve as the root of a GPUVM.
-#[expect(dead_code)]
pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
let bo = gem::shmem::Object::<BoData>::new(
ddev,
diff --git a/drivers/gpu/drm/tyr/gpu.rs b/drivers/gpu/drm/tyr/gpu.rs
index b5f11bc96fa0..f5e7086ff73c 100644
--- a/drivers/gpu/drm/tyr/gpu.rs
+++ b/drivers/gpu/drm/tyr/gpu.rs
@@ -135,7 +135,6 @@ pub(crate) fn log(&self, pdev: &platform::Device) {
}
/// Returns the number of virtual address bits supported by the GPU.
- #[expect(dead_code)]
pub(crate) fn va_bits(&self) -> u32 {
self.mmu_features & genmask_u32(0..=7)
}
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index ae435c7e80b1..8e73db3a080a 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -14,6 +14,7 @@
mod mmu;
mod regs;
mod slot;
+mod vm;
kernel::module_platform_driver! {
type: TyrPlatformDeviceData,
diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
new file mode 100644
index 000000000000..806bc4e587d6
--- /dev/null
+++ b/drivers/gpu/drm/tyr/vm.rs
@@ -0,0 +1,783 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! GPU virtual memory management using the DRM GPUVM framework.
+//!
+//! This module manages GPU virtual address spaces, providing memory isolation and
+//! the illusion of owning the entire VA range, similar to CPU virtual memory. Each
+//! VM is backed by ARM64 LPAE Stage 1 page tables and can be mapped into hardware
+//! address space (AS) slots for GPU execution.
+#![allow(dead_code)]
+
+use core::ops::Range;
+
+use kernel::{
+ alloc::KBox,
+ c_str,
+ drm::{
+ gpuvm::{
+ DriverGpuVm,
+ GpuVaAlloc,
+ GpuVm,
+ GpuVmBoRegistered,
+ GpuVmCore,
+ OpMap,
+ OpMapRequest,
+ OpMapped,
+ OpRemap,
+ OpRemapped,
+ OpUnmap,
+ OpUnmapped, //
+ },
+ DeviceContext, //
+ },
+ impl_flags,
+ iommu::pgtable::{
+ prot,
+ Config,
+ IoPageTable,
+ ARM64LPAES1, //
+ },
+ new_mutex,
+ platform,
+ prelude::*,
+ sizes::{
+ SZ_1G,
+ SZ_2M,
+ SZ_4K, //
+ },
+ sync::{
+ aref::ARef,
+ Arc,
+ ArcBorrow,
+ Mutex, //
+ },
+ uapi, //
+};
+
+use crate::{
+ driver::{
+ TyrDrmDevice,
+ TyrDrmDriver, //
+ },
+ gem,
+ gem::Bo,
+ gpu::GpuInfo,
+ mmu::{
+ address_space::*,
+ Mmu, //
+ },
+ regs::*, //
+};
+
+impl_flags!(
+ #[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
+ pub(crate) struct VmMapFlags(u32);
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ pub(crate) enum VmFlag {
+ Readonly = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY as u32,
+ Noexec = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC as u32,
+ Uncached = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED as u32,
+ }
+);
+
+impl VmMapFlags {
+ /// Convert the flags to `pgtable::prot`.
+ fn to_prot(self) -> u32 {
+ let mut prot = 0;
+
+ if self.contains(VmFlag::Readonly) {
+ prot |= prot::READ;
+ } else {
+ prot |= prot::READ | prot::WRITE;
+ }
+
+ if self.contains(VmFlag::Noexec) {
+ prot |= prot::NOEXEC;
+ }
+
+ if !self.contains(VmFlag::Uncached) {
+ prot |= prot::CACHE;
+ }
+
+ prot
+ }
+}
+
+impl core::fmt::Display for VmMapFlags {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ let mut first = true;
+
+ if self.contains(VmFlag::Readonly) {
+ write!(f, "READONLY")?;
+ first = false;
+ }
+ if self.contains(VmFlag::Noexec) {
+ if !first {
+ write!(f, " | ")?;
+ }
+ write!(f, "NOEXEC")?;
+ first = false;
+ }
+
+ if self.contains(VmFlag::Uncached) {
+ if !first {
+ write!(f, " | ")?;
+ }
+ write!(f, "UNCACHED")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl TryFrom<u32> for VmMapFlags {
+ type Error = Error;
+
+ fn try_from(value: u32) -> core::result::Result<Self, Self::Error> {
+ let valid = (kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY
+ | kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC
+ | kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED)
+ as u32;
+
+ if value & !valid != 0 {
+ pr_err!("Invalid VM map flags: {:#x}\n", value);
+ return Err(EINVAL);
+ }
+ Ok(Self(value))
+ }
+}
+
+struct VmMapArgs {
+ flags: VmMapFlags,
+ vm_bo: GpuVmBoRegistered<GpuVmData>,
+ bo_offset: u64,
+}
+
+enum VmOpType {
+ Map(VmMapArgs),
+ Unmap,
+}
+
+struct VmOpResources {
+ /// This handles the remap case.
+ ///
+ /// Partial unmap requests or map requests overlapping existing mappings
+ /// will trigger a remap call, which needs to register up to three VA
+ /// objects (one for the new mapping, and two for the previous and next
+ /// mappings).
+ preallocated_gpuvas: [Option<GpuVaAlloc<GpuVmData>>; 3],
+}
+
+struct VmOpRequest {
+ /// Request type.
+ op_type: VmOpType,
+
+ /// Region of the virtual address space covered by this request.
+ region: Range<u64>,
+}
+
+struct PtMapArgs {
+ /// Flags describing authorized accesses for this mapping.
+ ///
+ /// This is directly derived from the VmMapFlags.
+ prot: u32,
+}
+
+enum PtOpType {
+ Map(PtMapArgs),
+ Unmap,
+}
+
+pub(crate) struct PtUpdateContext<'ctx> {
+ /// Page table.
+ pt: &'ctx IoPageTable<ARM64LPAES1>,
+
+ /// MMU
+ mmu: &'ctx Mmu,
+
+ /// Reference to the AS data to pass to the MMU functions
+ as_data: &'ctx VmAsData,
+
+ /// Region of the virtual address space covered by this request.
+ region: Range<u64>,
+
+ /// Operation type.
+ op_type: PtOpType,
+
+ /// Pre-allocated resources that can be used when executing the request.
+ resources: &'ctx mut VmOpResources,
+}
+
+impl<'ctx> PtUpdateContext<'ctx> {
+ fn new(
+ pt: &'ctx IoPageTable<ARM64LPAES1>,
+ mmu: &'ctx Mmu,
+ as_data: &'ctx VmAsData,
+ region: Range<u64>,
+ op_type: PtOpType,
+ resources: &'ctx mut VmOpResources,
+ ) -> Result<PtUpdateContext<'ctx>> {
+ mmu.start_vm_update(as_data, ®ion)?;
+
+ Ok(Self {
+ pt,
+ mmu,
+ as_data,
+ region,
+ op_type,
+ resources,
+ })
+ }
+
+ /// Finds one of our pre-allocated VAs.
+ ///
+ /// It is a logic error to call this more than three times for a given
+ /// PtUpdateContext.
+ fn preallocated_gpuva(&mut self) -> Result<GpuVaAlloc<GpuVmData>> {
+ self.resources
+ .preallocated_gpuvas
+ .iter_mut()
+ .find_map(|f| f.take())
+ .ok_or(EINVAL)
+ }
+}
+
+impl Drop for PtUpdateContext<'_> {
+ fn drop(&mut self) {
+ if let Err(e) = self.mmu.end_vm_update(self.as_data) {
+ pr_err!("Failed to end VM update {:?}\n", e);
+ }
+
+ if let Err(e) = self.mmu.flush_vm(self.as_data) {
+ pr_err!("Failed to flush VM {:?}\n", e);
+ }
+ }
+}
+
+pub(crate) struct GpuVmData {}
+
+/// GPU virtual address space.
+///
+/// Each VM can be mapped into a hardware address space slot.
+#[pin_data]
+pub(crate) struct Vm {
+ /// Data referenced by an AS when the VM is active
+ as_data: Arc<VmAsData>,
+ /// MMU manager.
+ mmu: Arc<Mmu>,
+ /// Platform device reference (needed to access the page table through devres).
+ pdev: ARef<platform::Device>,
+ /// DRM GPUVM core for managing virtual address space.
+ #[pin]
+ gpuvm_core: Mutex<GpuVmCore<GpuVmData>>,
+ /// Non-core part of the GPUVM. Can be used for stuff that doesn't modify the
+ /// internal mapping tree, like GpuVm::obtain()
+ gpuvm: ARef<GpuVm<GpuVmData>>,
+ /// VA range for this VM.
+ va_range: Range<u64>,
+}
+
+impl Vm {
+ pub(crate) fn new<Ctx: DeviceContext>(
+ pdev: &platform::Device,
+ ddev: &TyrDrmDevice<Ctx>,
+ mmu: ArcBorrow<'_, Mmu>,
+ gpu_info: &GpuInfo,
+ ) -> Result<Arc<Vm>> {
+ let va_bits = gpu_info.va_bits();
+ let pa_bits = gpu_info.pa_bits();
+
+ let pt_config = Config {
+ quirks: 0,
+ pgsize_bitmap: SZ_4K | SZ_2M,
+ ias: va_bits,
+ oas: pa_bits,
+ coherent_walk: false,
+ };
+
+ // SAFETY: pdev is a bound device.
+ let dev = unsafe { pdev.as_ref().as_bound() };
+ let page_table_init = IoPageTable::new(dev, pt_config);
+ let page_table = KBox::pin_init(page_table_init, GFP_KERNEL).inspect_err(|e| {
+ pr_err!("Failed to initialize page table: {:?}\n", e);
+ })?;
+ let pt = page_table.access(dev).inspect_err(|e| {
+ pr_err!("Failed to access page table: {:?}\n", e);
+ })?;
+
+ let as_config = AddressSpaceConfig {
+ transcfg: AS_TRANSCFG_PTW_MEMATTR_WB
+ | AS_TRANSCFG_PTW_RA
+ | AS_TRANSCFG_ADRMODE_AARCH64_4K
+ | as_transcfg_ina_bits(u64::from(55 - va_bits)),
+ // SAFETY: Vm::drop() evicts the address space and performs deferred
+ // cleanup before dropping the page_table Arc. This ensures that
+ // the device stops using the page table before it is dropped
+ transtab: unsafe { pt.ttbr() },
+ memattr: mair_to_memattr(pt.mair()),
+ };
+
+ let range = 0..(1u64 << va_bits);
+ let reserve_range = 0..0u64;
+
+ let dummy_obj = gem::new_dummy_object(ddev).inspect_err(|e| {
+ pr_err!("Failed to create dummy GEM object: {:?}\n", e);
+ })?;
+
+ let gpuvm_core = kernel::drm::gpuvm::GpuVm::new::<Error, _>(
+ c_str!("Tyr::GpuVm"),
+ ddev,
+ &*dummy_obj,
+ range.clone(),
+ reserve_range,
+ GpuVmData {},
+ )
+ .inspect_err(|e| {
+ pr_err!("Failed to create GpuVm: {:?}\n", e);
+ })?;
+ let gpuvm = ARef::from(&*gpuvm_core);
+
+ let as_data = Arc::new(VmAsData::new(&mmu, as_config, page_table), GFP_KERNEL)?;
+
+ let vm = Arc::pin_init(
+ pin_init!(Self{
+ as_data: as_data,
+ pdev: pdev.into(),
+ mmu: mmu.into(),
+ gpuvm: gpuvm,
+ gpuvm_core <- new_mutex!(gpuvm_core),
+ va_range: range,
+ }),
+ GFP_KERNEL,
+ )?;
+
+ Ok(vm)
+ }
+
+ /// Activate the VM in a hardware address space slot.
+ pub(crate) fn activate(&self) -> Result {
+ self.mmu
+ .activate_vm(self.as_data.as_arc_borrow())
+ .inspect_err(|e| {
+ pr_err!("Failed to activate VM: {:?}\n", e);
+ })
+ }
+
+ /// Deactivate the VM by evicting it from its address space slot.
+ fn deactivate(&self) -> Result {
+ self.mmu.deactivate_vm(&self.as_data).inspect_err(|e| {
+ pr_err!("Failed to deactivate VM: {:?}\n", e);
+ })
+ }
+
+ pub(crate) fn kill(&self) {
+ // TODO: Turn the VM into a state where it can't be used.
+ let _ = self.deactivate().inspect_err(|e| {
+ pr_err!("Failed to deactivate VM: {:?}\n", e);
+ });
+ let _ = self
+ .unmap_range(self.va_range.start, self.va_range.end - self.va_range.start)
+ .inspect_err(|e| {
+ pr_err!("Failed to unmap range during deactivate: {:?}\n", e);
+ });
+ }
+
+ fn exec_op(
+ &self,
+ gpuvm_core: &mut GpuVmCore<GpuVmData>,
+ req: VmOpRequest,
+ resources: &mut VmOpResources,
+ ) -> Result {
+ let pt = self
+ .as_data
+ .page_table
+ // SAFETY: pdev is a bound device.
+ .access(unsafe { self.pdev.as_ref().as_bound() })
+ .inspect_err(|e| {
+ pr_err!("Failed to access page table while mapping pages: {:?}\n", e);
+ })?;
+
+ match req.op_type {
+ VmOpType::Map(args) => {
+ let mut pt_upd = PtUpdateContext::new(
+ pt,
+ &self.mmu,
+ &self.as_data,
+ req.region,
+ PtOpType::Map(PtMapArgs {
+ prot: args.flags.to_prot(),
+ }),
+ resources,
+ )?;
+
+ gpuvm_core.sm_map(OpMapRequest {
+ addr: pt_upd.region.start,
+ range: pt_upd.region.end - pt_upd.region.start,
+ gem_offset: args.bo_offset,
+ vm_bo: args.vm_bo,
+ context: &mut pt_upd,
+ })
+ //PtUpdateContext drops here flushing the page table
+ }
+ VmOpType::Unmap => {
+ let mut pt_upd = PtUpdateContext::new(
+ pt,
+ &self.mmu,
+ &self.as_data,
+ req.region,
+ PtOpType::Unmap,
+ resources,
+ )?;
+
+ gpuvm_core.sm_unmap(
+ pt_upd.region.start,
+ pt_upd.region.end - pt_upd.region.start,
+ &mut pt_upd,
+ )
+ //PtUpdateContext drops here flushing the page table
+ }
+ }
+ }
+
+ /// Map a GEM object range into the VM.
+ pub(crate) fn map_bo_range(
+ &self,
+ bo: &Bo,
+ bo_offset: u64,
+ size: u64,
+ va: u64,
+ flags: VmMapFlags,
+ ) -> Result {
+ let req = VmOpRequest {
+ op_type: VmOpType::Map(VmMapArgs {
+ vm_bo: self.gpuvm.obtain(bo, ())?,
+ flags,
+ bo_offset,
+ }),
+ region: va..(va + size),
+ };
+ let mut resources = VmOpResources {
+ preallocated_gpuvas: [
+ Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
+ ],
+ };
+ let mut gpuvm_core = self.gpuvm_core.lock();
+
+ self.exec_op(gpuvm_core.as_mut().get_mut(), req, &mut resources)?;
+
+ // We flush the defer cleanup list now. Things will be different in
+ // the asynchronous VM_BIND path, where we want the cleanup to
+ // happen outside the DMA signalling path.
+ self.gpuvm.deferred_cleanup();
+ Ok(())
+ }
+
+ pub(crate) fn unmap_range(&self, va: u64, size: u64) -> Result {
+ let req = VmOpRequest {
+ op_type: VmOpType::Unmap,
+ region: va..(va + size),
+ };
+ let mut resources = VmOpResources {
+ preallocated_gpuvas: [
+ Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
+ Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
+ None,
+ ],
+ };
+ let mut gpuvm_core = self.gpuvm_core.lock();
+
+ self.exec_op(gpuvm_core.as_mut().get_mut(), req, &mut resources)?;
+
+ // We flush the defer cleanup list now. Things will be different in
+ // the asynchronous VM_BIND path, where we want the cleanup to
+ // happen outside the DMA signalling path.
+ self.gpuvm.deferred_cleanup();
+ Ok(())
+ }
+}
+
+impl DriverGpuVm for GpuVmData {
+ type Driver = TyrDrmDriver;
+ type Object = Bo;
+ type VmBoData = ();
+ type VaData = ();
+ type SmContext<'ctx> = PtUpdateContext<'ctx>;
+
+ fn sm_step_map<'op>(
+ &mut self,
+ op: OpMap<'op, Self>,
+ context: &mut Self::SmContext<'_>,
+ ) -> Result<OpMapped<'op, Self>, Error> {
+ let start_iova = op.addr();
+ let mut iova = start_iova;
+ let mut bytes_left_to_map = op.length();
+ let mut gem_offset = op.gem_offset();
+ let sgt = op.obj().sg_table().inspect_err(|e| {
+ pr_err!("Failed to get sg_table: {:?}\n", e);
+ })?;
+ let prot = match &context.op_type {
+ PtOpType::Map(args) => args.prot,
+ _ => {
+ return Err(EINVAL);
+ }
+ };
+
+ for sgt_entry in sgt.iter() {
+ let mut paddr = sgt_entry.dma_address();
+ let mut sgt_entry_length: u64 = sgt_entry.dma_len();
+
+ if bytes_left_to_map == 0 {
+ break;
+ }
+
+ if gem_offset > 0 {
+ // Skip the entire SGT entry if the gem_offset exceeds its length
+ let skip = sgt_entry_length.min(gem_offset);
+ paddr += skip;
+ sgt_entry_length -= skip;
+ gem_offset -= skip;
+ }
+
+ if sgt_entry_length == 0 {
+ continue;
+ }
+
+ if gem_offset != 0 {
+ pr_err!("Invalid gem_offset {} in page table mapping.\n", gem_offset);
+ return Err(EINVAL);
+ }
+ let len = sgt_entry_length.min(bytes_left_to_map);
+
+ let segment_mapped = match pt_map(context.pt, iova, paddr, len, prot) {
+ Ok(segment_mapped) => segment_mapped,
+ Err(e) => {
+ // clean up any successful mappings from previous SGT entries.
+ let total_mapped = iova - start_iova;
+ if total_mapped > 0 {
+ pt_unmap(context.pt, start_iova..(start_iova + total_mapped)).ok();
+ }
+ return Err(e);
+ }
+ };
+
+ // Since there could be a partial mapping, only advance by the actual amount mapped
+ bytes_left_to_map -= segment_mapped;
+ iova += segment_mapped;
+ }
+
+ let gpuva = context.preallocated_gpuva()?;
+ let op = op.insert(gpuva, pin_init::init_zeroed());
+
+ Ok(op)
+ }
+
+ fn sm_step_unmap<'op>(
+ &mut self,
+ op: OpUnmap<'op, Self>,
+ context: &mut Self::SmContext<'_>,
+ ) -> Result<OpUnmapped<'op, Self>, Error> {
+ let start_iova = op.va().addr();
+ let length = op.va().length();
+
+ let region = start_iova..(start_iova + length);
+ pt_unmap(context.pt, region.clone()).inspect_err(|e| {
+ pr_err!(
+ "Failed to unmap region {:#x}..{:#x}: {:?}\n",
+ region.start,
+ region.end,
+ e
+ );
+ })?;
+
+ let (op_unmapped, _va_removed) = op.remove();
+
+ Ok(op_unmapped)
+ }
+
+ fn sm_step_remap<'op>(
+ &mut self,
+ op: OpRemap<'op, Self>,
+ context: &mut Self::SmContext<'_>,
+ ) -> Result<OpRemapped<'op, Self>, Error> {
+ let unmap_start = if let Some(prev) = op.prev() {
+ prev.addr() + prev.length()
+ } else {
+ op.va_to_unmap().addr()
+ };
+
+ let unmap_end = if let Some(next) = op.next() {
+ next.addr()
+ } else {
+ op.va_to_unmap().addr() + op.va_to_unmap().length()
+ };
+
+ let unmap_length = unmap_end - unmap_start;
+
+ if unmap_length > 0 {
+ let region = unmap_start..(unmap_start + unmap_length);
+ pt_unmap(context.pt, region.clone()).inspect_err(|e| {
+ pr_err!(
+ "Failed to unmap remap region {:#x}..{:#x}: {:?}\n",
+ region.start,
+ region.end,
+ e
+ );
+ })?;
+ }
+
+ let prev_va = context.preallocated_gpuva()?;
+ let next_va = context.preallocated_gpuva()?;
+
+ let (op_remapped, _remap_ret) = op.remap(
+ [prev_va, next_va],
+ pin_init::init_zeroed(),
+ pin_init::init_zeroed(),
+ );
+
+ Ok(op_remapped)
+ }
+}
+
+fn mair_to_memattr(mair: u64) -> u64 {
+ let mut memattr: u64 = 0;
+
+ for i in 0..8 {
+ let in_attr = (mair >> (8 * i)) as u8;
+ let outer = in_attr >> 4;
+ let inner = in_attr & 0xf;
+
+ // For caching to be enabled, inner and outer caching policy
+ // have to be both write-back, if one of them is write-through
+ // or non-cacheable, we just choose non-cacheable. Device
+ // memory is also translated to non-cacheable.
+ let out_attr = if (outer & 3 == 0) || (outer & 4 == 0) || (inner & 4 == 0) {
+ AS_MEMATTR_AARCH64_INNER_OUTER_NC
+ | AS_MEMATTR_AARCH64_SH_MIDGARD_INNER
+ | as_memattr_aarch64_inner_alloc_expl(false, false)
+ } else {
+ // Use SH_CPU_INNER mode so SH_IS, which is used when
+ // IOMMU_CACHE is set, actually maps to the standard
+ // definition of inner-shareable and not Mali's
+ // internal-shareable mode.
+ //
+ // TODO: this assumes a non-coherent system.
+ AS_MEMATTR_AARCH64_INNER_OUTER_WB
+ | AS_MEMATTR_AARCH64_SH_MIDGARD_INNER
+ | as_memattr_aarch64_inner_alloc_expl(inner & 1 != 0, inner & 2 != 0)
+ };
+
+ memattr |= (u64::from(out_attr)) << (8 * i);
+ }
+
+ memattr
+}
+
+// We can map multiple pages at once but we can't exceed the size of the
+// table entry itself. So, if mapping 4KB pages, figure out how many pages
+// can be mapped before we hit the 2MB boundary. Or, if mapping 2MB pages,
+// figure out how many pages can be mapped before hitting the 1GB boundary
+// Returns the page size (4KB or 2MB) and the number of pages that can be mapped at that size.
+fn get_pgsize(addr: u64, size: u64) -> (u64, u64) {
+ // Get the distance to the next boundary of 2MB block
+ let blk_offset_2m = addr.wrapping_neg() % (SZ_2M as u64);
+
+ // Use 4K blocks if the address is not 2MB aligned, or we have less than 2MB to map
+ if blk_offset_2m != 0 || size < SZ_2M as u64 {
+ let pgcount = if blk_offset_2m == 0 {
+ size / SZ_4K as u64
+ } else {
+ blk_offset_2m.min(size) / SZ_4K as u64
+ };
+ return (SZ_4K as u64, pgcount);
+ }
+
+ let blk_offset_1g = addr.wrapping_neg() % (SZ_1G as u64);
+ let blk_offset = if blk_offset_1g == 0 {
+ SZ_1G as u64
+ } else {
+ blk_offset_1g
+ };
+ let pgcount = blk_offset.min(size) / SZ_2M as u64;
+
+ (SZ_2M as u64, pgcount)
+}
+
+fn pt_map(
+ pt: &IoPageTable<ARM64LPAES1>,
+ iova: u64,
+ paddr: u64,
+ len: u64,
+ prot: u32,
+) -> Result<u64> {
+ let mut segment_mapped = 0u64;
+ while segment_mapped < len {
+ let remaining = len - segment_mapped;
+ let curr_iova = iova + segment_mapped;
+ let curr_paddr = paddr + segment_mapped;
+
+ let (pgsize, pgcount) = get_pgsize(curr_iova | curr_paddr, remaining);
+
+ // SAFETY: Exclusive access to the page table is ensured because
+ // the pt reference comes from PtUpdateContext, which was
+ // created while holding &mut Vm, preventing any other access to the
+ // page table for the duration of this operation.
+ let (mapped, result) = unsafe {
+ pt.map_pages(
+ curr_iova as usize,
+ (curr_paddr as usize).try_into().unwrap(),
+ pgsize as usize,
+ pgcount as usize,
+ prot,
+ GFP_KERNEL,
+ )
+ };
+
+ if let Err(e) = result {
+ pr_err!("pt.map_pages failed at iova {:#x}: {:?}\n", curr_iova, e);
+ if segment_mapped > 0 {
+ pt_unmap(pt, iova..(iova + segment_mapped)).ok();
+ }
+ return Err(e);
+ }
+
+ if mapped == 0 {
+ pr_err!("Failed to map any pages at iova {:#x}\n", curr_iova);
+ if segment_mapped > 0 {
+ pt_unmap(pt, iova..(iova + segment_mapped)).ok();
+ }
+ return Err(ENOMEM);
+ }
+
+ segment_mapped += mapped as u64;
+ }
+
+ Ok(segment_mapped)
+}
+
+fn pt_unmap(pt: &IoPageTable<ARM64LPAES1>, range: Range<u64>) -> Result {
+ let mut iova = range.start;
+ let mut bytes_left_to_unmap = range.end - range.start;
+
+ while bytes_left_to_unmap > 0 {
+ let (pgsize, pgcount) = get_pgsize(iova, bytes_left_to_unmap);
+
+ // SAFETY: Exclusive access to the page table is ensured because
+ // the pt reference comes from PtUpdateContext, which was
+ // created while holding &mut Vm, preventing any other access to the
+ // page table for the duration of this operation.
+ let unmapped = unsafe { pt.unmap_pages(iova as usize, pgsize as usize, pgcount as usize) };
+
+ if unmapped == 0 {
+ pr_err!("Failed to unmap any bytes at iova {:#x}\n", iova);
+ return Err(EINVAL);
+ }
+
+ bytes_left_to_unmap -= unmapped as u64;
+ iova += unmapped as u64;
+ }
+
+ Ok(())
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 10/12] drm/tyr: add a kernel buffer object
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (8 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 09/12] drm/tyr: add GPU virtual memory module Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 11:00 ` Boris Brezillon
2026-02-12 1:37 ` [PATCH 11/12] drm/tyr: add parser for firmware binary Deborah Brouwer
2026-02-12 1:37 ` [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support Deborah Brouwer
11 siblings, 1 reply; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
Introduce a buffer object type (KernelBo) for internal driver allocations
that are managed by the kernel rather than userspace.
KernelBo wraps a GEM shmem object and automatically handles GPU virtual
address space mapping during creation and unmapping on drop. This provides
a safe and convenient way for the driver to both allocate and clean up
internal buffers for kernel-managed resources.
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/gem.rs | 74 +++++++++++++++++++++++++++++++++++---
1 file changed, 70 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index 111acf33993f..3807810be7ea 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -4,6 +4,8 @@
//! This module provides buffer object (BO) management functionality using
//! DRM's GEM subsystem with shmem backing.
+use core::ops::Range;
+
use kernel::{
drm::{
gem,
@@ -11,12 +13,22 @@
DeviceContext, //
},
prelude::*,
- sync::aref::ARef, //
+ sync::{
+ aref::ARef,
+ Arc,
+ ArcBorrow, //
+ },
};
-use crate::driver::{
- TyrDrmDevice,
- TyrDrmDriver, //
+use crate::{
+ driver::{
+ TyrDrmDevice,
+ TyrDrmDriver, //
+ },
+ vm::{
+ Vm,
+ VmMapFlags, //
+ },
};
/// Tyr's DriverObject type for GEM objects.
@@ -61,3 +73,57 @@ pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) ->
Ok(bo)
}
+
+/// A buffer object that is owned and managed by Tyr rather than userspace.
+pub(crate) struct KernelBo {
+ #[expect(dead_code)]
+ pub(crate) bo: ARef<Bo>,
+ vm: Arc<Vm>,
+ va_range: Range<u64>,
+}
+
+impl KernelBo {
+ /// Creates a new kernel-owned buffer object and maps it into GPU VA space.
+ #[expect(dead_code)]
+ pub(crate) fn new<Ctx: DeviceContext>(
+ ddev: &TyrDrmDevice<Ctx>,
+ vm: ArcBorrow<'_, Vm>,
+ size: u64,
+ va: u64,
+ flags: VmMapFlags,
+ ) -> Result<Self> {
+ let bo = gem::shmem::Object::<BoData>::new(
+ ddev,
+ size as usize,
+ shmem::ObjectConfig {
+ map_wc: true,
+ parent_resv_obj: None,
+ },
+ BoCreateArgs { flags: 0 },
+ )?;
+
+ vm.map_bo_range(&bo, 0, size, va, flags)?;
+
+ Ok(KernelBo {
+ bo,
+ vm: vm.into(),
+ va_range: va..(va + size),
+ })
+ }
+}
+
+impl Drop for KernelBo {
+ fn drop(&mut self) {
+ let va = self.va_range.start;
+ let size = self.va_range.end - self.va_range.start;
+
+ if let Err(e) = self.vm.unmap_range(va, size) {
+ pr_err!(
+ "Failed to unmap KernelBo range {:#x}..{:#x}: {:?}\n",
+ self.va_range.start,
+ self.va_range.end,
+ e
+ );
+ }
+ }
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 11/12] drm/tyr: add parser for firmware binary
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (9 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 10/12] drm/tyr: add a kernel buffer object Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support Deborah Brouwer
11 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
From: Daniel Almeida <daniel.almeida@collabora.com>
Add a parser for the Mali CSF GPU firmware binary format. The firmware
consists of a header followed by entries describing how to load firmware
sections into the MCU's memory.
The parser extracts section metadata including virtual address ranges,
data byte offsets within the binary, and section flags controlling
permissions and cache modes. It validates the basic firmware structure
and alignment and ignores protected-mode sections for now.
Signed-off-by: Daniel Almeida <daniel.almeida@collabora.com>
Co-developed-by: Beata Michalska <beata.michalska@arm.com>
Signed-off-by: Beata Michalska <beata.michalska@arm.com>
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/fw/parser.rs | 469 +++++++++++++++++++++++++++++++
1 file changed, 469 insertions(+)
create mode 100644 drivers/gpu/drm/tyr/fw/parser.rs
diff --git a/drivers/gpu/drm/tyr/fw/parser.rs b/drivers/gpu/drm/tyr/fw/parser.rs
new file mode 100644
index 000000000000..5a4ce3212983
--- /dev/null
+++ b/drivers/gpu/drm/tyr/fw/parser.rs
@@ -0,0 +1,469 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Firmware binary parser for Mali CSF GPU.
+//!
+//! This module implements a parser for the Mali GPU firmware binary format. The firmware
+//! file contains a header followed by a sequence of entries, each describing how to load
+//! firmware sections into the MCU's memory. The parser extracts section metadata including:
+//! - Virtual address ranges where sections should be mapped
+//! - Data ranges (byte offsets) within the firmware binary
+//! - Section flags (permissions, cache modes)
+
+use core::{
+ mem::size_of,
+ ops::Range, //
+};
+
+use kernel::{
+ bits::bit_u32,
+ prelude::*,
+ str::CString, //
+};
+
+use crate::{
+ fw::{
+ SectionFlag,
+ SectionFlags,
+ CSF_MCU_SHARED_REGION_START, //
+ },
+ vm::{
+ VmFlag,
+ VmMapFlags, //
+ }, //
+};
+
+pub(super) struct ParsedSection {
+ pub(super) data_range: Range<u32>,
+ pub(super) va: Range<u32>,
+ pub(super) vm_map_flags: VmMapFlags,
+}
+
+/// A bare-bones `std::io::Cursor<[u8]>` clone to keep track of the current
+/// position in the firmware binary.
+struct Cursor<'a> {
+ data: &'a [u8],
+ pos: usize,
+}
+
+impl<'a> Cursor<'a> {
+ fn new(data: &'a [u8]) -> Self {
+ Self { data, pos: 0 }
+ }
+
+ fn len(&self) -> usize {
+ self.data.len()
+ }
+
+ fn pos(&self) -> usize {
+ self.pos
+ }
+
+ /// Returns a view into the cursor's data.
+ ///
+ /// This spawns a new cursor, leaving the current cursor unchanged.
+ fn view(&self, range: Range<usize>) -> Result<Cursor<'_>> {
+ if range.start < self.pos || range.end > self.data.len() {
+ pr_err!(
+ "Invalid cursor range {:?} for data of length {}",
+ range,
+ self.data.len()
+ );
+
+ Err(EINVAL)
+ } else {
+ Ok(Self {
+ data: &self.data[range],
+ pos: 0,
+ })
+ }
+ }
+
+ fn read(&mut self, nbytes: usize) -> Result<&[u8]> {
+ let start = self.pos;
+ let end = start + nbytes;
+
+ if end > self.data.len() {
+ pr_err!(
+ "Invalid firmware file: read of size {} at position {} is out of bounds",
+ nbytes,
+ start,
+ );
+ return Err(EINVAL);
+ }
+
+ self.pos += nbytes;
+ Ok(&self.data[start..end])
+ }
+
+ fn read_u8(&mut self) -> Result<u8> {
+ let bytes = self.read(size_of::<u8>())?;
+ Ok(bytes[0])
+ }
+
+ fn read_u16(&mut self) -> Result<u16> {
+ let bytes = self.read(size_of::<u16>())?;
+ Ok(u16::from_le_bytes(bytes.try_into().unwrap()))
+ }
+
+ fn read_u32(&mut self) -> Result<u32> {
+ let bytes = self.read(size_of::<u32>())?;
+ Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
+ }
+
+ fn advance(&mut self, nbytes: usize) -> Result {
+ if self.pos + nbytes > self.data.len() {
+ pr_err!(
+ "Invalid firmware file: advance of size {} at position {} is out of bounds",
+ nbytes,
+ self.pos,
+ );
+ return Err(EINVAL);
+ }
+ self.pos += nbytes;
+ Ok(())
+ }
+}
+
+pub(super) struct FwParser<'a> {
+ cursor: Cursor<'a>,
+}
+
+impl<'a> FwParser<'a> {
+ pub(super) fn new(data: &'a [u8]) -> Self {
+ Self {
+ cursor: Cursor::new(data),
+ }
+ }
+
+ pub(super) fn parse(&mut self) -> Result<KVec<ParsedSection>> {
+ let fw_header = self.parse_fw_header()?;
+
+ let mut parsed_sections = KVec::new();
+ while (self.cursor.pos() as u32) < fw_header.size {
+ let entry_section = self.parse_entry()?;
+
+ if let Some(inner) = entry_section.inner {
+ parsed_sections.push(inner, GFP_KERNEL)?;
+ }
+ }
+
+ Ok(parsed_sections)
+ }
+
+ fn parse_fw_header(&mut self) -> Result<FirmwareHeader> {
+ let fw_header: FirmwareHeader = match FirmwareHeader::new(&mut self.cursor) {
+ Ok(fw_header) => fw_header,
+ Err(e) => {
+ pr_err!("Invalid firmware file: {}", e.to_errno());
+ return Err(e);
+ }
+ };
+
+ if fw_header.size > self.cursor.len() as u32 {
+ pr_err!("Firmware image is truncated");
+ return Err(EINVAL);
+ }
+ Ok(fw_header)
+ }
+
+ fn parse_entry(&mut self) -> Result<EntrySection> {
+ let entry_section = EntrySection {
+ entry_hdr: EntryHeader(self.cursor.read_u32()?),
+ inner: None,
+ };
+
+ if self.cursor.pos() % size_of::<u32>() != 0
+ || entry_section.entry_hdr.size() as usize % size_of::<u32>() != 0
+ {
+ pr_err!(
+ "Firmware entry isn't 32 bit aligned, offset={:#x} size={:#x}\n",
+ self.cursor.pos() - size_of::<u32>(),
+ entry_section.entry_hdr.size()
+ );
+ return Err(EINVAL);
+ }
+
+ let section_hdr_size = entry_section.entry_hdr.size() as usize - size_of::<EntryHeader>();
+
+ let entry_section = {
+ let mut entry_cursor = self
+ .cursor
+ .view(self.cursor.pos()..self.cursor.pos() + section_hdr_size)?;
+
+ match entry_section.entry_hdr.entry_type() {
+ Ok(EntryType::Iface) => Ok(EntrySection {
+ entry_hdr: entry_section.entry_hdr,
+ inner: Self::parse_section_entry(&mut entry_cursor)?,
+ }),
+ Ok(
+ EntryType::Config
+ | EntryType::FutfTest
+ | EntryType::TraceBuffer
+ | EntryType::TimelineMetadata
+ | EntryType::BuildInfoMetadata,
+ ) => Ok(entry_section),
+
+ entry_type => {
+ if entry_type.is_err() || !entry_section.entry_hdr.optional() {
+ if !entry_section.entry_hdr.optional() {
+ pr_err!(
+ "Failed to handle firmware entry type: {}\n",
+ entry_type
+ .map_or(entry_section.entry_hdr.entry_type_raw(), |e| e as u8)
+ );
+ Err(EINVAL)
+ } else {
+ Ok(entry_section)
+ }
+ } else {
+ Ok(entry_section)
+ }
+ }
+ }
+ };
+
+ if entry_section.is_ok() {
+ self.cursor.advance(section_hdr_size)?;
+ }
+
+ entry_section
+ }
+
+ fn parse_section_entry(entry_cursor: &mut Cursor<'_>) -> Result<Option<ParsedSection>> {
+ let section_hdr: SectionHeader = SectionHeader::new(entry_cursor)?;
+
+ if section_hdr.data.end < section_hdr.data.start {
+ pr_err!(
+ "Firmware corrupted, data.end < data.start (0x{:x} < 0x{:x})\n",
+ section_hdr.data.end,
+ section_hdr.data.start
+ );
+ return Err(EINVAL);
+ }
+
+ if section_hdr.va.end < section_hdr.va.start {
+ pr_err!(
+ "Firmware corrupted, section_hdr.va.end < section_hdr.va.start (0x{:x} < 0x{:x})\n",
+ section_hdr.va.end,
+ section_hdr.va.start
+ );
+ return Err(EINVAL);
+ }
+
+ if section_hdr.section_flags.contains(SectionFlag::Prot) {
+ pr_info!("Firmware protected mode entry not supported, ignoring");
+ return Ok(None);
+ }
+
+ if section_hdr.va.start == CSF_MCU_SHARED_REGION_START
+ && !section_hdr.section_flags.contains(SectionFlag::Shared)
+ {
+ pr_err!(
+ "Interface at 0x{:x} must be shared",
+ CSF_MCU_SHARED_REGION_START
+ );
+ return Err(EINVAL);
+ }
+
+ let name_len = entry_cursor.len() - entry_cursor.pos();
+ let name_bytes = entry_cursor.read(name_len)?;
+
+ let mut name = KVec::with_capacity(name_bytes.len() + 1, GFP_KERNEL)?;
+ name.extend_from_slice(name_bytes, GFP_KERNEL)?;
+ name.push(0, GFP_KERNEL)?;
+
+ let _name = CStr::from_bytes_with_nul(&name)
+ .ok()
+ .and_then(|name| CString::try_from(name).ok());
+
+ let cache_mode = section_hdr.section_flags.cache_mode();
+ let mut vm_map_flags = VmMapFlags::empty();
+
+ if !section_hdr.section_flags.contains(SectionFlag::Write) {
+ vm_map_flags |= VmFlag::Readonly;
+ }
+ if !section_hdr.section_flags.contains(SectionFlag::Exec) {
+ vm_map_flags |= VmFlag::Noexec;
+ }
+ if cache_mode != SectionFlag::CacheModeCached.into() {
+ vm_map_flags |= VmFlag::Uncached;
+ }
+
+ Ok(Some(ParsedSection {
+ data_range: section_hdr.data.clone(),
+ va: section_hdr.va,
+ vm_map_flags,
+ }))
+ }
+}
+
+#[expect(dead_code)]
+struct FirmwareHeader {
+ /// Magic value to check binary validity.
+ magic: u32,
+
+ /// Minor FW version.
+ minor: u8,
+
+ /// Major FW version.
+ major: u8,
+
+ /// Padding. Must be set to zero.
+ _padding1: u16,
+
+ /// FW Version hash.
+ version_hash: u32,
+
+ /// Padding. Must be set to zero.
+ _padding2: u32,
+
+ /// Total size of all the structured data headers at beginning of fw binary.
+ size: u32,
+}
+
+impl FirmwareHeader {
+ const FW_BINARY_MAGIC: u32 = 0xc3f13a6e;
+ const FW_BINARY_MAJOR_MAX: u8 = 0;
+
+ fn new(cursor: &mut Cursor<'_>) -> Result<Self> {
+ let magic = cursor.read_u32()?;
+ if magic != Self::FW_BINARY_MAGIC {
+ pr_err!("Invalid firmware magic");
+ return Err(EINVAL);
+ }
+
+ let minor = cursor.read_u8()?;
+ let major = cursor.read_u8()?;
+
+ if major > Self::FW_BINARY_MAJOR_MAX {
+ pr_err!(
+ "Unsupported firmware binary header version {}.{} (expected {}.x)\n",
+ major,
+ minor,
+ Self::FW_BINARY_MAJOR_MAX
+ );
+ return Err(EINVAL);
+ }
+
+ let padding1 = cursor.read_u16()?;
+ let version_hash = cursor.read_u32()?;
+ let padding2 = cursor.read_u32()?;
+ let size = cursor.read_u32()?;
+
+ if padding1 != 0 || padding2 != 0 {
+ pr_err!("Invalid firmware file: header padding is not zero");
+ return Err(EINVAL);
+ }
+
+ let fw_header = Self {
+ magic,
+ minor,
+ major,
+ _padding1: padding1,
+ version_hash,
+ _padding2: padding2,
+ size,
+ };
+
+ Ok(fw_header)
+ }
+}
+
+/// Firmware section header for loading binary sections into MCU memory.
+#[derive(Debug)]
+struct SectionHeader {
+ section_flags: SectionFlags,
+ /// MCU virtual range to map this binary section to.
+ va: Range<u32>,
+ /// References the data in the FW binary.
+ data: Range<u32>,
+}
+
+impl SectionHeader {
+ fn new(cursor: &mut Cursor<'_>) -> Result<Self> {
+ let section_flags = cursor.read_u32()?;
+ let section_flags = SectionFlags::try_from(section_flags)?;
+
+ let va_start = cursor.read_u32()?;
+ let va_end = cursor.read_u32()?;
+
+ let va = va_start..va_end;
+
+ if va.is_empty() {
+ pr_err!(
+ "Invalid firmware file: empty VA range at pos {}\n",
+ cursor.pos(),
+ );
+ return Err(EINVAL);
+ }
+
+ let data_start = cursor.read_u32()?;
+ let data_end = cursor.read_u32()?;
+ let data = data_start..data_end;
+
+ Ok(Self {
+ section_flags,
+ va,
+ data,
+ })
+ }
+}
+
+struct EntrySection {
+ entry_hdr: EntryHeader,
+ inner: Option<ParsedSection>,
+}
+
+struct EntryHeader(u32);
+
+impl EntryHeader {
+ fn entry_type_raw(&self) -> u8 {
+ (self.0 & 0xff) as u8
+ }
+
+ fn entry_type(&self) -> Result<EntryType> {
+ let v = self.entry_type_raw();
+ EntryType::try_from(v)
+ }
+
+ fn optional(&self) -> bool {
+ self.0 & bit_u32(31) != 0
+ }
+
+ fn size(&self) -> u32 {
+ self.0 >> 8 & 0xff
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+#[repr(u8)]
+enum EntryType {
+ /// Host <-> FW interface.
+ Iface = 0,
+ /// FW config.
+ Config = 1,
+ /// Unit tests.
+ FutfTest = 2,
+ /// Trace buffer interface.
+ TraceBuffer = 3,
+ /// Timeline metadata interface.
+ TimelineMetadata = 4,
+ /// Metadata about how the FW binary was built.
+ BuildInfoMetadata = 6,
+}
+
+impl TryFrom<u8> for EntryType {
+ type Error = Error;
+
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(EntryType::Iface),
+ 1 => Ok(EntryType::Config),
+ 2 => Ok(EntryType::FutfTest),
+ 3 => Ok(EntryType::TraceBuffer),
+ 4 => Ok(EntryType::TimelineMetadata),
+ 6 => Ok(EntryType::BuildInfoMetadata),
+ _ => Err(EINVAL),
+ }
+ }
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
` (10 preceding siblings ...)
2026-02-12 1:37 ` [PATCH 11/12] drm/tyr: add parser for firmware binary Deborah Brouwer
@ 2026-02-12 1:37 ` Deborah Brouwer
2026-02-21 11:25 ` Alice Ryhl
11 siblings, 1 reply; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-12 1:37 UTC (permalink / raw)
To: dri-devel, rust-for-linux
Cc: daniel.almeida, aliceryhl, boris.brezillon, beata.michalska,
lyude, Deborah Brouwer
Add firmware loading and management for the Mali CSF GPU. This introduces
the fw module that loads the Mali GPU firmware binary, parses it into
sections, and maps those sections into the MCU VM at the required
virtual addresses.
On probe, the firmware is loaded, its sections are mapped and populated,
the MCU VM is activated, and the MCU is booted.
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
---
drivers/gpu/drm/tyr/driver.rs | 16 ++-
drivers/gpu/drm/tyr/fw.rs | 263 ++++++++++++++++++++++++++++++++++
drivers/gpu/drm/tyr/gem.rs | 2 -
drivers/gpu/drm/tyr/mmu.rs | 1 -
drivers/gpu/drm/tyr/slot.rs | 1 -
drivers/gpu/drm/tyr/tyr.rs | 1 +
drivers/gpu/drm/tyr/vm.rs | 1 -
7 files changed, 279 insertions(+), 6 deletions(-)
create mode 100644 drivers/gpu/drm/tyr/fw.rs
diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
index ad5a765a6c2a..9d6ead443228 100644
--- a/drivers/gpu/drm/tyr/driver.rs
+++ b/drivers/gpu/drm/tyr/driver.rs
@@ -40,6 +40,7 @@
use crate::{
file::TyrDrmFileData,
+ fw::Firmware,
gem::BoData,
gpu,
gpu::GpuInfo,
@@ -63,6 +64,8 @@ pub(crate) struct TyrPlatformDeviceData {
pub(crate) struct TyrDrmDeviceData {
pub(crate) pdev: ARef<platform::Device>,
+ pub(crate) fw: Arc<Firmware>,
+
#[pin]
clks: Mutex<Clocks>,
@@ -149,10 +152,21 @@ fn probe(
let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
let platform: ARef<platform::Device> = pdev.into();
- let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
+ let mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
+
+ let firmware = Firmware::new(
+ pdev,
+ iomem.as_arc_borrow(),
+ &uninit_ddev,
+ mmu.as_arc_borrow(),
+ &gpu_info,
+ )?;
+
+ firmware.boot()?;
let data = try_pin_init!(TyrDrmDeviceData {
pdev: platform.clone(),
+ fw: firmware,
clks <- new_mutex!(Clocks {
core: core_clk,
stacks: stacks_clk,
diff --git a/drivers/gpu/drm/tyr/fw.rs b/drivers/gpu/drm/tyr/fw.rs
new file mode 100644
index 000000000000..eeb3258350fc
--- /dev/null
+++ b/drivers/gpu/drm/tyr/fw.rs
@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+
+//! Firmware loading and management for Mali CSF GPU.
+//!
+//! This module handles loading the Mali GPU firmware binary, parsing it into sections,
+//! and mapping those sections into the MCU's virtual address space. Each firmware section
+//! has specific properties (read/write/execute permissions, cache modes) and must be loaded
+//! at specific virtual addresses expected by the MCU.
+//!
+//! See [`Firmware`] for the main firmware management interface and [`Section`] for
+//! individual firmware sections.
+//!
+//! [`Firmware`]: crate::fw::Firmware
+//! [`Section`]: crate::fw::Section
+
+use kernel::{
+ bits::genmask_u32,
+ devres::Devres,
+ drm::{
+ gem::BaseObject,
+ Uninit, //
+ },
+ impl_flags,
+ io::{
+ poll,
+ Io, //
+ },
+ platform,
+ prelude::*,
+ str::CString,
+ sync::{
+ Arc,
+ ArcBorrow, //
+ },
+ time,
+ types::ARef, //
+};
+
+use crate::{
+ driver::{
+ IoMem, //
+ TyrDrmDevice,
+ },
+ fw::parser::{
+ FwParser,
+ ParsedSection, //
+ },
+ gem,
+ gem::KernelBo,
+ gpu::{
+ GpuId,
+ GpuInfo, //
+ },
+ mmu::Mmu,
+ regs,
+ vm::Vm, //
+};
+
+mod parser;
+
+impl_flags!(
+ #[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
+ pub(super) struct SectionFlags(u32);
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ pub(super) enum SectionFlag {
+ Read = 1 << 0,
+ Write = 1 << 1,
+ Exec = 1 << 2,
+ CacheModeNone = 0 << 3,
+ CacheModeCached = 1 << 3,
+ CacheModeUncachedCoherent = 2 << 3,
+ CacheModeCachedCoherent = 3 << 3,
+ Prot = 1 << 5,
+ Shared = 1 << 30,
+ Zero = 1 << 31,
+ }
+);
+
+pub(super) const CACHE_MODE_MASK: SectionFlags = SectionFlags(genmask_u32(3..=4));
+
+pub(super) const CSF_MCU_SHARED_REGION_START: u32 = 0x04000000;
+
+impl SectionFlags {
+ fn cache_mode(&self) -> SectionFlags {
+ *self & CACHE_MODE_MASK
+ }
+}
+
+impl TryFrom<u32> for SectionFlags {
+ type Error = Error;
+
+ fn try_from(value: u32) -> Result<Self, Self::Error> {
+ let valid_flags = SectionFlags::from(SectionFlag::Read)
+ | SectionFlags::from(SectionFlag::Write)
+ | SectionFlags::from(SectionFlag::Exec)
+ | CACHE_MODE_MASK
+ | SectionFlags::from(SectionFlag::Prot)
+ | SectionFlags::from(SectionFlag::Shared)
+ | SectionFlags::from(SectionFlag::Zero);
+
+ if value & valid_flags.0 != value {
+ Err(EINVAL)
+ } else {
+ Ok(Self(value))
+ }
+ }
+}
+
+/// A parsed section of the firmware binary.
+struct Section {
+ // Raw firmware section data for reset purposes
+ #[expect(dead_code)]
+ data: KVec<u8>,
+
+ // Keep the BO backing this firmware section so that both the
+ // GPU mapping and CPU mapping remain valid until the Section is dropped.
+ #[expect(dead_code)]
+ mem: gem::KernelBo,
+}
+
+/// Loaded firmware with sections mapped into MCU VM.
+pub(crate) struct Firmware {
+ /// Platform device reference (needed to access the MCU JOB_IRQ registers).
+ pdev: ARef<platform::Device>,
+
+ /// Iomem need to access registers.
+ iomem: Arc<Devres<IoMem>>,
+
+ /// MCU VM.
+ vm: Arc<Vm>,
+
+ /// List of firmware sections.
+ #[expect(dead_code)]
+ sections: KVec<KBox<Section>>,
+}
+
+impl Drop for Firmware {
+ fn drop(&mut self) {
+ // AS slots retain a VM ref, we need to kill the circular ref manually.
+ self.vm.kill();
+ }
+}
+
+impl Firmware {
+ fn init_section_mem(mem: &mut KernelBo, data: &KVec<u8>) -> Result {
+ if data.is_empty() {
+ return Ok(());
+ }
+
+ let vmap = mem.bo.vmap::<0>()?;
+ let size = mem.bo.size();
+
+ if data.len() > size {
+ pr_err!("fw section {} bigger than BO {}\n", data.len(), size);
+ return Err(EINVAL);
+ }
+
+ for (i, &byte) in data.iter().enumerate() {
+ vmap.try_write8(byte, i)?;
+ }
+
+ Ok(())
+ }
+
+ fn request(
+ ddev: &TyrDrmDevice<Uninit>,
+ gpu_info: &GpuInfo,
+ ) -> Result<kernel::firmware::Firmware> {
+ let gpu_id = GpuId::from(gpu_info.gpu_id);
+
+ let path = CString::try_from_fmt(fmt!(
+ "arm/mali/arch{}.{}/mali_csffw.bin",
+ gpu_id.arch_major,
+ gpu_id.arch_minor
+ ))?;
+
+ kernel::firmware::Firmware::request(&path, ddev.as_ref())
+ }
+
+ fn load(
+ ddev: &TyrDrmDevice<Uninit>,
+ gpu_info: &GpuInfo,
+ ) -> Result<(kernel::firmware::Firmware, KVec<ParsedSection>)> {
+ let fw = Self::request(ddev, gpu_info)?;
+ let mut parser = FwParser::new(fw.data());
+
+ let parsed_sections = parser.parse()?;
+
+ Ok((fw, parsed_sections))
+ }
+
+ /// Load firmware and map sections into MCU VM.
+ pub(crate) fn new(
+ pdev: &platform::Device,
+ iomem: ArcBorrow<'_, Devres<IoMem>>,
+ ddev: &TyrDrmDevice<Uninit>,
+ mmu: ArcBorrow<'_, Mmu>,
+ gpu_info: &GpuInfo,
+ ) -> Result<Arc<Firmware>> {
+ let vm = Vm::new(pdev, ddev, mmu, gpu_info)?;
+
+ let (fw, parsed_sections) = Self::load(ddev, gpu_info)?;
+
+ vm.activate()?;
+
+ let mut sections = KVec::new();
+ for parsed in parsed_sections {
+ let size = (parsed.va.end - parsed.va.start) as usize;
+ let va = u64::from(parsed.va.start);
+
+ let mut mem = KernelBo::new(
+ ddev,
+ vm.as_arc_borrow(),
+ size.try_into().unwrap(),
+ va,
+ parsed.vm_map_flags,
+ )?;
+
+ let section_start = parsed.data_range.start as usize;
+ let section_end = parsed.data_range.end as usize;
+ let mut data = KVec::new();
+ data.extend_from_slice(&fw.data()[section_start..section_end], GFP_KERNEL)?;
+
+ Self::init_section_mem(&mut mem, &data)?;
+
+ sections.push(KBox::new(Section { data, mem }, GFP_KERNEL)?, GFP_KERNEL)?;
+ }
+
+ let firmware = Arc::new(
+ Firmware {
+ pdev: pdev.into(),
+ iomem: iomem.into(),
+ vm,
+ sections,
+ },
+ GFP_KERNEL,
+ )?;
+
+ Ok(firmware)
+ }
+
+ pub(crate) fn boot(&self) -> Result {
+ // SAFETY: Boot is currently only called in the probe path, so we're sure we have a bound
+ // device.
+ let dev = unsafe { self.pdev.as_ref().as_bound() };
+
+ regs::MCU_CONTROL.write(dev, &self.iomem, regs::MCU_CONTROL_AUTO)?;
+
+ if let Err(e) = poll::read_poll_timeout(
+ || regs::MCU_STATUS.read(dev, &self.iomem),
+ |status| *status == regs::MCU_STATUS_ENABLED,
+ time::Delta::from_millis(1),
+ time::Delta::from_millis(100),
+ ) {
+ let status = regs::MCU_STATUS.read(dev, &self.iomem)?;
+ pr_err!("MCU failed to boot, status: {}", status);
+ return Err(e);
+ }
+
+ Ok(())
+ }
+}
diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
index 3807810be7ea..4fdaf8f842ec 100644
--- a/drivers/gpu/drm/tyr/gem.rs
+++ b/drivers/gpu/drm/tyr/gem.rs
@@ -76,7 +76,6 @@ pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) ->
/// A buffer object that is owned and managed by Tyr rather than userspace.
pub(crate) struct KernelBo {
- #[expect(dead_code)]
pub(crate) bo: ARef<Bo>,
vm: Arc<Vm>,
va_range: Range<u64>,
@@ -84,7 +83,6 @@ pub(crate) struct KernelBo {
impl KernelBo {
/// Creates a new kernel-owned buffer object and maps it into GPU VA space.
- #[expect(dead_code)]
pub(crate) fn new<Ctx: DeviceContext>(
ddev: &TyrDrmDevice<Ctx>,
vm: ArcBorrow<'_, Vm>,
diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
index 8e076c35f342..5e7d767fcd44 100644
--- a/drivers/gpu/drm/tyr/mmu.rs
+++ b/drivers/gpu/drm/tyr/mmu.rs
@@ -12,7 +12,6 @@
//!
//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
//! [`SlotManager`]: crate::slot::SlotManager
-#![allow(dead_code)]
use core::ops::Range;
diff --git a/drivers/gpu/drm/tyr/slot.rs b/drivers/gpu/drm/tyr/slot.rs
index 37bf8800a225..b16fd482f2b4 100644
--- a/drivers/gpu/drm/tyr/slot.rs
+++ b/drivers/gpu/drm/tyr/slot.rs
@@ -20,7 +20,6 @@
//!
//! [SlotOperations]: crate::slot::SlotOperations
//! [SlotManager]: crate::slot::SlotManager
-#![allow(dead_code)]
use core::{
mem::take,
diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
index 8e73db3a080a..ba00b1164354 100644
--- a/drivers/gpu/drm/tyr/tyr.rs
+++ b/drivers/gpu/drm/tyr/tyr.rs
@@ -9,6 +9,7 @@
mod driver;
mod file;
+mod fw;
mod gem;
mod gpu;
mod mmu;
diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
index 806bc4e587d6..d75fa1fe80fb 100644
--- a/drivers/gpu/drm/tyr/vm.rs
+++ b/drivers/gpu/drm/tyr/vm.rs
@@ -6,7 +6,6 @@
//! the illusion of owning the entire VA range, similar to CPU virtual memory. Each
//! VM is backed by ARM64 LPAE Stage 1 page tables and can be mapped into hardware
//! address space (AS) slots for GPU execution.
-#![allow(dead_code)]
use core::ops::Range;
--
2.52.0
^ permalink raw reply related [flat|nested] 63+ messages in thread
* Re: [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl
2026-02-12 1:37 ` [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl Deborah Brouwer
@ 2026-02-12 8:12 ` Boris Brezillon
2026-02-28 0:18 ` Deborah Brouwer
2026-02-20 14:03 ` Daniel Almeida
2026-02-21 9:01 ` Alice Ryhl
2 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 8:12 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:03 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> Currently Tyr disables its clocks from TyrDrmDeviceData::drop(), which
> causes them to be shut down before any other fields in TyrDrmDeviceData
> are dropped. This prevents us from using the clocks when dropping the
> other fields in TyrDrmDeviceData.
>
> In order to better control when the clocks are dropped, move this cleanup
> logic into a Drop implementation on the Clocks struct itself.
>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Maybe you should mention that Clocks is no longer considered pinned,
because it's not needed in practice.
Reviewed-by: Boris Brezillon <boris.brezillon@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 23 +++++++++--------------
> 1 file changed, 9 insertions(+), 14 deletions(-)
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index ae4daa12b3e5..9bc6ed56c45e 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -54,7 +54,7 @@ pub(crate) struct TyrPlatformDeviceData {
> _device: ARef<TyrDrmDevice>,
> }
>
> -#[pin_data(PinnedDrop)]
> +#[pin_data]
> pub(crate) struct TyrDrmDeviceData {
> pub(crate) pdev: ARef<platform::Device>,
>
> @@ -168,17 +168,6 @@ impl PinnedDrop for TyrPlatformDeviceData {
> fn drop(self: Pin<&mut Self>) {}
> }
>
> -#[pinned_drop]
> -impl PinnedDrop for TyrDrmDeviceData {
> - fn drop(self: Pin<&mut Self>) {
> - // TODO: the type-state pattern for Clks will fix this.
> - let clks = self.clks.lock();
> - clks.core.disable_unprepare();
> - clks.stacks.disable_unprepare();
> - clks.coregroup.disable_unprepare();
> - }
> -}
> -
> // We need to retain the name "panthor" to achieve drop-in compatibility with
> // the C driver in the userspace stack.
> const INFO: drm::DriverInfo = drm::DriverInfo {
> @@ -202,14 +191,20 @@ impl drm::Driver for TyrDrmDriver {
> }
> }
>
> -#[pin_data]
> struct Clocks {
> core: Clk,
> stacks: OptionalClk,
> coregroup: OptionalClk,
> }
>
> -#[pin_data]
> +impl Drop for Clocks {
> + fn drop(&mut self) {
> + self.core.disable_unprepare();
> + self.stacks.disable_unprepare();
> + self.coregroup.disable_unprepare();
> + }
> +}
> +
> struct Regulators {
> _mali: Regulator<regulator::Enabled>,
> _sram: Regulator<regulator::Enabled>,
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-12 1:37 ` [PATCH 05/12] drm/tyr: add MMU address space registers Deborah Brouwer
@ 2026-02-12 8:16 ` Boris Brezillon
2026-02-28 0:12 ` Deborah Brouwer
2026-02-20 14:21 ` Daniel Almeida
2026-02-21 9:09 ` Alice Ryhl
2 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 8:16 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:06 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
I'm pretty sure I got that from Daniel's branch, and only tweaked a few
minor things to make it work (I probably messed up authorship when
doing that). I'd prefer to attribute that work to Daniel, if you don't
mind.
>
> Add register definitions and constants for managing MMU address space,
> including:
> - Address space translation configuration (page table format, attributes)
> - Memory attributes (cacheability, shareability)
> - Address space commands (update, lock, flush)
> - AsRegister helper for per-AS register access
>
> These will be used by the MMU/VM manager to configure page tables and
> control address space operations.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/regs.rs | 101 +++++++++++++++++++++++++++++++++++-
> 1 file changed, 100 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/tyr/regs.rs b/drivers/gpu/drm/tyr/regs.rs
> index 611870c2e6af..9cb7ab0c806a 100644
> --- a/drivers/gpu/drm/tyr/regs.rs
> +++ b/drivers/gpu/drm/tyr/regs.rs
> @@ -8,7 +8,10 @@
> #![allow(dead_code)]
>
> use kernel::{
> - bits::bit_u32,
> + bits::{
> + bit_u32,
> + bit_u64, //
> + },
> device::{
> Bound,
> Device, //
> @@ -111,3 +114,99 @@ pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u3
> pub(crate) const MMU_IRQ_CLEAR: Register<0x2004> = Register;
> pub(crate) const MMU_IRQ_MASK: Register<0x2008> = Register;
> pub(crate) const MMU_IRQ_STAT: Register<0x200c> = Register;
> +
> +pub(crate) const AS_TRANSCFG_ADRMODE_UNMAPPED: u64 = bit_u64(0);
> +pub(crate) const AS_TRANSCFG_ADRMODE_AARCH64_4K: u64 = bit_u64(2) | bit_u64(1);
> +pub(crate) const AS_TRANSCFG_PTW_MEMATTR_WB: u64 = bit_u64(25);
> +pub(crate) const AS_TRANSCFG_PTW_RA: u64 = bit_u64(30);
> +
> +pub(crate) const fn as_transcfg_ina_bits(x: u64) -> u64 {
> + x << 6
> +}
> +
> +pub(crate) const AS_MEMATTR_AARCH64_SH_MIDGARD_INNER: u32 = 0 << 4;
> +pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_NC: u32 = 1 << 6;
> +pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_WB: u32 = 2 << 6;
> +
> +pub(crate) fn as_memattr_aarch64_inner_alloc_expl(w: bool, r: bool) -> u32 {
> + (3 << 2) | (u32::from(w)) | ((u32::from(r)) << 1)
> +}
> +
> +pub(crate) const AS_COMMAND_UPDATE: u32 = 1;
> +pub(crate) const AS_COMMAND_LOCK: u32 = 2;
> +pub(crate) const AS_COMMAND_FLUSH_PT: u32 = 4;
> +pub(crate) const AS_COMMAND_FLUSH_MEM: u32 = 5;
> +
> +pub(crate) const AS_STATUS_ACTIVE: u32 = bit_u32(0);
> +
> +pub(crate) const AS_LOCK_REGION_MIN_SIZE: u32 = bit_u32(15);
> +
> +/// Maximum number of hardware address space slots.
> +/// The actual number of slots available is usually much lower.
> +pub(crate) const MAX_AS_REGISTERS: usize = 32;
> +
> +const MMU_BASE: usize = 0x2400;
> +const MMU_AS_SHIFT: usize = 6;
> +
> +const fn mmu_as(as_nr: usize) -> usize {
> + MMU_BASE + (as_nr << MMU_AS_SHIFT)
> +}
> +
> +pub(crate) struct AsRegister(usize);
> +
> +impl AsRegister {
> + fn new(as_nr: usize, offset: usize) -> Result<Self> {
> + Ok(AsRegister(mmu_as(as_nr) + offset))
> + }
> +
> + #[inline]
> + pub(crate) fn read(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>) -> Result<u32> {
> + let value = (*iomem).access(dev)?.try_read32(self.0)?;
> + Ok(value)
> + }
> +
> + #[inline]
> + pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u32) -> Result {
> + (*iomem).access(dev)?.try_write32(value, self.0)?;
> + Ok(())
> + }
> +}
> +
> +pub(crate) fn as_transtab_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x0)
> +}
> +
> +pub(crate) fn as_transtab_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x4)
> +}
> +
> +pub(crate) fn as_memattr_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x8)
> +}
> +
> +pub(crate) fn as_memattr_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0xc)
> +}
> +
> +pub(crate) fn as_lockaddr_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x10)
> +}
> +
> +pub(crate) fn as_lockaddr_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x14)
> +}
> +
> +pub(crate) fn as_command(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x18)
> +}
> +
> +pub(crate) fn as_status(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x28)
> +}
> +
> +pub(crate) fn as_transcfg_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x30)
> +}
> +pub(crate) fn as_transcfg_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x34)
> +}
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-02-12 1:37 ` [PATCH 06/12] drm/tyr: add shmem backing for GEM objects Deborah Brouwer
@ 2026-02-12 8:17 ` Boris Brezillon
2026-02-28 0:15 ` Deborah Brouwer
2026-02-20 14:25 ` Daniel Almeida
1 sibling, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 8:17 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:07 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> Add support for GEM buffer objects backed by shared memory.
>
> This introduces the BoCreateArgs structure for passing creation parameters
> including flags, and adds a flags field to BoData. A new_dummy_object()
> helper is provided to create a dummy GEM object for use as a GPUVM root.
>
> The Bo type alias is added to simplify working with Tyr's shmem-backed
> GEM objects throughout the driver.
>
> Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
> 1 file changed, 44 insertions(+), 8 deletions(-)
>
> diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> index c1208d332dea..6a58f2da88d3 100644
> --- a/drivers/gpu/drm/tyr/gem.rs
> +++ b/drivers/gpu/drm/tyr/gem.rs
> @@ -1,28 +1,64 @@
> // SPDX-License-Identifier: GPL-2.0 or MIT
> +//! GEM buffer object management for the Tyr driver.
> +//!
> +//! This module provides buffer object (BO) management functionality using
> +//! DRM's GEM subsystem with shmem backing.
>
> use kernel::{
> drm::{
> gem,
> + gem::shmem,
> DeviceContext, //
> },
> - prelude::*, //
> + prelude::*,
> + sync::aref::ARef, //
> };
>
> -use crate::driver::TyrDrmDriver;
> +use crate::driver::{
> + TyrDrmDevice,
> + TyrDrmDriver, //
> +};
>
> -/// GEM Object inner driver data
> +/// Tyr's DriverObject type for GEM objects.
> #[pin_data]
> -pub(crate) struct BoData {}
> +pub(crate) struct BoData {
> + flags: u32,
> +}
> +
> +/// Provides a way to pass arguments when creating BoData
> +/// as required by the gem::DriverObject trait.
> +pub(crate) struct BoCreateArgs {
> + flags: u32,
> +}
>
> impl gem::DriverObject for BoData {
> type Driver = TyrDrmDriver;
> - type Args = ();
> + type Args = BoCreateArgs;
>
> fn new<Ctx: DeviceContext>(
> - _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
> + _dev: &TyrDrmDevice<Ctx>,
> _size: usize,
> - _args: (),
> + args: BoCreateArgs,
> ) -> impl PinInit<Self, Error> {
> - try_pin_init!(BoData {})
> + try_pin_init!(Self { flags: args.flags })
> }
> }
> +
> +/// Type alias for Tyr GEM buffer objects.
> +pub(crate) type Bo = gem::shmem::Object<BoData>;
> +
> +/// Creates a dummy GEM object to serve as the root of a GPUVM.
> +#[expect(dead_code)]
> +pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
> + let bo = gem::shmem::Object::<BoData>::new(
> + ddev,
> + 4096,
> + shmem::ObjectConfig {
> + map_wc: true,
> + parent_resv_obj: None,
> + },
> + BoCreateArgs { flags: 0 },
> + )?;
> +
> + Ok(bo)
> +}
Nit: I'd probably move this new_dummy_object() addition to the commit
introducing Vm support.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-12 1:37 ` [PATCH 07/12] drm/tyr: Add generic slot manager Deborah Brouwer
@ 2026-02-12 10:11 ` Boris Brezillon
2026-02-12 10:45 ` Miguel Ojeda
` (3 more replies)
0 siblings, 4 replies; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 10:11 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:08 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Introduce a generic slot manager to dynamically allocate limited hardware
> slots to software "seats". It can be used for both address space (AS) and
> command stream group (CSG) slots.
>
> The slot manager initially assigns seats to its free slots. It then
> continues to reuse the same slot for a seat, as long as another seat
> did not start to use the slot in the interim.
>
> When contention arises because all of the slots are allocated, the slot
> manager will lazily evict and reuse slots that have become idle (if any).
>
> The seat state is protected using the LockedBy pattern with the same lock
> that guards the SlotManager. This ensures the seat state stays consistent
> across slot operations.
>
> Hardware specific behaviour can be customized using the slot manager's
> `SlotOperations` trait.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/slot.rs | 359 ++++++++++++++++++++++++++++++++++++
> drivers/gpu/drm/tyr/tyr.rs | 1 +
> 2 files changed, 360 insertions(+)
> create mode 100644 drivers/gpu/drm/tyr/slot.rs
>
> diff --git a/drivers/gpu/drm/tyr/slot.rs b/drivers/gpu/drm/tyr/slot.rs
> new file mode 100644
> index 000000000000..37bf8800a225
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/slot.rs
> @@ -0,0 +1,359 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +
> +//! Slot management abstraction for limited hardware resources.
> +//!
> +//! This module provides a generic [`SlotManager`] that assigns limited hardware
> +//! slots to logical "seats". A seat represents an entity (such as a vm address
> +//! space) that needs access to a hardware slot.
> +//!
> +//! The [`SlotManager`] tracks slot allocation using sequence numbers to detect
> +//! when a seat's binding has been invalidated. When a seat requests activation,
> +//! the manager will either reuse the seat's existing slot (if still valid),
> +//! allocate a free slot (if any are available), or evict the oldest idle slot if any
> +//! slots are idle.
> +//!
> +//! Hardware-specific behavior is customized by implementing the [`SlotOperations`]
> +//! trait, which allows callbacks when slots are activated or evicted.
> +//!
> +//! This is primarily used for managing address space slots in the GPU, where
> +//! the number of hardware address space slots is limited.
I'd probably mention that we intend to use it for other stuff (Csg
slots), hence the generalization done here.
> +//!
> +//! [SlotOperations]: crate::slot::SlotOperations
> +//! [SlotManager]: crate::slot::SlotManager
Thanks a lot for adding some docs to my barebone initial implementation
and fixing the stuff I got wrong along the way. :D
> +#![allow(dead_code)]
> +
> +use core::{
> + mem::take,
> + ops::{
> + Deref,
> + DerefMut, //
> + }, //
> +};
> +
> +use kernel::{
> + prelude::*,
> + sync::LockedBy, //
> +};
> +
Don't know what the doc rules are in rust, but for this sort of generic
layer, maybe we should provide extensive docs around objects, fields
and public functions. I see that most struct fields are documented, but
not the struct themselves. the enum doesn't seem to be documented, and
some of the public functions are not either. And that's all my fault,
because I gave you this raw piece of code without much doc (you added a
lot already). Just saying that, maybe now that things have settled
down, is a good time to add proper doc where it's missing.
/// Seat information.
///
/// This can't be accessed directly by the element embedding a `Seat`,
/// but is used by the generic slot manager logic to control residency
/// of a certain object on a hardware slot.
> +pub(crate) struct SeatInfo {
> + /// Slot used by this seat.
///
/// This index is only valid if the slot pointed by this index
/// has its `SlotInfo::seqno` match SeatInfo::seqno. Otherwise,
/// it means the object has been evicted from the hardware slot,
/// and a new slot needs to be acquired to make this object
/// resident again.
> + slot: u8,
> +
> + /// Sequence number encoding the last time this seat was active.
> + /// We also use it to check if a slot is still bound to a seat.
> + seqno: u64,
> +}
> +
/// Seat state
///
/// This is meant to be embedded in the object that wants to acquire
/// hardware slots. It also starts in the `Seat::NoSeat` state, and
/// the slot manager will change the object value when an active/evict
/// request to is issued.
> +#[derive(Default)]
> +pub(crate) enum Seat {
> + #[expect(clippy::enum_variant_names)]
/// Resource is not resident.
///
/// All objects start with a seat in that state. The seat also
/// gets back to that state if the user requests eviction. It
/// can also end up in that state next time an operation is done
/// on an `Seat::Idle` seat and the slot managers finds out this
/// object has been evicted from the slot.
> + #[default]
> + NoSeat,
/// Resource is actively used and resident.
///
/// When a seat is in that state, it can't be evicted, and the
/// slot pointed by `SlotInfo::slot` is guaranteed to be reserved
/// for this object as long as the seat stays active.
> + Active(SeatInfo),
/// Resource is idle and might or might not be resident.
///
/// When a seat is in that state, we can't know for sure if the
/// object is resident or evicted until the next request we issue
/// to the slot manager. This tells the slot manager it can
/// reclaim the underlying slot if needed.
/// In order for the hardware to use this object again, the seat
/// needs to be turned into an `Seat::Active` state again
/// with a `SlotManager::activate()` call.
> + Idle(SeatInfo),
> +}
> +
> +impl Seat {
/// Get the slot index this seat is pointing to.
///
/// If the seat is not `Seat::Active` we can't trust the
/// `SeatInfo`. In that case `None` is returned, otherwise
/// `Some(SeatInfo::slot)` is returned.
> + pub(super) fn slot(&self) -> Option<u8> {
> + match self {
> + Self::Active(info) => Some(info.slot),
> + _ => None,
> + }
> + }
> +}
> +
/// Trait describing the slot-related operations.
> +pub(crate) trait SlotOperations {
> + type SlotData;
> +
> + /// Called when a slot is being activated for a seat.
> + ///
> + /// This callback allows hardware-specific actions to be performed when a slot
> + /// becomes active, such as updating hardware registers or invalidating caches.
> + fn activate(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> + Ok(())
> + }
> +
> + /// Called when a slot is being evicted and freed.
> + ///
> + /// This callback allows hardware-specific cleanup when a slot is being
> + /// completely freed, either explicitly or when an idle slot is being
> + /// reused for a different seat. Any hardware state should be invalidated.
> + fn evict(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> + Ok(())
> + }
> +}
> +
/// Data attached to a slot.
> +struct SlotInfo<T> {
> + /// Type specific data attached to a slot
> + slot_data: T,
> +
> + /// Sequence number from when this slot was last activated
> + seqno: u64,
> +}
> +
/// Slot state.
> +#[derive(Default)]
> +enum Slot<T> {
/// Slot is free.
///
/// All slots start in this state when the slot manager is created.
> + #[default]
> + Free,
/// Slot is active.
///
/// When is that state, the slot is guaranteed to stay active
/// for as long as the resource bound to it has its seat in the
/// `Seat::Active` state. No new resource can be bound to it.
> + Active(SlotInfo<T>),
/// Slot is idle.
///
/// Happens when the underlying resource has been flagged
/// `Seat::Idle`. When in that state, the slot manager is allowed
/// to evict the resource and re-assign the slot to someone else.
/// This process involves updating the `SlotInfo::seqno` which
/// will be checked against the `SeatInfo::seqno` in case the idle
/// resource wants to become active again.
> + Idle(SlotInfo<T>),
> +}
> +
/// Generic slot manager object.
///
/// It abstracts away all the churn around activeness/idleness tracking
/// and let the implementer of the SlotOperations trait focus on how to
/// make a resource active or evict it.
> +pub(crate) struct SlotManager<T: SlotOperations, const MAX_SLOTS: usize> {
> + /// Manager specific data
> + manager: T,
> +
> + /// Number of slots actually available
> + slot_count: usize,
> +
> + /// Slots
> + slots: [Slot<T::SlotData>; MAX_SLOTS],
> +
> + /// Sequence number incremented each time a Seat is successfully activated
> + use_seqno: u64,
> +}
> +
> +// A `Seat` protected by the same lock that is used to wrap the `SlotManager`.
Should this be
/// A `Seat` ....
?
> +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
> +
> +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
Do we really need to explicitly flag this type Unpin? I thought this
was the default if the struct is not pinned (and it's not AFAICT).
> +
> +impl<T: SlotOperations, const MAX_SLOTS: usize> SlotManager<T, MAX_SLOTS> {
/// Creates a new slot manager.
> + pub(crate) fn new(manager: T, slot_count: usize) -> Result<Self> {
> + if slot_count == 0 {
> + return Err(EINVAL);
> + }
> + if slot_count > MAX_SLOTS {
> + return Err(EINVAL);
> + }
> + Ok(Self {
> + manager,
> + slot_count,
> + slots: [const { Slot::Free }; MAX_SLOTS],
> + use_seqno: 1,
> + })
> + }
> +
> + fn record_active_slot(
> + &mut self,
> + slot_idx: usize,
> + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> + slot_data: T::SlotData,
> + ) -> Result {
> + let cur_seqno = self.use_seqno;
> +
> + *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> + slot: slot_idx as u8,
> + seqno: cur_seqno,
> + });
> +
> + self.slots[slot_idx] = Slot::Active(SlotInfo {
> + slot_data,
> + seqno: cur_seqno,
> + });
> +
> + self.use_seqno += 1;
> + Ok(())
> + }
> +
> + fn activate_slot(
> + &mut self,
> + slot_idx: usize,
> + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> + slot_data: T::SlotData,
> + ) -> Result {
> + self.manager.activate(slot_idx, &slot_data)?;
> + self.record_active_slot(slot_idx, locked_seat, slot_data)
> + }
> +
> + fn allocate_slot(
> + &mut self,
> + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> + slot_data: T::SlotData,
> + ) -> Result {
> + let slots = &self.slots[..self.slot_count];
> +
> + let mut idle_slot_idx = None;
> + let mut idle_slot_seqno: u64 = 0;
> +
> + for (slot_idx, slot) in slots.iter().enumerate() {
> + match slot {
> + Slot::Free => {
> + return self.activate_slot(slot_idx, locked_seat, slot_data);
> + }
> + Slot::Idle(slot_info) => {
> + if idle_slot_idx.is_none() || slot_info.seqno < idle_slot_seqno {
> + idle_slot_idx = Some(slot_idx);
> + idle_slot_seqno = slot_info.seqno;
> + }
> + }
> + Slot::Active(_) => (),
> + }
> + }
> +
> + match idle_slot_idx {
> + Some(slot_idx) => {
> + // Lazily evict idle slot just before it is reused
> + if let Slot::Idle(slot_info) = &self.slots[slot_idx] {
> + self.manager.evict(slot_idx, &slot_info.slot_data)?;
> + }
> + self.activate_slot(slot_idx, locked_seat, slot_data)
> + }
> + None => {
> + pr_err!(
> + "Slot allocation failed: all {} slots in use\n",
> + self.slot_count
> + );
> + Err(EBUSY)
> + }
> + }
> + }
> +
> + fn idle_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> + let slot = take(&mut self.slots[slot_idx]);
> +
> + if let Slot::Active(slot_info) = slot {
> + self.slots[slot_idx] = Slot::Idle(SlotInfo {
> + slot_data: slot_info.slot_data,
> + seqno: slot_info.seqno,
> + })
> + };
> +
> + *locked_seat.access_mut(self) = match locked_seat.access(self) {
> + Seat::Active(seat_info) | Seat::Idle(seat_info) => Seat::Idle(SeatInfo {
> + slot: seat_info.slot,
> + seqno: seat_info.seqno,
> + }),
> + Seat::NoSeat => Seat::NoSeat,
> + };
> + Ok(())
> + }
> +
> + fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> + match &self.slots[slot_idx] {
> + Slot::Active(slot_info) | Slot::Idle(slot_info) => {
> + self.manager.evict(slot_idx, &slot_info.slot_data)?;
> + take(&mut self.slots[slot_idx]);
> + }
> + _ => (),
> + }
> +
> + *locked_seat.access_mut(self) = Seat::NoSeat;
> + Ok(())
> + }
> +
> + // Checks and updates the seat state based on the slot it points to
> + // (if any). Returns a Seat with a value matching the slot state.
> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> + let new_seat = match locked_seat.access(self) {
> + Seat::Active(seat_info) => {
> + let old_slot_idx = seat_info.slot as usize;
> + let slot = &self.slots[old_slot_idx];
> +
> + if kernel::warn_on!(
> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
> + ) {
> + Seat::NoSeat
> + } else {
> + Seat::Active(SeatInfo {
> + slot: seat_info.slot,
> + seqno: seat_info.seqno,
> + })
> + }
> + }
> +
> + Seat::Idle(seat_info) => {
> + let old_slot_idx = seat_info.slot as usize;
> + let slot = &self.slots[old_slot_idx];
> +
> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
> + Seat::NoSeat
> + } else {
> + Seat::Idle(SeatInfo {
> + slot: seat_info.slot,
> + seqno: seat_info.seqno,
> + })
> + }
> + }
> +
> + _ => Seat::NoSeat,
> + };
> +
> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> + // so that only slot.rs can change the seat state, but there might be better solutions
> + // to prevent that.
Okay, I guess we want some inputs from Daniel and/or Alice on that one.
> + match &new_seat {
> + Seat::Active(seat_info) => {
> + *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> + slot: seat_info.slot,
> + seqno: seat_info.seqno,
> + })
> + }
> + Seat::Idle(seat_info) => {
> + *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
> + slot: seat_info.slot,
> + seqno: seat_info.seqno,
> + })
> + }
> + _ => *locked_seat.access_mut(self) = Seat::NoSeat,
> + }
> +
> + new_seat
> + }
> +
/// Make a resource active on any available/reclaimable slot.
///
/// Will return an error if no slot is available/reclaimable, or if
/// the reclaim failed.
> + pub(crate) fn activate(
> + &mut self,
> + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> + slot_data: T::SlotData,
> + ) -> Result {
> + let seat = self.check_seat(locked_seat);
> + match seat {
> + Seat::Active(seat_info) | Seat::Idle(seat_info) => {
> + // With lazy eviction, if seqno matches, the hardware state is still
> + // valid for both Active and Idle slots, so just update our records
> + self.record_active_slot(seat_info.slot as usize, locked_seat, slot_data)
> + }
> + _ => self.allocate_slot(locked_seat, slot_data),
> + }
> + }
> +
/// Flag a resource idle.
///
/// The slot manager can decide to reclaim the slot this resource
/// was bound to at any point after function returns.
> + // The idle() method will be used when we start adding support for user VMs
> + #[expect(dead_code)]
> + pub(crate) fn idle(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> + let seat = self.check_seat(locked_seat);
> + if let Seat::Active(seat_info) = seat {
> + self.idle_slot(seat_info.slot as usize, locked_seat)?;
> + }
> + Ok(())
> + }
> +
> + /// Evict a seat from its slot, freeing up the hardware resource.
I think I'd go:
/// Evict a resource from its slot, and make this slot free again
/// for other users.
> + pub(crate) fn evict(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> + let seat = self.check_seat(locked_seat);
> +
> + match seat {
> + Seat::Active(seat_info) | Seat::Idle(seat_info) => {
> + let slot_idx = seat_info.slot as usize;
> +
> + self.evict_slot(slot_idx, locked_seat)?;
> + }
> + _ => (),
> + }
> +
> + Ok(())
> + }
> +}
> +
> +impl<T: SlotOperations, const MAX_SLOTS: usize> Deref for SlotManager<T, MAX_SLOTS> {
> + type Target = T;
> +
> + fn deref(&self) -> &Self::Target {
> + &self.manager
> + }
> +}
> +
> +impl<T: SlotOperations, const MAX_SLOTS: usize> DerefMut for SlotManager<T, MAX_SLOTS> {
> + fn deref_mut(&mut self) -> &mut Self::Target {
> + &mut self.manager
> + }
> +}
> diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> index 6eaa2135fe07..f54b997355e0 100644
> --- a/drivers/gpu/drm/tyr/tyr.rs
> +++ b/drivers/gpu/drm/tyr/tyr.rs
> @@ -12,6 +12,7 @@
> mod gem;
> mod gpu;
> mod regs;
> +mod slot;
>
> kernel::module_platform_driver! {
> type: TyrPlatformDeviceData,
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address
2026-02-12 1:37 ` [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address Deborah Brouwer
@ 2026-02-12 10:16 ` Boris Brezillon
2026-02-20 14:19 ` Daniel Almeida
2026-02-21 9:03 ` Alice Ryhl
2 siblings, 0 replies; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 10:16 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:05 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> From: Beata Michalska <beata.michalska@arm.com>
>
> Configure the device DMA mask during probe using the GPU's physical
> address capability reported in GpuInfo. This ensures DMA allocations
> use an appropriate address mask.
>
> Signed-off-by: Beata Michalska <beata.michalska@arm.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Reviewed-by: Boris Brezillon <boris.brezillon@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 11 +++++++++++
> drivers/gpu/drm/tyr/gpu.rs | 1 -
> 2 files changed, 11 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index e5eae5a73371..2973a8b3cc09 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -11,6 +11,10 @@
> Device, //
> },
> devres::Devres,
> + dma::{
> + Device as DmaDevice,
> + DmaMask, //
> + },
> drm,
> drm::{
> driver::Registration,
> @@ -134,6 +138,13 @@ fn probe(
> let gpu_info = GpuInfo::new(pdev.as_ref(), &iomem)?;
> gpu_info.log(pdev);
>
> + // SAFETY: No concurrent DMA allocations or mappings can be made because
> + // the device is still being probed and therefore isn't being used by
> + // other threads of execution.
> + unsafe {
> + pdev.dma_set_mask_and_coherent(DmaMask::try_new(gpu_info.pa_bits())?)?;
> + }
> +
> let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> let platform: ARef<platform::Device> = pdev.into();
>
> diff --git a/drivers/gpu/drm/tyr/gpu.rs b/drivers/gpu/drm/tyr/gpu.rs
> index affca5b0dc6c..b5f11bc96fa0 100644
> --- a/drivers/gpu/drm/tyr/gpu.rs
> +++ b/drivers/gpu/drm/tyr/gpu.rs
> @@ -141,7 +141,6 @@ pub(crate) fn va_bits(&self) -> u32 {
> }
>
> /// Returns the number of physical address bits supported by the GPU.
> - #[expect(dead_code)]
> pub(crate) fn pa_bits(&self) -> u32 {
> (self.mmu_features >> 8) & genmask_u32(0..=7)
> }
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
@ 2026-02-12 10:44 ` Boris Brezillon
2026-02-28 0:31 ` Deborah Brouwer
2026-02-12 11:05 ` Boris Brezillon
` (2 subsequent siblings)
3 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 10:44 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:09 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> SlotManager for allocating hardware address space slots. The underlying
> AddressSpaceManager performs MMU operations including enabling/disabling
> address spaces, flushing page tables, and locking regions for page table
> updates.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 3 +
> drivers/gpu/drm/tyr/mmu.rs | 91 +++++++
> drivers/gpu/drm/tyr/mmu/address_space.rs | 322 +++++++++++++++++++++++
> drivers/gpu/drm/tyr/tyr.rs | 1 +
> 4 files changed, 417 insertions(+)
> create mode 100644 drivers/gpu/drm/tyr/mmu.rs
> create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index 2973a8b3cc09..ad5a765a6c2a 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -43,6 +43,7 @@
> gem::BoData,
> gpu,
> gpu::GpuInfo,
> + mmu::Mmu,
> regs, //
> };
>
> @@ -148,6 +149,8 @@ fn probe(
> let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> let platform: ARef<platform::Device> = pdev.into();
>
> + let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
> +
> let data = try_pin_init!(TyrDrmDeviceData {
> pdev: platform.clone(),
> clks <- new_mutex!(Clocks {
> diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
> new file mode 100644
> index 000000000000..8e076c35f342
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu.rs
> @@ -0,0 +1,91 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +
> +//! Memory Management Unit (MMU) driver for the Tyr GPU.
> +//!
> +//! This module manages GPU address spaces and virtual memory operations through
> +//! hardware MMU slots. It provides functionality for flushing page tables and
> +//! managing VM updates for active address spaces.
> +//!
> +//! The MMU coordinates with the [`AddressSpaceManager`] to handle hardware
> +//! address space allocation and page table operations, using [`SlotManager`]
> +//! to track which address spaces are currently active in hardware slots.
> +//!
> +//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
> +//! [`SlotManager`]: crate::slot::SlotManager
> +#![allow(dead_code)]
> +
> +use core::ops::Range;
> +
> +use kernel::{
> + devres::Devres,
> + new_mutex,
> + platform,
> + prelude::*,
> + sync::{
> + Arc,
> + ArcBorrow,
> + Mutex, //
> + }, //
> +};
> +
> +use crate::{
> + driver::IoMem,
> + gpu::GpuInfo,
> + mmu::address_space::{
> + AddressSpaceManager,
> + VmAsData, //
> + },
> + regs::MAX_AS_REGISTERS,
> + slot::{
> + SlotManager, //
> + }, //
formatting nit:
slot::SlotManager, //
> +};
> +
> +pub(crate) mod address_space;
> +
> +pub(crate) type AsSlotManager = SlotManager<AddressSpaceManager, MAX_AS_REGISTERS>;
> +
/// MMU component of the GPU.
///
/// This is used to bind VM objects to an AS (Address Space) slot
/// and make the VM active on the GPU.
> +#[pin_data]
> +pub(crate) struct Mmu {
> + /// Manages the allocation of hardware MMU slots to GPU address spaces.
> + ///
> + /// Tracks which address spaces are currently active in hardware slots and
> + /// coordinates address space operations like flushing and VM updates.
> + #[pin]
> + pub(crate) as_manager: Mutex<AsSlotManager>,
> +}
> +
> +impl Mmu {
/// Create an MMU component for this device.
> + pub(crate) fn new(
> + pdev: &platform::Device,
> + iomem: ArcBorrow<'_, Devres<IoMem>>,
> + gpu_info: &GpuInfo,
> + ) -> Result<Arc<Mmu>> {
> + let slot_count = gpu_info.as_present.count_ones().try_into()?;
> + let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
> + let mmu_init = try_pin_init!(Self{
> + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
> + });
> + Arc::pin_init(mmu_init, GFP_KERNEL)
> + }
> +
/// Make a VM active.
///
/// This implies assigning the VM to an AS slot through the slot
/// manager.
> + pub(crate) fn activate_vm(&self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> + self.as_manager.lock().activate_vm(vm)
> + }
> +
/// Make the VM inactive.
///
/// Evicts the VM from its AS slot through the slot manager.
> + pub(crate) fn deactivate_vm(&self, vm: &VmAsData) -> Result {
> + self.as_manager.lock().deactivate_vm(vm)
> + }
> +
/// Flush caches after a VM update.
///
/// If the VM is no longer resident, this is a NOP, otherwise, the
/// AS manager will flush the GPU and MMU (TLB) caches.
> + pub(crate) fn flush_vm(&self, vm: &VmAsData) -> Result {
> + self.as_manager.lock().flush_vm(vm)
> + }
> +
/// Flags the start of a VM update.
///
/// If the VM is resident, any GPU access on the memory range being
/// updated will be blocked until `Mmu::end_vm_update()` is called.
/// This guarantees the atomicity of a VM update.
/// If the VM is not resident, this is a NOP.
> + pub(crate) fn start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result {
> + self.as_manager.lock().start_vm_update(vm, region)
> + }
> +
/// Flags the end of a VM update.
///
/// If the VM is resident, this will let GPU accesse on the updated
/// range go through, in case any of them were blocked.
/// If the VM is not resident, this is a NOP.
> + pub(crate) fn end_vm_update(&self, vm: &VmAsData) -> Result {
> + self.as_manager.lock().end_vm_update(vm)
> + }
> +}
> diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
> new file mode 100644
> index 000000000000..60e9a79112f0
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
> @@ -0,0 +1,322 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +
> +//! GPU address space management and hardware operations.
> +//!
> +//! This module manages GPU hardware address spaces, including configuration,
> +//! command submission, and page table update regions. It handles the hardware
> +//! interaction for MMU operations through MMIO register access.
> +//!
> +//! The [`AddressSpaceManager`] implements [`SlotOperations`] to integrate with
> +//! the slot management system, enabling and configuring address spaces in the
> +//! hardware slots as needed.
> +//!
> +//! [`SlotOperations`]: crate::slot::SlotOperations
> +
> +use core::ops::Range;
> +
> +use kernel::{
> + bits::*,
> + device::{
> + Bound,
> + Device, //
> + },
> + devres::Devres,
> + error::Result,
> + io,
> + iommu::pgtable::{
> + IoPageTable,
> + ARM64LPAES1, //
> + },
> + platform,
> + prelude::*,
> + sync::{
> + aref::ARef,
> + Arc,
> + ArcBorrow,
> + LockedBy, //
> + },
> + time::Delta, //
> +};
> +
> +use crate::{
> + driver::IoMem,
> + mmu::{
> + AsSlotManager,
> + Mmu, //
> + },
> + regs::*,
> + slot::{
> + Seat,
> + SlotOperations, //
> + }, //
> +};
> +
> +/// Hardware address space configuration registers.
> +///
> +/// Contains the values to be written to the GPU's AS registers when
> +/// activating this address space.
> +#[derive(Clone, Copy)]
> +pub(crate) struct AddressSpaceConfig {
> + pub(crate) transcfg: u64,
> + pub(crate) transtab: u64,
> + pub(crate) memattr: u64,
Don't know if we need to document those fields.
> +}
> +
> +/// Any resource/information that will be used by the AddressSpaceManager
> +/// to make a VM active is present in VmAsData.
> +///
> +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> +/// the slot to make sure the page table and the underlying resources
> +/// (pages) used by the AS slot won't go away while the MMU points to
> +/// those.
> +pub(crate) struct VmAsData {
> + /// Tracks this VM's binding to a hardware address space slot.
> + as_seat: LockedBy<Seat, AsSlotManager>,
> + /// Hardware configuration for this address space.
> + as_config: AddressSpaceConfig,
> + /// Page table (managed by devres).
> + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> +}
> +
> +impl VmAsData {
Missing doc.
> + pub(crate) fn new(
> + mmu: &Mmu,
> + as_config: AddressSpaceConfig,
> + page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> + ) -> VmAsData {
> + Self {
> + as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
> + as_config,
> + page_table,
> + }
> + }
> +}
> +
> +/// Manages GPU hardware address spaces via MMIO register operations.
> +pub(crate) struct AddressSpaceManager {
Missing docs on the first two fields.
> + pdev: ARef<platform::Device>,
> + iomem: Arc<Devres<IoMem>>,
> + /// Bitmask of available address space slots from GPU_AS_PRESENT register
> + as_present: u32,
> +}
> +
> +impl SlotOperations for AddressSpaceManager {
> + type SlotData = Arc<VmAsData>;
> +
> + fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) -> Result {
> + self.as_enable(slot_idx, &slot_data.as_config)
> + }
> +
> + fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> + if self.iomem.try_access().is_some() {
> + let _ = self.as_flush(slot_idx);
> + let _ = self.as_disable(slot_idx);
I don't think we should ignore the errors returned by
as_flush/disable(). Ultimately, what we should do is trigger a GPU
reset and leave the slot 'busy' until we're sure no one will try
to use it (the "stop everything" happening in the reset path). Once this
is done, we can forcibly evict the VM (basically remove the VM from the
slot without doing any HW transaction) and let the reset do its job.
Since we're not yet at a point where we have a functional reset flow,
I'd recommend failing the eviction if as_flush/disable() returns an
error (shouldn't happen with just the FW-boot logic anyway). This means
we might leave active slots behind and possibly leak resources when that
happens, but that's better than the UAF we'd create if the AS still
points to a page table that's been freed.
> + }
> + Ok(())
> + }
> +}
> +
> +impl AddressSpaceManager {
Missing docs.
> + pub(super) fn new(
> + pdev: &platform::Device,
> + iomem: ArcBorrow<'_, Devres<IoMem>>,
> + as_present: u32,
> + ) -> Result<AddressSpaceManager> {
> + Ok(Self {
> + pdev: pdev.into(),
> + iomem: iomem.into(),
> + as_present,
> + })
> + }
> +
> + fn dev(&self) -> &Device<Bound> {
> + // SAFETY: pdev is a bound device.
> + unsafe { self.pdev.as_ref().as_bound() }
> + }
> +
> + fn validate_as_slot(&self, as_nr: usize) -> Result {
> + if as_nr >= MAX_AS_REGISTERS {
> + pr_err!(
> + "AS slot {} out of valid range (max {})\n",
> + as_nr,
> + MAX_AS_REGISTERS
> + );
> + return Err(EINVAL);
> + }
> +
> + if (self.as_present & (1 << as_nr)) == 0 {
> + pr_err!(
> + "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
> + as_nr,
> + self.as_present
> + );
> + return Err(EINVAL);
> + }
> +
> + Ok(())
> + }
> +
> + fn as_wait_ready(&self, as_nr: usize) -> Result {
> + let op = || as_status(as_nr)?.read(self.dev(), &self.iomem);
> + let cond = |status: &u32| -> bool { *status & AS_STATUS_ACTIVE == 0 };
> + let _ =
> + io::poll::read_poll_timeout(op, cond, Delta::from_millis(0), Delta::from_millis(10))?;
> +
> + Ok(())
> + }
> +
> + fn as_send_cmd(&mut self, as_nr: usize, cmd: u32) -> Result {
> + self.as_wait_ready(as_nr)?;
> + as_command(as_nr)?.write(self.dev(), &self.iomem, cmd)?;
> + Ok(())
> + }
> +
> + fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: u32) -> Result {
> + self.as_send_cmd(as_nr, cmd)?;
> + self.as_wait_ready(as_nr)?;
> + Ok(())
> + }
> +
> + fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig) -> Result {
> + self.validate_as_slot(as_nr)?;
> +
> + let transtab = as_config.transtab;
> + let transcfg = as_config.transcfg;
> + let memattr = as_config.memattr;
> +
> + let transtab_lo = (transtab & 0xffffffff) as u32;
> + let transtab_hi = (transtab >> 32) as u32;
> +
> + let transcfg_lo = (transcfg & 0xffffffff) as u32;
> + let transcfg_hi = (transcfg >> 32) as u32;
> +
> + let memattr_lo = (memattr & 0xffffffff) as u32;
> + let memattr_hi = (memattr >> 32) as u32;
> +
> + let dev = self.dev();
> + as_transtab_lo(as_nr)?.write(dev, &self.iomem, transtab_lo)?;
> + as_transtab_hi(as_nr)?.write(dev, &self.iomem, transtab_hi)?;
> +
> + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, transcfg_lo)?;
> + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, transcfg_hi)?;
> +
> + as_memattr_lo(as_nr)?.write(dev, &self.iomem, memattr_lo)?;
> + as_memattr_hi(as_nr)?.write(dev, &self.iomem, memattr_hi)?;
> +
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> +
> + Ok(())
> + }
> +
> + fn as_disable(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> +
> + // Flush AS before disabling
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_MEM)?;
> +
> + let dev = self.dev();
> + as_transtab_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> + as_transtab_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> +
> + as_memattr_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> + as_memattr_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> +
> + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, AS_TRANSCFG_ADRMODE_UNMAPPED as u32)?;
> + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> +
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> +
> + Ok(())
> + }
> +
> + fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
> + self.validate_as_slot(as_nr)?;
> +
> + // The locked region is a naturally aligned power of 2 block encoded as
> + // log2 minus(1).
> + //
> + // Calculate the desired start/end and look for the highest bit which
> + // differs. The smallest naturally aligned block must include this bit
> + // change, the desired region starts with this bit (and subsequent bits)
> + // zeroed and ends with the bit (and subsequent bits) set to one.
> + let region_width = core::cmp::max(
> + 64 - (region.start ^ (region.end - 1)).leading_zeros() as u8,
> + AS_LOCK_REGION_MIN_SIZE.trailing_zeros() as u8,
> + ) - 1;
> +
> + // Mask off the low bits of region.start, which would be ignored by the
> + // hardware anyways.
> + let region_start =
> + region.start & genmask_checked_u64(u32::from(region_width)..=63).ok_or(EINVAL)?;
> +
> + let region = (u64::from(region_width)) | region_start;
> +
> + let region_lo = (region & 0xffffffff) as u32;
> + let region_hi = (region >> 32) as u32;
> +
> + // Lock the region that needs to be updated.
> + let dev = self.dev();
> + as_lockaddr_lo(as_nr)?.write(dev, &self.iomem, region_lo)?;
> + as_lockaddr_hi(as_nr)?.write(dev, &self.iomem, region_hi)?;
> +
> + self.as_send_cmd(as_nr, AS_COMMAND_LOCK)
> + }
> +
> + fn as_end_update(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_PT)
> + }
> +
> + fn as_flush(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> + self.as_send_cmd(as_nr, AS_COMMAND_FLUSH_PT)
> + }
> +}
> +
> +impl AsSlotManager {
> + /// Locks a region for page table updates if the VM has an active slot.
> + pub(super) fn start_vm_update(&mut self, vm: &VmAsData, region: &Range<u64>) -> Result {
> + let seat = vm.as_seat.access(self);
> + match seat.slot() {
> + Some(slot) => {
> + let as_nr = slot as usize;
> + self.as_start_update(as_nr, region)
> + }
> + _ => Ok(()),
> + }
> + }
> +
> + /// Flushes page table updates for a VM if it has an active slot.
> + pub(super) fn end_vm_update(&mut self, vm: &VmAsData) -> Result {
> + let seat = vm.as_seat.access(self);
> + match seat.slot() {
> + Some(slot) => {
> + let as_nr = slot as usize;
> + self.as_end_update(as_nr)
> + }
> + _ => Ok(()),
> + }
> + }
> +
> + /// Flushes page tables for a VM if it has an active slot.
> + pub(super) fn flush_vm(&mut self, vm: &VmAsData) -> Result {
> + let seat = vm.as_seat.access(self);
> + match seat.slot() {
> + Some(slot) => {
> + let as_nr = slot as usize;
> + self.as_flush(as_nr)
> + }
> + _ => Ok(()),
> + }
> + }
> +
> + /// Flushes page tables for a VM if it has an active slot.
> + pub(super) fn activate_vm(&mut self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> + self.activate(&vm.as_seat, vm.into())
> + }
> +
> + /// Flushes page tables for a VM if it has an active slot.
> + pub(super) fn deactivate_vm(&mut self, vm: &VmAsData) -> Result {
> + self.evict(&vm.as_seat)
> + }
> +}
> diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> index f54b997355e0..ae435c7e80b1 100644
> --- a/drivers/gpu/drm/tyr/tyr.rs
> +++ b/drivers/gpu/drm/tyr/tyr.rs
> @@ -11,6 +11,7 @@
> mod file;
> mod gem;
> mod gpu;
> +mod mmu;
> mod regs;
> mod slot;
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-12 10:11 ` Boris Brezillon
@ 2026-02-12 10:45 ` Miguel Ojeda
2026-02-20 15:25 ` Daniel Almeida
` (2 subsequent siblings)
3 siblings, 0 replies; 63+ messages in thread
From: Miguel Ojeda @ 2026-02-12 10:45 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, daniel.almeida,
aliceryhl, beata.michalska, lyude
On Thu, Feb 12, 2026 at 11:11 AM Boris Brezillon
<boris.brezillon@collabora.com> wrote:
>
> Don't know what the doc rules are in rust, but for this sort of generic
> layer, maybe we should provide extensive docs around objects, fields
> and public functions. I see that most struct fields are documented, but
> not the struct themselves. the enum doesn't seem to be documented, and
> some of the public functions are not either. And that's all my fault,
> because I gave you this raw piece of code without much doc (you added a
> lot already). Just saying that, maybe now that things have settled
> down, is a good time to add proper doc where it's missing.
Yeah, documentation is definitely welcome, and will be rendered for
all crates in the future.
We configure the compiler to require documentation for public items,
and while it is not enforced for private ones, it is definitely wanted
and expected unless unreasonable (e.g. too trivial or onerous).
Examples are also encouraged.
We generally try to be strict within the `kernel` crate, but we don't
want to make things too hard either. It is a balance. Subsystems
should decide on how strict they need/want to be on things like docs
for private items. Some subsystems define further guidelines/rules,
e.g. like Nova:
https://docs.kernel.org/gpu/nova/guidelines.html
A few related notes at (I am writing a "coding guidelines list" to go
along that document with simple, short notes I have been accumulating
over the months, and that may also be useful to give to AI review
tooling):
https://docs.kernel.org/rust/coding-guidelines.html#code-documentation
I hope that helps.
Cheers,
Miguel
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 09/12] drm/tyr: add GPU virtual memory module
2026-02-12 1:37 ` [PATCH 09/12] drm/tyr: add GPU virtual memory module Deborah Brouwer
@ 2026-02-12 10:54 ` Boris Brezillon
2026-02-28 0:52 ` Deborah Brouwer
0 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 10:54 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:10 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Add GPU virtual address space management using the DRM GPUVM framework.
> Each virtual memory (VM) space is backed by ARM64 LPAE Stage 1 page tables
> and can be mapped into hardware address space (AS) slots for GPU execution.
>
> The implementation provides memory isolation and virtual address
> allocation. VMs support mapping GEM buffer objects with configurable
> protection flags (readonly, noexec, uncached) and handle both 4KB and 2MB
> page sizes.
>
> The vm module integrates with the MMU for address space activation and
> provides map/unmap/remap operations with page table synchronization.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Daniel Almeida <daniel.almeida@collabora.com>
> Signed-off-by: Daniel Almeida <daniel.almeida@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/gem.rs | 1 -
> drivers/gpu/drm/tyr/gpu.rs | 1 -
> drivers/gpu/drm/tyr/tyr.rs | 1 +
> drivers/gpu/drm/tyr/vm.rs | 783 +++++++++++++++++++++++++++++++++++++
> 4 files changed, 784 insertions(+), 2 deletions(-)
> create mode 100644 drivers/gpu/drm/tyr/vm.rs
>
> diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> index 6a58f2da88d3..111acf33993f 100644
> --- a/drivers/gpu/drm/tyr/gem.rs
> +++ b/drivers/gpu/drm/tyr/gem.rs
> @@ -48,7 +48,6 @@ fn new<Ctx: DeviceContext>(
> pub(crate) type Bo = gem::shmem::Object<BoData>;
>
> /// Creates a dummy GEM object to serve as the root of a GPUVM.
> -#[expect(dead_code)]
> pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
> let bo = gem::shmem::Object::<BoData>::new(
> ddev,
> diff --git a/drivers/gpu/drm/tyr/gpu.rs b/drivers/gpu/drm/tyr/gpu.rs
> index b5f11bc96fa0..f5e7086ff73c 100644
> --- a/drivers/gpu/drm/tyr/gpu.rs
> +++ b/drivers/gpu/drm/tyr/gpu.rs
> @@ -135,7 +135,6 @@ pub(crate) fn log(&self, pdev: &platform::Device) {
> }
>
> /// Returns the number of virtual address bits supported by the GPU.
> - #[expect(dead_code)]
> pub(crate) fn va_bits(&self) -> u32 {
> self.mmu_features & genmask_u32(0..=7)
> }
> diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> index ae435c7e80b1..8e73db3a080a 100644
> --- a/drivers/gpu/drm/tyr/tyr.rs
> +++ b/drivers/gpu/drm/tyr/tyr.rs
> @@ -14,6 +14,7 @@
> mod mmu;
> mod regs;
> mod slot;
> +mod vm;
>
> kernel::module_platform_driver! {
> type: TyrPlatformDeviceData,
> diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
> new file mode 100644
> index 000000000000..806bc4e587d6
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/vm.rs
> @@ -0,0 +1,783 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +
> +//! GPU virtual memory management using the DRM GPUVM framework.
> +//!
> +//! This module manages GPU virtual address spaces, providing memory isolation and
> +//! the illusion of owning the entire VA range, similar to CPU virtual memory. Each
> +//! VM is backed by ARM64 LPAE Stage 1 page tables and can be mapped into hardware
> +//! address space (AS) slots for GPU execution.
> +#![allow(dead_code)]
> +
> +use core::ops::Range;
> +
> +use kernel::{
> + alloc::KBox,
> + c_str,
> + drm::{
> + gpuvm::{
> + DriverGpuVm,
> + GpuVaAlloc,
> + GpuVm,
> + GpuVmBoRegistered,
> + GpuVmCore,
> + OpMap,
> + OpMapRequest,
> + OpMapped,
> + OpRemap,
> + OpRemapped,
> + OpUnmap,
> + OpUnmapped, //
> + },
> + DeviceContext, //
> + },
> + impl_flags,
> + iommu::pgtable::{
> + prot,
> + Config,
> + IoPageTable,
> + ARM64LPAES1, //
> + },
> + new_mutex,
> + platform,
> + prelude::*,
> + sizes::{
> + SZ_1G,
> + SZ_2M,
> + SZ_4K, //
> + },
> + sync::{
> + aref::ARef,
> + Arc,
> + ArcBorrow,
> + Mutex, //
> + },
> + uapi, //
> +};
> +
> +use crate::{
> + driver::{
> + TyrDrmDevice,
> + TyrDrmDriver, //
> + },
> + gem,
> + gem::Bo,
> + gpu::GpuInfo,
> + mmu::{
> + address_space::*,
> + Mmu, //
> + },
> + regs::*, //
> +};
> +
> +impl_flags!(
> + #[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
> + pub(crate) struct VmMapFlags(u32);
Missing docs. I'll stop pointing those out, and let you check any
undocumented pub struct/enum/field/function in v2. I'm here to help if
you're struggling to find a good description.
> +
> + #[derive(Debug, Clone, Copy, PartialEq, Eq)]
> + pub(crate) enum VmFlag {
> + Readonly = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY as u32,
> + Noexec = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC as u32,
> + Uncached = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED as u32,
> + }
> +);
> +
> +impl VmMapFlags {
> + /// Convert the flags to `pgtable::prot`.
> + fn to_prot(self) -> u32 {
> + let mut prot = 0;
> +
> + if self.contains(VmFlag::Readonly) {
> + prot |= prot::READ;
> + } else {
> + prot |= prot::READ | prot::WRITE;
> + }
> +
> + if self.contains(VmFlag::Noexec) {
> + prot |= prot::NOEXEC;
> + }
> +
> + if !self.contains(VmFlag::Uncached) {
> + prot |= prot::CACHE;
> + }
> +
> + prot
> + }
> +}
> +
> +impl core::fmt::Display for VmMapFlags {
> + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
> + let mut first = true;
> +
> + if self.contains(VmFlag::Readonly) {
> + write!(f, "READONLY")?;
> + first = false;
> + }
> + if self.contains(VmFlag::Noexec) {
> + if !first {
> + write!(f, " | ")?;
> + }
> + write!(f, "NOEXEC")?;
> + first = false;
> + }
> +
> + if self.contains(VmFlag::Uncached) {
> + if !first {
> + write!(f, " | ")?;
> + }
> + write!(f, "UNCACHED")?;
> + }
> +
> + Ok(())
> + }
> +}
> +
> +impl TryFrom<u32> for VmMapFlags {
> + type Error = Error;
> +
> + fn try_from(value: u32) -> core::result::Result<Self, Self::Error> {
> + let valid = (kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY
> + | kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC
> + | kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED)
> + as u32;
> +
> + if value & !valid != 0 {
> + pr_err!("Invalid VM map flags: {:#x}\n", value);
> + return Err(EINVAL);
> + }
> + Ok(Self(value))
> + }
> +}
> +
> +struct VmMapArgs {
> + flags: VmMapFlags,
> + vm_bo: GpuVmBoRegistered<GpuVmData>,
> + bo_offset: u64,
> +}
> +
> +enum VmOpType {
> + Map(VmMapArgs),
> + Unmap,
> +}
> +
> +struct VmOpResources {
> + /// This handles the remap case.
> + ///
> + /// Partial unmap requests or map requests overlapping existing mappings
> + /// will trigger a remap call, which needs to register up to three VA
> + /// objects (one for the new mapping, and two for the previous and next
> + /// mappings).
> + preallocated_gpuvas: [Option<GpuVaAlloc<GpuVmData>>; 3],
> +}
> +
> +struct VmOpRequest {
> + /// Request type.
> + op_type: VmOpType,
> +
> + /// Region of the virtual address space covered by this request.
> + region: Range<u64>,
> +}
> +
> +struct PtMapArgs {
> + /// Flags describing authorized accesses for this mapping.
> + ///
> + /// This is directly derived from the VmMapFlags.
> + prot: u32,
> +}
> +
> +enum PtOpType {
> + Map(PtMapArgs),
> + Unmap,
> +}
> +
> +pub(crate) struct PtUpdateContext<'ctx> {
> + /// Page table.
> + pt: &'ctx IoPageTable<ARM64LPAES1>,
> +
> + /// MMU
> + mmu: &'ctx Mmu,
> +
> + /// Reference to the AS data to pass to the MMU functions
> + as_data: &'ctx VmAsData,
> +
> + /// Region of the virtual address space covered by this request.
> + region: Range<u64>,
> +
> + /// Operation type.
> + op_type: PtOpType,
> +
> + /// Pre-allocated resources that can be used when executing the request.
> + resources: &'ctx mut VmOpResources,
> +}
> +
> +impl<'ctx> PtUpdateContext<'ctx> {
> + fn new(
> + pt: &'ctx IoPageTable<ARM64LPAES1>,
> + mmu: &'ctx Mmu,
> + as_data: &'ctx VmAsData,
> + region: Range<u64>,
> + op_type: PtOpType,
> + resources: &'ctx mut VmOpResources,
> + ) -> Result<PtUpdateContext<'ctx>> {
> + mmu.start_vm_update(as_data, ®ion)?;
> +
> + Ok(Self {
> + pt,
> + mmu,
> + as_data,
> + region,
> + op_type,
> + resources,
> + })
> + }
> +
> + /// Finds one of our pre-allocated VAs.
> + ///
> + /// It is a logic error to call this more than three times for a given
> + /// PtUpdateContext.
> + fn preallocated_gpuva(&mut self) -> Result<GpuVaAlloc<GpuVmData>> {
> + self.resources
> + .preallocated_gpuvas
> + .iter_mut()
> + .find_map(|f| f.take())
> + .ok_or(EINVAL)
> + }
> +}
> +
> +impl Drop for PtUpdateContext<'_> {
> + fn drop(&mut self) {
> + if let Err(e) = self.mmu.end_vm_update(self.as_data) {
> + pr_err!("Failed to end VM update {:?}\n", e);
> + }
> +
> + if let Err(e) = self.mmu.flush_vm(self.as_data) {
> + pr_err!("Failed to flush VM {:?}\n", e);
> + }
> + }
> +}
> +
> +pub(crate) struct GpuVmData {}
> +
> +/// GPU virtual address space.
> +///
> +/// Each VM can be mapped into a hardware address space slot.
> +#[pin_data]
> +pub(crate) struct Vm {
> + /// Data referenced by an AS when the VM is active
> + as_data: Arc<VmAsData>,
> + /// MMU manager.
> + mmu: Arc<Mmu>,
> + /// Platform device reference (needed to access the page table through devres).
> + pdev: ARef<platform::Device>,
> + /// DRM GPUVM core for managing virtual address space.
> + #[pin]
> + gpuvm_core: Mutex<GpuVmCore<GpuVmData>>,
> + /// Non-core part of the GPUVM. Can be used for stuff that doesn't modify the
> + /// internal mapping tree, like GpuVm::obtain()
> + gpuvm: ARef<GpuVm<GpuVmData>>,
> + /// VA range for this VM.
> + va_range: Range<u64>,
> +}
> +
> +impl Vm {
> + pub(crate) fn new<Ctx: DeviceContext>(
> + pdev: &platform::Device,
> + ddev: &TyrDrmDevice<Ctx>,
> + mmu: ArcBorrow<'_, Mmu>,
> + gpu_info: &GpuInfo,
> + ) -> Result<Arc<Vm>> {
> + let va_bits = gpu_info.va_bits();
> + let pa_bits = gpu_info.pa_bits();
> +
> + let pt_config = Config {
> + quirks: 0,
> + pgsize_bitmap: SZ_4K | SZ_2M,
> + ias: va_bits,
> + oas: pa_bits,
> + coherent_walk: false,
> + };
> +
> + // SAFETY: pdev is a bound device.
> + let dev = unsafe { pdev.as_ref().as_bound() };
> + let page_table_init = IoPageTable::new(dev, pt_config);
> + let page_table = KBox::pin_init(page_table_init, GFP_KERNEL).inspect_err(|e| {
> + pr_err!("Failed to initialize page table: {:?}\n", e);
> + })?;
> + let pt = page_table.access(dev).inspect_err(|e| {
> + pr_err!("Failed to access page table: {:?}\n", e);
> + })?;
> +
> + let as_config = AddressSpaceConfig {
> + transcfg: AS_TRANSCFG_PTW_MEMATTR_WB
> + | AS_TRANSCFG_PTW_RA
> + | AS_TRANSCFG_ADRMODE_AARCH64_4K
> + | as_transcfg_ina_bits(u64::from(55 - va_bits)),
> + // SAFETY: Vm::drop() evicts the address space and performs deferred
> + // cleanup before dropping the page_table Arc. This ensures that
> + // the device stops using the page table before it is dropped
> + transtab: unsafe { pt.ttbr() },
> + memattr: mair_to_memattr(pt.mair()),
> + };
> +
> + let range = 0..(1u64 << va_bits);
> + let reserve_range = 0..0u64;
> +
> + let dummy_obj = gem::new_dummy_object(ddev).inspect_err(|e| {
> + pr_err!("Failed to create dummy GEM object: {:?}\n", e);
> + })?;
> +
> + let gpuvm_core = kernel::drm::gpuvm::GpuVm::new::<Error, _>(
> + c_str!("Tyr::GpuVm"),
> + ddev,
> + &*dummy_obj,
> + range.clone(),
> + reserve_range,
> + GpuVmData {},
> + )
> + .inspect_err(|e| {
> + pr_err!("Failed to create GpuVm: {:?}\n", e);
> + })?;
> + let gpuvm = ARef::from(&*gpuvm_core);
> +
> + let as_data = Arc::new(VmAsData::new(&mmu, as_config, page_table), GFP_KERNEL)?;
> +
> + let vm = Arc::pin_init(
> + pin_init!(Self{
> + as_data: as_data,
> + pdev: pdev.into(),
> + mmu: mmu.into(),
> + gpuvm: gpuvm,
> + gpuvm_core <- new_mutex!(gpuvm_core),
> + va_range: range,
> + }),
> + GFP_KERNEL,
> + )?;
> +
> + Ok(vm)
> + }
> +
> + /// Activate the VM in a hardware address space slot.
> + pub(crate) fn activate(&self) -> Result {
> + self.mmu
> + .activate_vm(self.as_data.as_arc_borrow())
> + .inspect_err(|e| {
> + pr_err!("Failed to activate VM: {:?}\n", e);
> + })
> + }
> +
> + /// Deactivate the VM by evicting it from its address space slot.
> + fn deactivate(&self) -> Result {
> + self.mmu.deactivate_vm(&self.as_data).inspect_err(|e| {
> + pr_err!("Failed to deactivate VM: {:?}\n", e);
> + })
> + }
> +
> + pub(crate) fn kill(&self) {
> + // TODO: Turn the VM into a state where it can't be used.
Should we address this TODO before it gets merged? I also believe this
should be done under some lock (the gpuvm lock?) to prevent concurrent
map/unmap operations.
> + let _ = self.deactivate().inspect_err(|e| {
> + pr_err!("Failed to deactivate VM: {:?}\n", e);
> + });
> + let _ = self
> + .unmap_range(self.va_range.start, self.va_range.end - self.va_range.start)
> + .inspect_err(|e| {
> + pr_err!("Failed to unmap range during deactivate: {:?}\n", e);
> + });
> + }
> +
> + fn exec_op(
> + &self,
> + gpuvm_core: &mut GpuVmCore<GpuVmData>,
> + req: VmOpRequest,
> + resources: &mut VmOpResources,
> + ) -> Result {
> + let pt = self
> + .as_data
> + .page_table
> + // SAFETY: pdev is a bound device.
> + .access(unsafe { self.pdev.as_ref().as_bound() })
> + .inspect_err(|e| {
> + pr_err!("Failed to access page table while mapping pages: {:?}\n", e);
> + })?;
> +
> + match req.op_type {
> + VmOpType::Map(args) => {
> + let mut pt_upd = PtUpdateContext::new(
> + pt,
> + &self.mmu,
> + &self.as_data,
> + req.region,
> + PtOpType::Map(PtMapArgs {
> + prot: args.flags.to_prot(),
> + }),
> + resources,
> + )?;
> +
> + gpuvm_core.sm_map(OpMapRequest {
> + addr: pt_upd.region.start,
> + range: pt_upd.region.end - pt_upd.region.start,
> + gem_offset: args.bo_offset,
> + vm_bo: args.vm_bo,
> + context: &mut pt_upd,
> + })
> + //PtUpdateContext drops here flushing the page table
> + }
> + VmOpType::Unmap => {
> + let mut pt_upd = PtUpdateContext::new(
> + pt,
> + &self.mmu,
> + &self.as_data,
> + req.region,
> + PtOpType::Unmap,
> + resources,
> + )?;
> +
> + gpuvm_core.sm_unmap(
> + pt_upd.region.start,
> + pt_upd.region.end - pt_upd.region.start,
> + &mut pt_upd,
> + )
> + //PtUpdateContext drops here flushing the page table
> + }
> + }
> + }
> +
> + /// Map a GEM object range into the VM.
> + pub(crate) fn map_bo_range(
> + &self,
> + bo: &Bo,
> + bo_offset: u64,
> + size: u64,
> + va: u64,
> + flags: VmMapFlags,
> + ) -> Result {
> + let req = VmOpRequest {
> + op_type: VmOpType::Map(VmMapArgs {
> + vm_bo: self.gpuvm.obtain(bo, ())?,
> + flags,
> + bo_offset,
> + }),
> + region: va..(va + size),
> + };
> + let mut resources = VmOpResources {
> + preallocated_gpuvas: [
> + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> + ],
> + };
> + let mut gpuvm_core = self.gpuvm_core.lock();
> +
> + self.exec_op(gpuvm_core.as_mut().get_mut(), req, &mut resources)?;
> +
> + // We flush the defer cleanup list now. Things will be different in
> + // the asynchronous VM_BIND path, where we want the cleanup to
> + // happen outside the DMA signalling path.
> + self.gpuvm.deferred_cleanup();
> + Ok(())
> + }
> +
> + pub(crate) fn unmap_range(&self, va: u64, size: u64) -> Result {
> + let req = VmOpRequest {
> + op_type: VmOpType::Unmap,
> + region: va..(va + size),
> + };
> + let mut resources = VmOpResources {
> + preallocated_gpuvas: [
> + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> + None,
> + ],
> + };
> + let mut gpuvm_core = self.gpuvm_core.lock();
> +
> + self.exec_op(gpuvm_core.as_mut().get_mut(), req, &mut resources)?;
> +
> + // We flush the defer cleanup list now. Things will be different in
> + // the asynchronous VM_BIND path, where we want the cleanup to
> + // happen outside the DMA signalling path.
> + self.gpuvm.deferred_cleanup();
> + Ok(())
> + }
> +}
> +
> +impl DriverGpuVm for GpuVmData {
> + type Driver = TyrDrmDriver;
> + type Object = Bo;
> + type VmBoData = ();
> + type VaData = ();
> + type SmContext<'ctx> = PtUpdateContext<'ctx>;
> +
> + fn sm_step_map<'op>(
> + &mut self,
> + op: OpMap<'op, Self>,
> + context: &mut Self::SmContext<'_>,
> + ) -> Result<OpMapped<'op, Self>, Error> {
> + let start_iova = op.addr();
> + let mut iova = start_iova;
> + let mut bytes_left_to_map = op.length();
> + let mut gem_offset = op.gem_offset();
> + let sgt = op.obj().sg_table().inspect_err(|e| {
> + pr_err!("Failed to get sg_table: {:?}\n", e);
> + })?;
> + let prot = match &context.op_type {
> + PtOpType::Map(args) => args.prot,
> + _ => {
> + return Err(EINVAL);
> + }
> + };
> +
> + for sgt_entry in sgt.iter() {
> + let mut paddr = sgt_entry.dma_address();
> + let mut sgt_entry_length: u64 = sgt_entry.dma_len();
> +
> + if bytes_left_to_map == 0 {
> + break;
> + }
> +
> + if gem_offset > 0 {
> + // Skip the entire SGT entry if the gem_offset exceeds its length
> + let skip = sgt_entry_length.min(gem_offset);
> + paddr += skip;
> + sgt_entry_length -= skip;
> + gem_offset -= skip;
> + }
> +
> + if sgt_entry_length == 0 {
> + continue;
> + }
> +
> + if gem_offset != 0 {
> + pr_err!("Invalid gem_offset {} in page table mapping.\n", gem_offset);
> + return Err(EINVAL);
> + }
> + let len = sgt_entry_length.min(bytes_left_to_map);
> +
> + let segment_mapped = match pt_map(context.pt, iova, paddr, len, prot) {
> + Ok(segment_mapped) => segment_mapped,
> + Err(e) => {
> + // clean up any successful mappings from previous SGT entries.
> + let total_mapped = iova - start_iova;
> + if total_mapped > 0 {
> + pt_unmap(context.pt, start_iova..(start_iova + total_mapped)).ok();
> + }
> + return Err(e);
> + }
> + };
> +
> + // Since there could be a partial mapping, only advance by the actual amount mapped
> + bytes_left_to_map -= segment_mapped;
> + iova += segment_mapped;
> + }
> +
> + let gpuva = context.preallocated_gpuva()?;
> + let op = op.insert(gpuva, pin_init::init_zeroed());
> +
> + Ok(op)
> + }
> +
> + fn sm_step_unmap<'op>(
> + &mut self,
> + op: OpUnmap<'op, Self>,
> + context: &mut Self::SmContext<'_>,
> + ) -> Result<OpUnmapped<'op, Self>, Error> {
> + let start_iova = op.va().addr();
> + let length = op.va().length();
> +
> + let region = start_iova..(start_iova + length);
> + pt_unmap(context.pt, region.clone()).inspect_err(|e| {
> + pr_err!(
> + "Failed to unmap region {:#x}..{:#x}: {:?}\n",
> + region.start,
> + region.end,
> + e
> + );
> + })?;
> +
> + let (op_unmapped, _va_removed) = op.remove();
> +
> + Ok(op_unmapped)
> + }
> +
> + fn sm_step_remap<'op>(
> + &mut self,
> + op: OpRemap<'op, Self>,
> + context: &mut Self::SmContext<'_>,
> + ) -> Result<OpRemapped<'op, Self>, Error> {
> + let unmap_start = if let Some(prev) = op.prev() {
> + prev.addr() + prev.length()
> + } else {
> + op.va_to_unmap().addr()
> + };
> +
> + let unmap_end = if let Some(next) = op.next() {
> + next.addr()
> + } else {
> + op.va_to_unmap().addr() + op.va_to_unmap().length()
> + };
> +
> + let unmap_length = unmap_end - unmap_start;
> +
> + if unmap_length > 0 {
> + let region = unmap_start..(unmap_start + unmap_length);
> + pt_unmap(context.pt, region.clone()).inspect_err(|e| {
> + pr_err!(
> + "Failed to unmap remap region {:#x}..{:#x}: {:?}\n",
> + region.start,
> + region.end,
> + e
> + );
> + })?;
> + }
> +
> + let prev_va = context.preallocated_gpuva()?;
> + let next_va = context.preallocated_gpuva()?;
> +
> + let (op_remapped, _remap_ret) = op.remap(
> + [prev_va, next_va],
> + pin_init::init_zeroed(),
> + pin_init::init_zeroed(),
> + );
> +
> + Ok(op_remapped)
> + }
> +}
> +
> +fn mair_to_memattr(mair: u64) -> u64 {
> + let mut memattr: u64 = 0;
> +
> + for i in 0..8 {
> + let in_attr = (mair >> (8 * i)) as u8;
> + let outer = in_attr >> 4;
> + let inner = in_attr & 0xf;
> +
> + // For caching to be enabled, inner and outer caching policy
> + // have to be both write-back, if one of them is write-through
> + // or non-cacheable, we just choose non-cacheable. Device
> + // memory is also translated to non-cacheable.
> + let out_attr = if (outer & 3 == 0) || (outer & 4 == 0) || (inner & 4 == 0) {
> + AS_MEMATTR_AARCH64_INNER_OUTER_NC
> + | AS_MEMATTR_AARCH64_SH_MIDGARD_INNER
> + | as_memattr_aarch64_inner_alloc_expl(false, false)
> + } else {
> + // Use SH_CPU_INNER mode so SH_IS, which is used when
> + // IOMMU_CACHE is set, actually maps to the standard
> + // definition of inner-shareable and not Mali's
> + // internal-shareable mode.
> + //
> + // TODO: this assumes a non-coherent system.
> + AS_MEMATTR_AARCH64_INNER_OUTER_WB
> + | AS_MEMATTR_AARCH64_SH_MIDGARD_INNER
> + | as_memattr_aarch64_inner_alloc_expl(inner & 1 != 0, inner & 2 != 0)
> + };
> +
> + memattr |= (u64::from(out_attr)) << (8 * i);
> + }
> +
> + memattr
> +}
> +
> +// We can map multiple pages at once but we can't exceed the size of the
> +// table entry itself. So, if mapping 4KB pages, figure out how many pages
> +// can be mapped before we hit the 2MB boundary. Or, if mapping 2MB pages,
> +// figure out how many pages can be mapped before hitting the 1GB boundary
> +// Returns the page size (4KB or 2MB) and the number of pages that can be mapped at that size.
> +fn get_pgsize(addr: u64, size: u64) -> (u64, u64) {
> + // Get the distance to the next boundary of 2MB block
> + let blk_offset_2m = addr.wrapping_neg() % (SZ_2M as u64);
> +
> + // Use 4K blocks if the address is not 2MB aligned, or we have less than 2MB to map
> + if blk_offset_2m != 0 || size < SZ_2M as u64 {
> + let pgcount = if blk_offset_2m == 0 {
> + size / SZ_4K as u64
> + } else {
> + blk_offset_2m.min(size) / SZ_4K as u64
> + };
> + return (SZ_4K as u64, pgcount);
> + }
> +
> + let blk_offset_1g = addr.wrapping_neg() % (SZ_1G as u64);
> + let blk_offset = if blk_offset_1g == 0 {
> + SZ_1G as u64
> + } else {
> + blk_offset_1g
> + };
> + let pgcount = blk_offset.min(size) / SZ_2M as u64;
> +
> + (SZ_2M as u64, pgcount)
> +}
> +
> +fn pt_map(
> + pt: &IoPageTable<ARM64LPAES1>,
> + iova: u64,
> + paddr: u64,
> + len: u64,
> + prot: u32,
> +) -> Result<u64> {
> + let mut segment_mapped = 0u64;
> + while segment_mapped < len {
> + let remaining = len - segment_mapped;
> + let curr_iova = iova + segment_mapped;
> + let curr_paddr = paddr + segment_mapped;
> +
> + let (pgsize, pgcount) = get_pgsize(curr_iova | curr_paddr, remaining);
> +
> + // SAFETY: Exclusive access to the page table is ensured because
> + // the pt reference comes from PtUpdateContext, which was
> + // created while holding &mut Vm, preventing any other access to the
> + // page table for the duration of this operation.
> + let (mapped, result) = unsafe {
> + pt.map_pages(
> + curr_iova as usize,
> + (curr_paddr as usize).try_into().unwrap(),
> + pgsize as usize,
> + pgcount as usize,
> + prot,
> + GFP_KERNEL,
> + )
> + };
> +
> + if let Err(e) = result {
> + pr_err!("pt.map_pages failed at iova {:#x}: {:?}\n", curr_iova, e);
> + if segment_mapped > 0 {
> + pt_unmap(pt, iova..(iova + segment_mapped)).ok();
> + }
> + return Err(e);
> + }
> +
> + if mapped == 0 {
> + pr_err!("Failed to map any pages at iova {:#x}\n", curr_iova);
> + if segment_mapped > 0 {
> + pt_unmap(pt, iova..(iova + segment_mapped)).ok();
> + }
> + return Err(ENOMEM);
> + }
> +
> + segment_mapped += mapped as u64;
> + }
> +
> + Ok(segment_mapped)
> +}
> +
> +fn pt_unmap(pt: &IoPageTable<ARM64LPAES1>, range: Range<u64>) -> Result {
> + let mut iova = range.start;
> + let mut bytes_left_to_unmap = range.end - range.start;
> +
> + while bytes_left_to_unmap > 0 {
> + let (pgsize, pgcount) = get_pgsize(iova, bytes_left_to_unmap);
> +
> + // SAFETY: Exclusive access to the page table is ensured because
> + // the pt reference comes from PtUpdateContext, which was
> + // created while holding &mut Vm, preventing any other access to the
> + // page table for the duration of this operation.
> + let unmapped = unsafe { pt.unmap_pages(iova as usize, pgsize as usize, pgcount as usize) };
> +
> + if unmapped == 0 {
> + pr_err!("Failed to unmap any bytes at iova {:#x}\n", iova);
> + return Err(EINVAL);
> + }
> +
> + bytes_left_to_unmap -= unmapped as u64;
> + iova += unmapped as u64;
> + }
> +
> + Ok(())
> +}
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 10/12] drm/tyr: add a kernel buffer object
2026-02-12 1:37 ` [PATCH 10/12] drm/tyr: add a kernel buffer object Deborah Brouwer
@ 2026-02-12 11:00 ` Boris Brezillon
2026-02-28 1:01 ` Deborah Brouwer
0 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 11:00 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:11 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> Introduce a buffer object type (KernelBo) for internal driver allocations
> that are managed by the kernel rather than userspace.
>
> KernelBo wraps a GEM shmem object and automatically handles GPU virtual
> address space mapping during creation and unmapping on drop. This provides
> a safe and convenient way for the driver to both allocate and clean up
> internal buffers for kernel-managed resources.
>
> Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/gem.rs | 74 +++++++++++++++++++++++++++++++++++---
> 1 file changed, 70 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> index 111acf33993f..3807810be7ea 100644
> --- a/drivers/gpu/drm/tyr/gem.rs
> +++ b/drivers/gpu/drm/tyr/gem.rs
> @@ -4,6 +4,8 @@
> //! This module provides buffer object (BO) management functionality using
> //! DRM's GEM subsystem with shmem backing.
>
> +use core::ops::Range;
> +
> use kernel::{
> drm::{
> gem,
> @@ -11,12 +13,22 @@
> DeviceContext, //
> },
> prelude::*,
> - sync::aref::ARef, //
> + sync::{
> + aref::ARef,
> + Arc,
> + ArcBorrow, //
> + },
> };
>
> -use crate::driver::{
> - TyrDrmDevice,
> - TyrDrmDriver, //
> +use crate::{
> + driver::{
> + TyrDrmDevice,
> + TyrDrmDriver, //
> + },
> + vm::{
> + Vm,
> + VmMapFlags, //
> + },
> };
>
> /// Tyr's DriverObject type for GEM objects.
> @@ -61,3 +73,57 @@ pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) ->
>
> Ok(bo)
> }
> +
> +/// A buffer object that is owned and managed by Tyr rather than userspace.
> +pub(crate) struct KernelBo {
> + #[expect(dead_code)]
> + pub(crate) bo: ARef<Bo>,
> + vm: Arc<Vm>,
> + va_range: Range<u64>,
Missing docs for all those fields.
> +}
> +
> +impl KernelBo {
> + /// Creates a new kernel-owned buffer object and maps it into GPU VA space.
> + #[expect(dead_code)]
> + pub(crate) fn new<Ctx: DeviceContext>(
> + ddev: &TyrDrmDevice<Ctx>,
> + vm: ArcBorrow<'_, Vm>,
> + size: u64,
> + va: u64,
I'm already thinking about the next step (automatic VA-range
allocation), and I'd be tempted to go directly for:
enum KernelBoVaAlloc {
Explicit(u64),
}
so we can easily extend it with
enum KernelBoVaAlloc {
Auto,
Explicit(u64),
}
when we have to.
> + flags: VmMapFlags,
> + ) -> Result<Self> {
> + let bo = gem::shmem::Object::<BoData>::new(
> + ddev,
> + size as usize,
> + shmem::ObjectConfig {
> + map_wc: true,
> + parent_resv_obj: None,
> + },
> + BoCreateArgs { flags: 0 },
> + )?;
> +
> + vm.map_bo_range(&bo, 0, size, va, flags)?;
> +
> + Ok(KernelBo {
> + bo,
> + vm: vm.into(),
> + va_range: va..(va + size),
> + })
> + }
> +}
> +
> +impl Drop for KernelBo {
> + fn drop(&mut self) {
> + let va = self.va_range.start;
> + let size = self.va_range.end - self.va_range.start;
> +
> + if let Err(e) = self.vm.unmap_range(va, size) {
> + pr_err!(
> + "Failed to unmap KernelBo range {:#x}..{:#x}: {:?}\n",
> + self.va_range.start,
> + self.va_range.end,
> + e
> + );
> + }
> + }
> +}
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
2026-02-12 10:44 ` Boris Brezillon
@ 2026-02-12 11:05 ` Boris Brezillon
2026-02-20 15:41 ` Daniel Almeida
2026-02-21 11:17 ` Alice Ryhl
2026-02-20 17:11 ` Daniel Almeida
2026-02-21 11:20 ` Alice Ryhl
3 siblings, 2 replies; 63+ messages in thread
From: Boris Brezillon @ 2026-02-12 11:05 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Wed, 11 Feb 2026 17:37:09 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> +
> +impl Mmu {
> + pub(crate) fn new(
> + pdev: &platform::Device,
> + iomem: ArcBorrow<'_, Devres<IoMem>>,
> + gpu_info: &GpuInfo,
> + ) -> Result<Arc<Mmu>> {
Maybe the Mmu should be wrapped in a Devres, like we do with other HW
components that require the underlying device to be bound to access
registers. I mean, we do have iomem wrapper into a Devres, so maybe
that's not needed, dunno.
> + let slot_count = gpu_info.as_present.count_ones().try_into()?;
> + let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
> + let mmu_init = try_pin_init!(Self{
> + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
> + });
> + Arc::pin_init(mmu_init, GFP_KERNEL)
> + }
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl
2026-02-12 1:37 ` [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl Deborah Brouwer
2026-02-12 8:12 ` Boris Brezillon
@ 2026-02-20 14:03 ` Daniel Almeida
2026-02-21 9:01 ` Alice Ryhl
2 siblings, 0 replies; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 14:03 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
> On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> Currently Tyr disables its clocks from TyrDrmDeviceData::drop(), which
> causes them to be shut down before any other fields in TyrDrmDeviceData
> are dropped. This prevents us from using the clocks when dropping the
> other fields in TyrDrmDeviceData.
>
> In order to better control when the clocks are dropped, move this cleanup
> logic into a Drop implementation on the Clocks struct itself.
>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 23 +++++++++--------------
> 1 file changed, 9 insertions(+), 14 deletions(-)
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index ae4daa12b3e5..9bc6ed56c45e 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -54,7 +54,7 @@ pub(crate) struct TyrPlatformDeviceData {
> _device: ARef<TyrDrmDevice>,
> }
>
> -#[pin_data(PinnedDrop)]
> +#[pin_data]
> pub(crate) struct TyrDrmDeviceData {
> pub(crate) pdev: ARef<platform::Device>,
>
> @@ -168,17 +168,6 @@ impl PinnedDrop for TyrPlatformDeviceData {
> fn drop(self: Pin<&mut Self>) {}
> }
>
> -#[pinned_drop]
> -impl PinnedDrop for TyrDrmDeviceData {
> - fn drop(self: Pin<&mut Self>) {
> - // TODO: the type-state pattern for Clks will fix this.
> - let clks = self.clks.lock();
> - clks.core.disable_unprepare();
> - clks.stacks.disable_unprepare();
> - clks.coregroup.disable_unprepare();
> - }
> -}
> -
> // We need to retain the name "panthor" to achieve drop-in compatibility with
> // the C driver in the userspace stack.
> const INFO: drm::DriverInfo = drm::DriverInfo {
> @@ -202,14 +191,20 @@ impl drm::Driver for TyrDrmDriver {
> }
> }
>
> -#[pin_data]
> struct Clocks {
> core: Clk,
> stacks: OptionalClk,
> coregroup: OptionalClk,
> }
>
> -#[pin_data]
> +impl Drop for Clocks {
> + fn drop(&mut self) {
> + self.core.disable_unprepare();
> + self.stacks.disable_unprepare();
> + self.coregroup.disable_unprepare();
> + }
> +}
> +
> struct Regulators {
> _mali: Regulator<regulator::Enabled>,
> _sram: Regulator<regulator::Enabled>,
> --
> 2.52.0
>
>
Reviewed-by: Daniel Almeida <daniel.almeida@collabora.com>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 03/12] drm/tyr: rename TyrObject to BoData
2026-02-12 1:37 ` [PATCH 03/12] drm/tyr: rename TyrObject to BoData Deborah Brouwer
@ 2026-02-20 14:04 ` Daniel Almeida
2026-02-21 9:01 ` Alice Ryhl
1 sibling, 0 replies; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 14:04 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
> On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Currently the GEM inner driver data object is called `TyrObject` which
> is a fairly generic name. To make the code easier to understand,
> rename `TyrObject` to `BoData` so that the name better reflects its
> role.
>
> No functional change is intended.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 4 ++--
> drivers/gpu/drm/tyr/gem.rs | 6 +++---
> 2 files changed, 5 insertions(+), 5 deletions(-)
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index 9bc6ed56c45e..e5eae5a73371 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -36,7 +36,7 @@
>
> use crate::{
> file::TyrDrmFileData,
> - gem::TyrObject,
> + gem::BoData,
> gpu,
> gpu::GpuInfo,
> regs, //
> @@ -182,7 +182,7 @@ fn drop(self: Pin<&mut Self>) {}
> impl drm::Driver for TyrDrmDriver {
> type Data = TyrDrmDeviceData;
> type File = TyrDrmFileData;
> - type Object<R: drm::DeviceContext> = drm::gem::Object<TyrObject, R>;
> + type Object<R: drm::DeviceContext> = drm::gem::Object<BoData, R>;
>
> const INFO: drm::DriverInfo = INFO;
>
> diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> index c59214e3d0ef..c1208d332dea 100644
> --- a/drivers/gpu/drm/tyr/gem.rs
> +++ b/drivers/gpu/drm/tyr/gem.rs
> @@ -12,9 +12,9 @@
>
> /// GEM Object inner driver data
> #[pin_data]
> -pub(crate) struct TyrObject {}
> +pub(crate) struct BoData {}
>
> -impl gem::DriverObject for TyrObject {
> +impl gem::DriverObject for BoData {
> type Driver = TyrDrmDriver;
> type Args = ();
>
> @@ -23,6 +23,6 @@ fn new<Ctx: DeviceContext>(
> _size: usize,
> _args: (),
> ) -> impl PinInit<Self, Error> {
> - try_pin_init!(TyrObject {})
> + try_pin_init!(BoData {})
> }
> }
> --
> 2.52.0
>
Reviewed-by: Daniel Almeida <daniel.almeida@collabora.com>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address
2026-02-12 1:37 ` [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address Deborah Brouwer
2026-02-12 10:16 ` Boris Brezillon
@ 2026-02-20 14:19 ` Daniel Almeida
2026-02-21 9:03 ` Alice Ryhl
2 siblings, 0 replies; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 14:19 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
> On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> From: Beata Michalska <beata.michalska@arm.com>
>
> Configure the device DMA mask during probe using the GPU's physical
> address capability reported in GpuInfo. This ensures DMA allocations
> use an appropriate address mask.
>
> Signed-off-by: Beata Michalska <beata.michalska@arm.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 11 +++++++++++
> drivers/gpu/drm/tyr/gpu.rs | 1 -
> 2 files changed, 11 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index e5eae5a73371..2973a8b3cc09 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -11,6 +11,10 @@
> Device, //
> },
> devres::Devres,
> + dma::{
> + Device as DmaDevice,
> + DmaMask, //
> + },
> drm,
> drm::{
> driver::Registration,
> @@ -134,6 +138,13 @@ fn probe(
> let gpu_info = GpuInfo::new(pdev.as_ref(), &iomem)?;
> gpu_info.log(pdev);
>
> + // SAFETY: No concurrent DMA allocations or mappings can be made because
> + // the device is still being probed and therefore isn't being used by
> + // other threads of execution.
> + unsafe {
> + pdev.dma_set_mask_and_coherent(DmaMask::try_new(gpu_info.pa_bits())?)?;
> + }
> +
> let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> let platform: ARef<platform::Device> = pdev.into();
>
> diff --git a/drivers/gpu/drm/tyr/gpu.rs b/drivers/gpu/drm/tyr/gpu.rs
> index affca5b0dc6c..b5f11bc96fa0 100644
> --- a/drivers/gpu/drm/tyr/gpu.rs
> +++ b/drivers/gpu/drm/tyr/gpu.rs
> @@ -141,7 +141,6 @@ pub(crate) fn va_bits(&self) -> u32 {
> }
>
> /// Returns the number of physical address bits supported by the GPU.
> - #[expect(dead_code)]
> pub(crate) fn pa_bits(&self) -> u32 {
> (self.mmu_features >> 8) & genmask_u32(0..=7)
> }
> --
> 2.52.0
>
Reviewed-by: Daniel Almeida <daniel.almeida@collabora.com>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-12 1:37 ` [PATCH 05/12] drm/tyr: add MMU address space registers Deborah Brouwer
2026-02-12 8:16 ` Boris Brezillon
@ 2026-02-20 14:21 ` Daniel Almeida
2026-02-21 9:09 ` Alice Ryhl
2 siblings, 0 replies; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 14:21 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
> On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Add register definitions and constants for managing MMU address space,
> including:
> - Address space translation configuration (page table format, attributes)
> - Memory attributes (cacheability, shareability)
> - Address space commands (update, lock, flush)
> - AsRegister helper for per-AS register access
>
> These will be used by the MMU/VM manager to configure page tables and
> control address space operations.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/regs.rs | 101 +++++++++++++++++++++++++++++++++++-
> 1 file changed, 100 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/tyr/regs.rs b/drivers/gpu/drm/tyr/regs.rs
> index 611870c2e6af..9cb7ab0c806a 100644
> --- a/drivers/gpu/drm/tyr/regs.rs
> +++ b/drivers/gpu/drm/tyr/regs.rs
> @@ -8,7 +8,10 @@
> #![allow(dead_code)]
>
> use kernel::{
> - bits::bit_u32,
> + bits::{
> + bit_u32,
> + bit_u64, //
> + },
> device::{
> Bound,
> Device, //
> @@ -111,3 +114,99 @@ pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u3
> pub(crate) const MMU_IRQ_CLEAR: Register<0x2004> = Register;
> pub(crate) const MMU_IRQ_MASK: Register<0x2008> = Register;
> pub(crate) const MMU_IRQ_STAT: Register<0x200c> = Register;
> +
> +pub(crate) const AS_TRANSCFG_ADRMODE_UNMAPPED: u64 = bit_u64(0);
> +pub(crate) const AS_TRANSCFG_ADRMODE_AARCH64_4K: u64 = bit_u64(2) | bit_u64(1);
> +pub(crate) const AS_TRANSCFG_PTW_MEMATTR_WB: u64 = bit_u64(25);
> +pub(crate) const AS_TRANSCFG_PTW_RA: u64 = bit_u64(30);
> +
> +pub(crate) const fn as_transcfg_ina_bits(x: u64) -> u64 {
> + x << 6
> +}
> +
> +pub(crate) const AS_MEMATTR_AARCH64_SH_MIDGARD_INNER: u32 = 0 << 4;
> +pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_NC: u32 = 1 << 6;
> +pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_WB: u32 = 2 << 6;
> +
> +pub(crate) fn as_memattr_aarch64_inner_alloc_expl(w: bool, r: bool) -> u32 {
> + (3 << 2) | (u32::from(w)) | ((u32::from(r)) << 1)
> +}
> +
> +pub(crate) const AS_COMMAND_UPDATE: u32 = 1;
> +pub(crate) const AS_COMMAND_LOCK: u32 = 2;
> +pub(crate) const AS_COMMAND_FLUSH_PT: u32 = 4;
> +pub(crate) const AS_COMMAND_FLUSH_MEM: u32 = 5;
> +
> +pub(crate) const AS_STATUS_ACTIVE: u32 = bit_u32(0);
> +
> +pub(crate) const AS_LOCK_REGION_MIN_SIZE: u32 = bit_u32(15);
> +
> +/// Maximum number of hardware address space slots.
> +/// The actual number of slots available is usually much lower.
> +pub(crate) const MAX_AS_REGISTERS: usize = 32;
> +
> +const MMU_BASE: usize = 0x2400;
> +const MMU_AS_SHIFT: usize = 6;
> +
> +const fn mmu_as(as_nr: usize) -> usize {
> + MMU_BASE + (as_nr << MMU_AS_SHIFT)
> +}
> +
> +pub(crate) struct AsRegister(usize);
> +
> +impl AsRegister {
> + fn new(as_nr: usize, offset: usize) -> Result<Self> {
> + Ok(AsRegister(mmu_as(as_nr) + offset))
> + }
> +
> + #[inline]
> + pub(crate) fn read(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>) -> Result<u32> {
> + let value = (*iomem).access(dev)?.try_read32(self.0)?;
> + Ok(value)
> + }
> +
> + #[inline]
> + pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u32) -> Result {
> + (*iomem).access(dev)?.try_write32(value, self.0)?;
> + Ok(())
> + }
> +}
> +
> +pub(crate) fn as_transtab_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x0)
> +}
> +
> +pub(crate) fn as_transtab_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x4)
> +}
> +
> +pub(crate) fn as_memattr_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x8)
> +}
> +
> +pub(crate) fn as_memattr_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0xc)
> +}
> +
> +pub(crate) fn as_lockaddr_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x10)
> +}
> +
> +pub(crate) fn as_lockaddr_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x14)
> +}
> +
> +pub(crate) fn as_command(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x18)
> +}
> +
> +pub(crate) fn as_status(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x28)
> +}
> +
> +pub(crate) fn as_transcfg_lo(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x30)
> +}
> +pub(crate) fn as_transcfg_hi(as_nr: usize) -> Result<AsRegister> {
> + AsRegister::new(as_nr, 0x34)
> +}
> --
> 2.52.0
>
Reviewed-by: Daniel Almeida <daniel.almeida@collabora.com>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-02-12 1:37 ` [PATCH 06/12] drm/tyr: add shmem backing for GEM objects Deborah Brouwer
2026-02-12 8:17 ` Boris Brezillon
@ 2026-02-20 14:25 ` Daniel Almeida
2026-02-28 0:17 ` Deborah Brouwer
1 sibling, 1 reply; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 14:25 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
> On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> Add support for GEM buffer objects backed by shared memory.
>
> This introduces the BoCreateArgs structure for passing creation parameters
> including flags, and adds a flags field to BoData. A new_dummy_object()
> helper is provided to create a dummy GEM object for use as a GPUVM root.
>
> The Bo type alias is added to simplify working with Tyr's shmem-backed
> GEM objects throughout the driver.
>
> Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
> 1 file changed, 44 insertions(+), 8 deletions(-)
>
> diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> index c1208d332dea..6a58f2da88d3 100644
> --- a/drivers/gpu/drm/tyr/gem.rs
> +++ b/drivers/gpu/drm/tyr/gem.rs
> @@ -1,28 +1,64 @@
> // SPDX-License-Identifier: GPL-2.0 or MIT
> +//! GEM buffer object management for the Tyr driver.
> +//!
> +//! This module provides buffer object (BO) management functionality using
> +//! DRM's GEM subsystem with shmem backing.
>
> use kernel::{
> drm::{
> gem,
> + gem::shmem,
> DeviceContext, //
> },
> - prelude::*, //
> + prelude::*,
> + sync::aref::ARef, //
> };
>
> -use crate::driver::TyrDrmDriver;
> +use crate::driver::{
> + TyrDrmDevice,
> + TyrDrmDriver, //
> +};
>
> -/// GEM Object inner driver data
> +/// Tyr's DriverObject type for GEM objects.
> #[pin_data]
> -pub(crate) struct BoData {}
> +pub(crate) struct BoData {
> + flags: u32,
> +}
> +
> +/// Provides a way to pass arguments when creating BoData
> +/// as required by the gem::DriverObject trait.
> +pub(crate) struct BoCreateArgs {
> + flags: u32,
> +}
>
> impl gem::DriverObject for BoData {
> type Driver = TyrDrmDriver;
> - type Args = ();
> + type Args = BoCreateArgs;
>
> fn new<Ctx: DeviceContext>(
> - _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
> + _dev: &TyrDrmDevice<Ctx>,
Unrelated change?
> _size: usize,
> - _args: (),
> + args: BoCreateArgs,
> ) -> impl PinInit<Self, Error> {
> - try_pin_init!(BoData {})
> + try_pin_init!(Self { flags: args.flags })
> }
> }
> +
> +/// Type alias for Tyr GEM buffer objects.
> +pub(crate) type Bo = gem::shmem::Object<BoData>;
> +
> +/// Creates a dummy GEM object to serve as the root of a GPUVM.
> +#[expect(dead_code)]
> +pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
> + let bo = gem::shmem::Object::<BoData>::new(
> + ddev,
> + 4096,
> + shmem::ObjectConfig {
> + map_wc: true,
> + parent_resv_obj: None,
> + },
> + BoCreateArgs { flags: 0 },
> + )?;
> +
> + Ok(bo)
> +}
> --
> 2.52.0
>
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-12 10:11 ` Boris Brezillon
2026-02-12 10:45 ` Miguel Ojeda
@ 2026-02-20 15:25 ` Daniel Almeida
2026-02-20 16:21 ` Boris Brezillon
2026-02-21 11:16 ` Alice Ryhl
2026-02-28 0:25 ` Deborah Brouwer
3 siblings, 1 reply; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 15:25 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
> On 12 Feb 2026, at 07:11, Boris Brezillon <boris.brezillon@collabora.com> wrote:
>
>
>> +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
>> +
>> +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
>
> Do we really need to explicitly flag this type Unpin? I thought this
> was the default if the struct is not pinned (and it's not AFAICT).
We don’t.
Note that “Unpin” and “not pinned” are distinct concepts. Unpin
means, in more informal terms, “this type does not care if it is
moved”, but it does not preclude you from putting it into Pin<T> or
kernel::sync::Arc<T>. If by “not pinned” you mean no #[pin_data] or
#[pin], then also note that deriving this macro is distinct from "being
pinned".
The relevant rule here is that a type is Unpin if all its fields are Unpin.
This is the case for all non-generic types in SlotManager, so it is Unpin as
long as the generic types T and T::SlotData are also Unpin.
>> +
>> + // Checks and updates the seat state based on the slot it points to
>> + // (if any). Returns a Seat with a value matching the slot state.
>> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
>> + let new_seat = match locked_seat.access(self) {
>> + Seat::Active(seat_info) => {
>> + let old_slot_idx = seat_info.slot as usize;
>> + let slot = &self.slots[old_slot_idx];
>> +
>> + if kernel::warn_on!(
>> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
>> + ) {
>> + Seat::NoSeat
>> + } else {
>> + Seat::Active(SeatInfo {
>> + slot: seat_info.slot,
>> + seqno: seat_info.seqno,
>> + })
>> + }
>> + }
>> +
>> + Seat::Idle(seat_info) => {
>> + let old_slot_idx = seat_info.slot as usize;
>> + let slot = &self.slots[old_slot_idx];
>> +
>> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
>> + Seat::NoSeat
>> + } else {
>> + Seat::Idle(SeatInfo {
>> + slot: seat_info.slot,
>> + seqno: seat_info.seqno,
>> + })
>> + }
>> + }
>> +
>> + _ => Seat::NoSeat,
>> + };
>> +
>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
>> + // so that only slot.rs can change the seat state, but there might be better solutions
>> + // to prevent that.
>
> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
Hm, I'd say we shouldn't implement Clone to avoid any possibility of holding on
to stale state by duplicating it.
Why do we need to return Seat from this function? Can't we simply write
locked_seat in place and not return anything?
>
>> + match &new_seat {
>> + Seat::Active(seat_info) => {
>> + *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
>> + slot: seat_info.slot,
>> + seqno: seat_info.seqno,
>> + })
>> + }
>> + Seat::Idle(seat_info) => {
>> + *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
>> + slot: seat_info.slot,
>> + seqno: seat_info.seqno,
>> + })
>> + }
>> + _ => *locked_seat.access_mut(self) = Seat::NoSeat,
>> + }
>> +
>> + new_seat
>> + }
>> +
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 11:05 ` Boris Brezillon
@ 2026-02-20 15:41 ` Daniel Almeida
2026-02-21 11:17 ` Alice Ryhl
1 sibling, 0 replies; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 15:41 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
> On 12 Feb 2026, at 08:05, Boris Brezillon <boris.brezillon@collabora.com> wrote:
>
> On Wed, 11 Feb 2026 17:37:09 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
>> +
>> +impl Mmu {
>> + pub(crate) fn new(
>> + pdev: &platform::Device,
>> + iomem: ArcBorrow<'_, Devres<IoMem>>,
>> + gpu_info: &GpuInfo,
>> + ) -> Result<Arc<Mmu>> {
>
> Maybe the Mmu should be wrapped in a Devres, like we do with other HW
> components that require the underlying device to be bound to access
> registers. I mean, we do have iomem wrapper into a Devres, so maybe
> that's not needed, dunno.
Isn’t it enough to have the mmio range wrapped in Devres? That’s the
actual device resource that goes away when the device goes away.
>
>> + let slot_count = gpu_info.as_present.count_ones().try_into()?;
>> + let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
>> + let mmu_init = try_pin_init!(Self{
>> + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
>> + });
>> + Arc::pin_init(mmu_init, GFP_KERNEL)
>> + }
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-20 15:25 ` Daniel Almeida
@ 2026-02-20 16:21 ` Boris Brezillon
2026-02-20 16:55 ` Daniel Almeida
0 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-20 16:21 UTC (permalink / raw)
To: Daniel Almeida
Cc: Deborah Brouwer, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
On Fri, 20 Feb 2026 12:25:43 -0300
Daniel Almeida <daniel.almeida@collabora.com> wrote:
> >> +
> >> + // Checks and updates the seat state based on the slot it points to
> >> + // (if any). Returns a Seat with a value matching the slot state.
> >> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> >> + let new_seat = match locked_seat.access(self) {
> >> + Seat::Active(seat_info) => {
> >> + let old_slot_idx = seat_info.slot as usize;
> >> + let slot = &self.slots[old_slot_idx];
> >> +
> >> + if kernel::warn_on!(
> >> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
> >> + ) {
> >> + Seat::NoSeat
> >> + } else {
> >> + Seat::Active(SeatInfo {
> >> + slot: seat_info.slot,
> >> + seqno: seat_info.seqno,
> >> + })
> >> + }
> >> + }
> >> +
> >> + Seat::Idle(seat_info) => {
> >> + let old_slot_idx = seat_info.slot as usize;
> >> + let slot = &self.slots[old_slot_idx];
> >> +
> >> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
> >> + Seat::NoSeat
> >> + } else {
> >> + Seat::Idle(SeatInfo {
> >> + slot: seat_info.slot,
> >> + seqno: seat_info.seqno,
> >> + })
> >> + }
> >> + }
> >> +
> >> + _ => Seat::NoSeat,
> >> + };
> >> +
> >> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> >> + // so that only slot.rs can change the seat state, but there might be better solutions
> >> + // to prevent that.
> >
> > Okay, I guess we want some inputs from Daniel and/or Alice on that one.
>
> Hm, I'd say we shouldn't implement Clone to avoid any possibility of holding on
> to stale state by duplicating it.
Okay, so basically what we have now.
>
> Why do we need to return Seat from this function? Can't we simply write
> locked_seat in place and not return anything?
We do both actually. IIRC, the reason is that LockedBy borrows &self if
we want to read the locked_seat, which prevents us from calling methods
taking a &mut ref from a `match(locked_seat.access())`.
>
> >
> >> + match &new_seat {
> >> + Seat::Active(seat_info) => {
> >> + *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> >> + slot: seat_info.slot,
> >> + seqno: seat_info.seqno,
> >> + })
> >> + }
> >> + Seat::Idle(seat_info) => {
> >> + *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
> >> + slot: seat_info.slot,
> >> + seqno: seat_info.seqno,
> >> + })
> >> + }
> >> + _ => *locked_seat.access_mut(self) = Seat::NoSeat,
> >> + }
> >> +
> >> + new_seat
> >> + }
> >> +
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-20 16:21 ` Boris Brezillon
@ 2026-02-20 16:55 ` Daniel Almeida
2026-02-22 17:57 ` Boris Brezillon
0 siblings, 1 reply; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 16:55 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
> On 20 Feb 2026, at 13:21, Boris Brezillon <boris.brezillon@collabora.com> wrote:
>
> On Fri, 20 Feb 2026 12:25:43 -0300
> Daniel Almeida <daniel.almeida@collabora.com> wrote:
>
>>>> +
>>>> + // Checks and updates the seat state based on the slot it points to
>>>> + // (if any). Returns a Seat with a value matching the slot state.
>>>> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
>>>> + let new_seat = match locked_seat.access(self) {
>>>> + Seat::Active(seat_info) => {
>>>> + let old_slot_idx = seat_info.slot as usize;
>>>> + let slot = &self.slots[old_slot_idx];
>>>> +
>>>> + if kernel::warn_on!(
>>>> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
>>>> + ) {
>>>> + Seat::NoSeat
>>>> + } else {
>>>> + Seat::Active(SeatInfo {
>>>> + slot: seat_info.slot,
>>>> + seqno: seat_info.seqno,
>>>> + })
>>>> + }
>>>> + }
>>>> +
>>>> + Seat::Idle(seat_info) => {
>>>> + let old_slot_idx = seat_info.slot as usize;
>>>> + let slot = &self.slots[old_slot_idx];
>>>> +
>>>> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
>>>> + Seat::NoSeat
>>>> + } else {
>>>> + Seat::Idle(SeatInfo {
>>>> + slot: seat_info.slot,
>>>> + seqno: seat_info.seqno,
>>>> + })
>>>> + }
>>>> + }
>>>> +
>>>> + _ => Seat::NoSeat,
>>>> + };
>>>> +
>>>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
>>>> + // so that only slot.rs can change the seat state, but there might be better solutions
>>>> + // to prevent that.
>>>
>>> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
>>
>> Hm, I'd say we shouldn't implement Clone to avoid any possibility of holding on
>> to stale state by duplicating it.
>
> Okay, so basically what we have now.
>
>>
>> Why do we need to return Seat from this function? Can't we simply write
>> locked_seat in place and not return anything?
>
> We do both actually. IIRC, the reason is that LockedBy borrows &self if
> we want to read the locked_seat, which prevents us from calling methods
> taking a &mut ref from a `match(locked_seat.access())`.
I am referring to this change:
--- a/drivers/gpu/drm/tyr/slot.rs
+++ b/drivers/gpu/drm/tyr/slot.rs
@@ -242,8 +242,8 @@ fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>
}
// Checks and updates the seat state based on the slot it points to
- fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
+ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
let new_seat = match locked_seat.access(self) {
Seat::Active(seat_info) => {
let old_slot_idx = seat_info.slot as usize;
@@ -278,26 +278,7 @@ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
_ => Seat::NoSeat,
};
- // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
- // so that only slot.rs can change the seat state, but there might be better solutions
- // to prevent that.
- match &new_seat {
- Seat::Active(seat_info) => {
- *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
- slot: seat_info.slot,
- seqno: seat_info.seqno,
- })
- }
- Seat::Idle(seat_info) => {
- *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
- slot: seat_info.slot,
- seqno: seat_info.seqno,
- })
- }
- _ => *locked_seat.access_mut(self) = Seat::NoSeat,
- }
-
- new_seat
+ *locked_seat.access_mut(self) = new_seat;
}
Or even shorter:
fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
let (slot_idx, seqno, is_active) = match locked_seat.access(self) {
Seat::Active(info) => (info.slot as usize, info.seqno, true),
Seat::Idle(info) => (info.slot as usize, info.seqno, false),
_ => return,
};
let valid = if is_active {
!kernel::warn_on!(!matches!(&self.slots[slot_idx], Slot::Active(s) if s.seqno == seqno))
} else {
matches!(&self.slots[slot_idx], Slot::Idle(s) if s.seqno == seqno)
};
if !valid {
*locked_seat.access_mut(self) = Seat::NoSeat;
}
}
access vs access_mut() does not matter here: since the owner is &mut self
anyways we know we have exclusive access to the LockedSeat throughout the whole
function.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
2026-02-12 10:44 ` Boris Brezillon
2026-02-12 11:05 ` Boris Brezillon
@ 2026-02-20 17:11 ` Daniel Almeida
2026-02-28 0:46 ` Deborah Brouwer
2026-02-21 11:20 ` Alice Ryhl
3 siblings, 1 reply; 63+ messages in thread
From: Daniel Almeida @ 2026-02-20 17:11 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
> On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> SlotManager for allocating hardware address space slots. The underlying
> AddressSpaceManager performs MMU operations including enabling/disabling
> address spaces, flushing page tables, and locking regions for page table
> updates.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> ---
> drivers/gpu/drm/tyr/driver.rs | 3 +
> drivers/gpu/drm/tyr/mmu.rs | 91 +++++++
> drivers/gpu/drm/tyr/mmu/address_space.rs | 322 +++++++++++++++++++++++
> drivers/gpu/drm/tyr/tyr.rs | 1 +
> 4 files changed, 417 insertions(+)
> create mode 100644 drivers/gpu/drm/tyr/mmu.rs
> create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
>
> diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> index 2973a8b3cc09..ad5a765a6c2a 100644
> --- a/drivers/gpu/drm/tyr/driver.rs
> +++ b/drivers/gpu/drm/tyr/driver.rs
> @@ -43,6 +43,7 @@
> gem::BoData,
> gpu,
> gpu::GpuInfo,
> + mmu::Mmu,
> regs, //
> };
>
> @@ -148,6 +149,8 @@ fn probe(
> let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> let platform: ARef<platform::Device> = pdev.into();
>
> + let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
> +
We need to store this or it will drop.
> let data = try_pin_init!(TyrDrmDeviceData {
> pdev: platform.clone(),
> clks <- new_mutex!(Clocks {
> diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
> new file mode 100644
> index 000000000000..8e076c35f342
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu.rs
> @@ -0,0 +1,91 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +
> +//! Memory Management Unit (MMU) driver for the Tyr GPU.
> +//!
> +//! This module manages GPU address spaces and virtual memory operations through
> +//! hardware MMU slots. It provides functionality for flushing page tables and
> +//! managing VM updates for active address spaces.
> +//!
> +//! The MMU coordinates with the [`AddressSpaceManager`] to handle hardware
> +//! address space allocation and page table operations, using [`SlotManager`]
> +//! to track which address spaces are currently active in hardware slots.
> +//!
> +//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
> +//! [`SlotManager`]: crate::slot::SlotManager
> +#![allow(dead_code)]
> +
> +use core::ops::Range;
> +
> +use kernel::{
> + devres::Devres,
> + new_mutex,
> + platform,
> + prelude::*,
> + sync::{
> + Arc,
> + ArcBorrow,
> + Mutex, //
> + }, //
> +};
> +
> +use crate::{
> + driver::IoMem,
> + gpu::GpuInfo,
> + mmu::address_space::{
> + AddressSpaceManager,
> + VmAsData, //
> + },
> + regs::MAX_AS_REGISTERS,
> + slot::{
> + SlotManager, //
> + }, //
> +};
> +
> +pub(crate) mod address_space;
> +
> +pub(crate) type AsSlotManager = SlotManager<AddressSpaceManager, MAX_AS_REGISTERS>;
> +
> +#[pin_data]
> +pub(crate) struct Mmu {
> + /// Manages the allocation of hardware MMU slots to GPU address spaces.
> + ///
> + /// Tracks which address spaces are currently active in hardware slots and
> + /// coordinates address space operations like flushing and VM updates.
> + #[pin]
> + pub(crate) as_manager: Mutex<AsSlotManager>,
> +}
> +
> +impl Mmu {
> + pub(crate) fn new(
> + pdev: &platform::Device,
> + iomem: ArcBorrow<'_, Devres<IoMem>>,
> + gpu_info: &GpuInfo,
> + ) -> Result<Arc<Mmu>> {
> + let slot_count = gpu_info.as_present.count_ones().try_into()?;
> + let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
> + let mmu_init = try_pin_init!(Self{
> + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
> + });
> + Arc::pin_init(mmu_init, GFP_KERNEL)
> + }
> +
> + pub(crate) fn activate_vm(&self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> + self.as_manager.lock().activate_vm(vm)
> + }
> +
> + pub(crate) fn deactivate_vm(&self, vm: &VmAsData) -> Result {
> + self.as_manager.lock().deactivate_vm(vm)
> + }
> +
> + pub(crate) fn flush_vm(&self, vm: &VmAsData) -> Result {
> + self.as_manager.lock().flush_vm(vm)
> + }
> +
> + pub(crate) fn start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result {
> + self.as_manager.lock().start_vm_update(vm, region)
> + }
Please return a token type from this.
// if you want the whole vm to be locked while this update takes place
struct VmUpdate<‘a> (Guard<‘a, AsSlotManager>);
// if you don’t
struct VmUpdate<‘a> (&’a AsSlotManager);
The lifetime will be correctly elided as the lifetime of &self, i.e.:
start_vm_update<‘a>(&’a self, vm: &VmAsData, region: &Range<u64>) -> Result<VmUpdate<‘a>>
Which is just:
start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result<VmUpdate<‘_>>
> +
> + pub(crate) fn end_vm_update(&self, vm: &VmAsData) -> Result {
> + self.as_manager.lock().end_vm_update(vm)
> + }
Move this to the token type’s Drop implementation.
> +}
> diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
> new file mode 100644
> index 000000000000..60e9a79112f0
> --- /dev/null
> +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
> @@ -0,0 +1,322 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +
> +//! GPU address space management and hardware operations.
> +//!
> +//! This module manages GPU hardware address spaces, including configuration,
> +//! command submission, and page table update regions. It handles the hardware
> +//! interaction for MMU operations through MMIO register access.
> +//!
> +//! The [`AddressSpaceManager`] implements [`SlotOperations`] to integrate with
> +//! the slot management system, enabling and configuring address spaces in the
> +//! hardware slots as needed.
> +//!
> +//! [`SlotOperations`]: crate::slot::SlotOperations
> +
> +use core::ops::Range;
> +
> +use kernel::{
> + bits::*,
> + device::{
> + Bound,
> + Device, //
> + },
> + devres::Devres,
> + error::Result,
> + io,
> + iommu::pgtable::{
> + IoPageTable,
> + ARM64LPAES1, //
> + },
> + platform,
> + prelude::*,
> + sync::{
> + aref::ARef,
> + Arc,
> + ArcBorrow,
> + LockedBy, //
> + },
> + time::Delta, //
> +};
> +
> +use crate::{
> + driver::IoMem,
> + mmu::{
> + AsSlotManager,
> + Mmu, //
> + },
> + regs::*,
> + slot::{
> + Seat,
> + SlotOperations, //
> + }, //
> +};
> +
> +/// Hardware address space configuration registers.
> +///
> +/// Contains the values to be written to the GPU's AS registers when
> +/// activating this address space.
> +#[derive(Clone, Copy)]
> +pub(crate) struct AddressSpaceConfig {
> + pub(crate) transcfg: u64,
> + pub(crate) transtab: u64,
> + pub(crate) memattr: u64,
> +}
Do we need pub(crate) here?
> +
> +/// Any resource/information that will be used by the AddressSpaceManager
> +/// to make a VM active is present in VmAsData.
> +///
> +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> +/// the slot to make sure the page table and the underlying resources
> +/// (pages) used by the AS slot won't go away while the MMU points to
> +/// those.
> +pub(crate) struct VmAsData {
> + /// Tracks this VM's binding to a hardware address space slot.
> + as_seat: LockedBy<Seat, AsSlotManager>,
> + /// Hardware configuration for this address space.
> + as_config: AddressSpaceConfig,
> + /// Page table (managed by devres).
> + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
Do we need pub(crate) here?
> +}
> +
> +impl VmAsData {
> + pub(crate) fn new(
> + mmu: &Mmu,
> + as_config: AddressSpaceConfig,
> + page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> + ) -> VmAsData {
> + Self {
> + as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
> + as_config,
> + page_table,
> + }
> + }
> +}
> +
> +/// Manages GPU hardware address spaces via MMIO register operations.
> +pub(crate) struct AddressSpaceManager {
> + pdev: ARef<platform::Device>,
> + iomem: Arc<Devres<IoMem>>,
> + /// Bitmask of available address space slots from GPU_AS_PRESENT register
> + as_present: u32,
> +}
> +
> +impl SlotOperations for AddressSpaceManager {
> + type SlotData = Arc<VmAsData>;
> +
> + fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) -> Result {
> + self.as_enable(slot_idx, &slot_data.as_config)
> + }
> +
> + fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> + if self.iomem.try_access().is_some() {
> + let _ = self.as_flush(slot_idx);
> + let _ = self.as_disable(slot_idx);
> + }
> + Ok(())
> + }
> +}
> +
> +impl AddressSpaceManager {
> + pub(super) fn new(
> + pdev: &platform::Device,
> + iomem: ArcBorrow<'_, Devres<IoMem>>,
> + as_present: u32,
> + ) -> Result<AddressSpaceManager> {
> + Ok(Self {
> + pdev: pdev.into(),
> + iomem: iomem.into(),
> + as_present,
> + })
> + }
> +
> + fn dev(&self) -> &Device<Bound> {
> + // SAFETY: pdev is a bound device.
I don’t think we can say this for sure? I don’t think there is anything
special about this scope that ensures Device<Bound>.
> + unsafe { self.pdev.as_ref().as_bound() }
> + }
> +
> + fn validate_as_slot(&self, as_nr: usize) -> Result {
> + if as_nr >= MAX_AS_REGISTERS {
> + pr_err!(
> + "AS slot {} out of valid range (max {})\n",
> + as_nr,
> + MAX_AS_REGISTERS
> + );
> + return Err(EINVAL);
> + }
> +
> + if (self.as_present & (1 << as_nr)) == 0 {
> + pr_err!(
> + "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
> + as_nr,
> + self.as_present
> + );
> + return Err(EINVAL);
> + }
> +
> + Ok(())
> + }
> +
> + fn as_wait_ready(&self, as_nr: usize) -> Result {
> + let op = || as_status(as_nr)?.read(self.dev(), &self.iomem);
> + let cond = |status: &u32| -> bool { *status & AS_STATUS_ACTIVE == 0 };
> + let _ =
> + io::poll::read_poll_timeout(op, cond, Delta::from_millis(0), Delta::from_millis(10))?;
> +
> + Ok(())
> + }
> +
> + fn as_send_cmd(&mut self, as_nr: usize, cmd: u32) -> Result {
> + self.as_wait_ready(as_nr)?;
> + as_command(as_nr)?.write(self.dev(), &self.iomem, cmd)?;
> + Ok(())
> + }
> +
> + fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: u32) -> Result {
> + self.as_send_cmd(as_nr, cmd)?;
> + self.as_wait_ready(as_nr)?;
> + Ok(())
> + }
> +
> + fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig) -> Result {
> + self.validate_as_slot(as_nr)?;
> +
> + let transtab = as_config.transtab;
> + let transcfg = as_config.transcfg;
> + let memattr = as_config.memattr;
> +
> + let transtab_lo = (transtab & 0xffffffff) as u32;
> + let transtab_hi = (transtab >> 32) as u32;
> +
> + let transcfg_lo = (transcfg & 0xffffffff) as u32;
> + let transcfg_hi = (transcfg >> 32) as u32;
> +
> + let memattr_lo = (memattr & 0xffffffff) as u32;
> + let memattr_hi = (memattr >> 32) as u32;
> +
> + let dev = self.dev();
> + as_transtab_lo(as_nr)?.write(dev, &self.iomem, transtab_lo)?;
> + as_transtab_hi(as_nr)?.write(dev, &self.iomem, transtab_hi)?;
> +
> + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, transcfg_lo)?;
> + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, transcfg_hi)?;
> +
> + as_memattr_lo(as_nr)?.write(dev, &self.iomem, memattr_lo)?;
> + as_memattr_hi(as_nr)?.write(dev, &self.iomem, memattr_hi)?;
> +
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> +
> + Ok(())
> + }
> +
> + fn as_disable(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> +
> + // Flush AS before disabling
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_MEM)?;
> +
> + let dev = self.dev();
> + as_transtab_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> + as_transtab_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> +
> + as_memattr_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> + as_memattr_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> +
> + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, AS_TRANSCFG_ADRMODE_UNMAPPED as u32)?;
> + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> +
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> +
> + Ok(())
> + }
> +
> + fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
> + self.validate_as_slot(as_nr)?;
> +
> + // The locked region is a naturally aligned power of 2 block encoded as
> + // log2 minus(1).
> + //
> + // Calculate the desired start/end and look for the highest bit which
> + // differs. The smallest naturally aligned block must include this bit
> + // change, the desired region starts with this bit (and subsequent bits)
> + // zeroed and ends with the bit (and subsequent bits) set to one.
> + let region_width = core::cmp::max(
> + 64 - (region.start ^ (region.end - 1)).leading_zeros() as u8,
> + AS_LOCK_REGION_MIN_SIZE.trailing_zeros() as u8,
> + ) - 1;
Is it me, or did this change from the prototype branch?
> +
> + // Mask off the low bits of region.start, which would be ignored by the
> + // hardware anyways.
> + let region_start =
> + region.start & genmask_checked_u64(u32::from(region_width)..=63).ok_or(EINVAL)?;
> +
> + let region = (u64::from(region_width)) | region_start;
> +
> + let region_lo = (region & 0xffffffff) as u32;
> + let region_hi = (region >> 32) as u32;
> +
> + // Lock the region that needs to be updated.
> + let dev = self.dev();
> + as_lockaddr_lo(as_nr)?.write(dev, &self.iomem, region_lo)?;
> + as_lockaddr_hi(as_nr)?.write(dev, &self.iomem, region_hi)?;
> +
> + self.as_send_cmd(as_nr, AS_COMMAND_LOCK)
> + }
> +
> + fn as_end_update(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_PT)
> + }
> +
> + fn as_flush(&mut self, as_nr: usize) -> Result {
> + self.validate_as_slot(as_nr)?;
> + self.as_send_cmd(as_nr, AS_COMMAND_FLUSH_PT)
> + }
> +}
> +
> +impl AsSlotManager {
> + /// Locks a region for page table updates if the VM has an active slot.
> + pub(super) fn start_vm_update(&mut self, vm: &VmAsData, region: &Range<u64>) -> Result {
> + let seat = vm.as_seat.access(self);
> + match seat.slot() {
> + Some(slot) => {
> + let as_nr = slot as usize;
> + self.as_start_update(as_nr, region)
> + }
> + _ => Ok(()),
> + }
> + }
> +
> + /// Flushes page table updates for a VM if it has an active slot.
> + pub(super) fn end_vm_update(&mut self, vm: &VmAsData) -> Result {
> + let seat = vm.as_seat.access(self);
> + match seat.slot() {
> + Some(slot) => {
> + let as_nr = slot as usize;
> + self.as_end_update(as_nr)
> + }
> + _ => Ok(()),
> + }
> + }
> +
> + /// Flushes page tables for a VM if it has an active slot.
> + pub(super) fn flush_vm(&mut self, vm: &VmAsData) -> Result {
> + let seat = vm.as_seat.access(self);
> + match seat.slot() {
> + Some(slot) => {
> + let as_nr = slot as usize;
> + self.as_flush(as_nr)
> + }
> + _ => Ok(()),
> + }
> + }
> +
> + /// Flushes page tables for a VM if it has an active slot.
> + pub(super) fn activate_vm(&mut self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> + self.activate(&vm.as_seat, vm.into())
> + }
> +
> + /// Flushes page tables for a VM if it has an active slot.
> + pub(super) fn deactivate_vm(&mut self, vm: &VmAsData) -> Result {
> + self.evict(&vm.as_seat)
> + }
> +}
> diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> index f54b997355e0..ae435c7e80b1 100644
> --- a/drivers/gpu/drm/tyr/tyr.rs
> +++ b/drivers/gpu/drm/tyr/tyr.rs
> @@ -11,6 +11,7 @@
> mod file;
> mod gem;
> mod gpu;
> +mod mmu;
> mod regs;
> mod slot;
>
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl
2026-02-12 1:37 ` [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl Deborah Brouwer
2026-02-12 8:12 ` Boris Brezillon
2026-02-20 14:03 ` Daniel Almeida
@ 2026-02-21 9:01 ` Alice Ryhl
2 siblings, 0 replies; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 9:01 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Wed, Feb 11, 2026 at 05:37:03PM -0800, Deborah Brouwer wrote:
> Currently Tyr disables its clocks from TyrDrmDeviceData::drop(), which
> causes them to be shut down before any other fields in TyrDrmDeviceData
> are dropped. This prevents us from using the clocks when dropping the
> other fields in TyrDrmDeviceData.
>
> In order to better control when the clocks are dropped, move this cleanup
> logic into a Drop implementation on the Clocks struct itself.
>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
I agree with Boris's point about pinning.
Reviewed-by: Alice Ryhl <aliceryhl@google.com>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 03/12] drm/tyr: rename TyrObject to BoData
2026-02-12 1:37 ` [PATCH 03/12] drm/tyr: rename TyrObject to BoData Deborah Brouwer
2026-02-20 14:04 ` Daniel Almeida
@ 2026-02-21 9:01 ` Alice Ryhl
1 sibling, 0 replies; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 9:01 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Wed, Feb 11, 2026 at 05:37:04PM -0800, Deborah Brouwer wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Currently the GEM inner driver data object is called `TyrObject` which
> is a fairly generic name. To make the code easier to understand,
> rename `TyrObject` to `BoData` so that the name better reflects its
> role.
>
> No functional change is intended.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Reviewed-by: Alice Ryhl <aliceryhl@google.com>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address
2026-02-12 1:37 ` [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address Deborah Brouwer
2026-02-12 10:16 ` Boris Brezillon
2026-02-20 14:19 ` Daniel Almeida
@ 2026-02-21 9:03 ` Alice Ryhl
2 siblings, 0 replies; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 9:03 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Wed, Feb 11, 2026 at 05:37:05PM -0800, Deborah Brouwer wrote:
> From: Beata Michalska <beata.michalska@arm.com>
>
> Configure the device DMA mask during probe using the GPU's physical
> address capability reported in GpuInfo. This ensures DMA allocations
> use an appropriate address mask.
>
> Signed-off-by: Beata Michalska <beata.michalska@arm.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Reviewed-by: Alice Ryhl <aliceryhl@google.com>
> + // SAFETY: No concurrent DMA allocations or mappings can be made because
> + // the device is still being probed and therefore isn't being used by
> + // other threads of execution.
> + unsafe {
> + pdev.dma_set_mask_and_coherent(DmaMask::try_new(gpu_info.pa_bits())?)?;
> + }
Nit: We usually move the semicolon outside of unsafe blocks:
unsafe { pdev.dma_set_mask_and_coherent(DmaMask::try_new(gpu_info.pa_bits())?)? };
Most of the time, this allows rustfmt to format the unsafe block on one
line like the above (but it depends on line length).
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-12 1:37 ` [PATCH 05/12] drm/tyr: add MMU address space registers Deborah Brouwer
2026-02-12 8:16 ` Boris Brezillon
2026-02-20 14:21 ` Daniel Almeida
@ 2026-02-21 9:09 ` Alice Ryhl
2026-02-22 18:13 ` Boris Brezillon
2 siblings, 1 reply; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 9:09 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Wed, Feb 11, 2026 at 05:37:06PM -0800, Deborah Brouwer wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Add register definitions and constants for managing MMU address space,
> including:
> - Address space translation configuration (page table format, attributes)
> - Memory attributes (cacheability, shareability)
> - Address space commands (update, lock, flush)
> - AsRegister helper for per-AS register access
>
> These will be used by the MMU/VM manager to configure page tables and
> control address space operations.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
Reviewed-by: Alice Ryhl <aliceryhl@google.com>
> +/// Maximum number of hardware address space slots.
> +/// The actual number of slots available is usually much lower.
> +pub(crate) const MAX_AS_REGISTERS: usize = 32;
Not necessarily a problem, but this constant is not present in Panthor's
header file.
Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-12 10:11 ` Boris Brezillon
2026-02-12 10:45 ` Miguel Ojeda
2026-02-20 15:25 ` Daniel Almeida
@ 2026-02-21 11:16 ` Alice Ryhl
2026-02-21 12:44 ` Daniel Almeida
2026-02-28 0:25 ` Deborah Brouwer
3 siblings, 1 reply; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 11:16 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, daniel.almeida,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 11:11:13AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:08 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Introduce a generic slot manager to dynamically allocate limited hardware
> > slots to software "seats". It can be used for both address space (AS) and
> > command stream group (CSG) slots.
> >
> > The slot manager initially assigns seats to its free slots. It then
> > continues to reuse the same slot for a seat, as long as another seat
> > did not start to use the slot in the interim.
> >
> > When contention arises because all of the slots are allocated, the slot
> > manager will lazily evict and reuse slots that have become idle (if any).
> >
> > The seat state is protected using the LockedBy pattern with the same lock
> > that guards the SlotManager. This ensures the seat state stays consistent
> > across slot operations.
> >
> > Hardware specific behaviour can be customized using the slot manager's
> > `SlotOperations` trait.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
> > +
> > +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
>
> Do we really need to explicitly flag this type Unpin? I thought this
> was the default if the struct is not pinned (and it's not AFAICT).
It may be cleaner to add `#[pin_data]` to the struct and rely on the
Unpin impl it generates.
In general, the default Unpin implementation is to inherit from the
fields. When you add #[pin_data], it's changed to only inherit from
fields marked #[pin]. By adding #[pin_data] but not marking any fields
#[pin], it will be Unpin unless any of the zero fields marked #[pin] are
Unpin, i.e. it will always be Unpin.
> > + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> > + // so that only slot.rs can change the seat state, but there might be better solutions
> > + // to prevent that.
>
> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
You could consider only implementing Clone. That way, copies do not
happen unless you do so explicitly. Or add a private method on the type
that has the same function as Clone if you do not wish to expose it.
Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 11:05 ` Boris Brezillon
2026-02-20 15:41 ` Daniel Almeida
@ 2026-02-21 11:17 ` Alice Ryhl
1 sibling, 0 replies; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 11:17 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, daniel.almeida,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 12:05:38PM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:09 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > +
> > +impl Mmu {
> > + pub(crate) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + gpu_info: &GpuInfo,
> > + ) -> Result<Arc<Mmu>> {
>
> Maybe the Mmu should be wrapped in a Devres, like we do with other HW
> components that require the underlying device to be bound to access
> registers. I mean, we do have iomem wrapper into a Devres, so maybe
> that's not needed, dunno.
I wouldn't add Devres just for fun. If none of the fields require it,
then I wouldn't bother.
Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
` (2 preceding siblings ...)
2026-02-20 17:11 ` Daniel Almeida
@ 2026-02-21 11:20 ` Alice Ryhl
2026-02-28 0:49 ` Deborah Brouwer
3 siblings, 1 reply; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 11:20 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Wed, Feb 11, 2026 at 05:37:09PM -0800, Deborah Brouwer wrote:
> From: Boris Brezillon <boris.brezillon@collabora.com>
>
> Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> SlotManager for allocating hardware address space slots. The underlying
> AddressSpaceManager performs MMU operations including enabling/disabling
> address spaces, flushing page tables, and locking regions for page table
> updates.
>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> +/// Any resource/information that will be used by the AddressSpaceManager
> +/// to make a VM active is present in VmAsData.
> +///
> +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> +/// the slot to make sure the page table and the underlying resources
> +/// (pages) used by the AS slot won't go away while the MMU points to
> +/// those.
> +pub(crate) struct VmAsData {
> + /// Tracks this VM's binding to a hardware address space slot.
> + as_seat: LockedBy<Seat, AsSlotManager>,
> + /// Hardware configuration for this address space.
> + as_config: AddressSpaceConfig,
> + /// Page table (managed by devres).
> + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
I don't think the Box is needed if you #[pin] this field.
Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support
2026-02-12 1:37 ` [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support Deborah Brouwer
@ 2026-02-21 11:25 ` Alice Ryhl
2026-02-28 1:02 ` Deborah Brouwer
0 siblings, 1 reply; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 11:25 UTC (permalink / raw)
To: Deborah Brouwer
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Wed, Feb 11, 2026 at 05:37:13PM -0800, Deborah Brouwer wrote:
> Add firmware loading and management for the Mali CSF GPU. This introduces
> the fw module that loads the Mali GPU firmware binary, parses it into
> sections, and maps those sections into the MCU VM at the required
> virtual addresses.
>
> On probe, the firmware is loaded, its sections are mapped and populated,
> the MCU VM is activated, and the MCU is booted.
>
> Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> +/// Loaded firmware with sections mapped into MCU VM.
> +pub(crate) struct Firmware {
> + /// Platform device reference (needed to access the MCU JOB_IRQ registers).
> + pdev: ARef<platform::Device>,
> +
> + /// Iomem need to access registers.
> + iomem: Arc<Devres<IoMem>>,
> +
> + /// MCU VM.
> + vm: Arc<Vm>,
> +
> + /// List of firmware sections.
> + #[expect(dead_code)]
> + sections: KVec<KBox<Section>>,
Why the box?
> + let section_start = parsed.data_range.start as usize;
> + let section_end = parsed.data_range.end as usize;
> + let mut data = KVec::new();
> + data.extend_from_slice(&fw.data()[section_start..section_end], GFP_KERNEL)?;
Could this access be out of bounds?
Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-21 11:16 ` Alice Ryhl
@ 2026-02-21 12:44 ` Daniel Almeida
2026-02-21 13:40 ` Alice Ryhl
0 siblings, 1 reply; 63+ messages in thread
From: Daniel Almeida @ 2026-02-21 12:44 UTC (permalink / raw)
To: Alice Ryhl
Cc: Boris Brezillon, Deborah Brouwer, dri-devel, rust-for-linux,
beata.michalska, lyude
> On 21 Feb 2026, at 08:17, Alice Ryhl <aliceryhl@google.com> wrote:
>
> On Thu, Feb 12, 2026 at 11:11:13AM +0100, Boris Brezillon wrote:
>>> On Wed, 11 Feb 2026 17:37:08 -0800
>>> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>>>
>>> From: Boris Brezillon <boris.brezillon@collabora.com>
>>>
>>> Introduce a generic slot manager to dynamically allocate limited hardware
>>> slots to software "seats". It can be used for both address space (AS) and
>>> command stream group (CSG) slots.
>>>
>>> The slot manager initially assigns seats to its free slots. It then
>>> continues to reuse the same slot for a seat, as long as another seat
>>> did not start to use the slot in the interim.
>>>
>>> When contention arises because all of the slots are allocated, the slot
>>> manager will lazily evict and reuse slots that have become idle (if any).
>>>
>>> The seat state is protected using the LockedBy pattern with the same lock
>>> that guards the SlotManager. This ensures the seat state stays consistent
>>> across slot operations.
>>>
>>> Hardware specific behaviour can be customized using the slot manager's
>>> `SlotOperations` trait.
>>>
>>> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
>>> Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
>>> Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
>
>
>>> +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
>>> +
>>> +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
>>
>> Do we really need to explicitly flag this type Unpin? I thought this
>> was the default if the struct is not pinned (and it's not AFAICT).
>
> It may be cleaner to add `#[pin_data]` to the struct and rely on the
> Unpin impl it generates.
>
> In general, the default Unpin implementation is to inherit from the
> fields. When you add #[pin_data], it's changed to only inherit from
> fields marked #[pin]. By adding #[pin_data] but not marking any fields
> #[pin], it will be Unpin unless any of the zero fields marked #[pin] are
> Unpin, i.e. it will always be Unpin.
Why do we need this if all fields are Unpin?
>
>>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
>>> + // so that only slot.rs can change the seat state, but there might be better solutions
>>> + // to prevent that.
>>
>> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
>
> You could consider only implementing Clone. That way, copies do not
> happen unless you do so explicitly. Or add a private method on the type
> that has the same function as Clone if you do not wish to expose it.
>
> Alice
Please check my solution where clone is not needed.
— Daniel
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-21 12:44 ` Daniel Almeida
@ 2026-02-21 13:40 ` Alice Ryhl
2026-02-21 13:48 ` Daniel Almeida
0 siblings, 1 reply; 63+ messages in thread
From: Alice Ryhl @ 2026-02-21 13:40 UTC (permalink / raw)
To: Daniel Almeida
Cc: Boris Brezillon, Deborah Brouwer, dri-devel, rust-for-linux,
beata.michalska, lyude
On Sat, Feb 21, 2026 at 09:44:45AM -0300, Daniel Almeida wrote:
>
>
> > On 21 Feb 2026, at 08:17, Alice Ryhl <aliceryhl@google.com> wrote:
> >
> > On Thu, Feb 12, 2026 at 11:11:13AM +0100, Boris Brezillon wrote:
> >>> +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
> >>> +
> >>> +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
> >>
> >> Do we really need to explicitly flag this type Unpin? I thought this
> >> was the default if the struct is not pinned (and it's not AFAICT).
> >
> > It may be cleaner to add `#[pin_data]` to the struct and rely on the
> > Unpin impl it generates.
> >
> > In general, the default Unpin implementation is to inherit from the
> > fields. When you add #[pin_data], it's changed to only inherit from
> > fields marked #[pin]. By adding #[pin_data] but not marking any fields
> > #[pin], it will be Unpin unless any of the zero fields marked #[pin] are
> > Unpin, i.e. it will always be Unpin.
>
> Why do we need this if all fields are Unpin?
Its fields are not necessarily Unpin. 'manager' could be any type,
including !Unpin types.
Adding #[pin_data] without marking any fields #[pin] is equivalent to
the explicit impl Unpin the patch has now.
> >>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> >>> + // so that only slot.rs can change the seat state, but there might be better solutions
> >>> + // to prevent that.
> >>
> >> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
> >
> > You could consider only implementing Clone. That way, copies do not
> > happen unless you do so explicitly. Or add a private method on the type
> > that has the same function as Clone if you do not wish to expose it.
>
> Please check my solution where clone is not needed.
If you do not need to return it from the function, then that's fine too,
of course.
Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-21 13:40 ` Alice Ryhl
@ 2026-02-21 13:48 ` Daniel Almeida
0 siblings, 0 replies; 63+ messages in thread
From: Daniel Almeida @ 2026-02-21 13:48 UTC (permalink / raw)
To: Alice Ryhl
Cc: Boris Brezillon, Deborah Brouwer, dri-devel, rust-for-linux,
beata.michalska, lyude
> On 21 Feb 2026, at 10:40, Alice Ryhl <aliceryhl@google.com> wrote:
>
> On Sat, Feb 21, 2026 at 09:44:45AM -0300, Daniel Almeida wrote:
>>
>>
>>>> On 21 Feb 2026, at 08:17, Alice Ryhl <aliceryhl@google.com> wrote:
>>>
>>> On Thu, Feb 12, 2026 at 11:11:13AM +0100, Boris Brezillon wrote:
>>>>> +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
>>>>> +
>>>>> +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
>>>>
>>>> Do we really need to explicitly flag this type Unpin? I thought this
>>>> was the default if the struct is not pinned (and it's not AFAICT).
>>>
>>> It may be cleaner to add `#[pin_data]` to the struct and rely on the
>>> Unpin impl it generates.
>>>
>>> In general, the default Unpin implementation is to inherit from the
>>> fields. When you add #[pin_data], it's changed to only inherit from
>>> fields marked #[pin]. By adding #[pin_data] but not marking any fields
>>> #[pin], it will be Unpin unless any of the zero fields marked #[pin] are
>>> Unpin, i.e. it will always be Unpin.
>>
>> Why do we need this if all fields are Unpin?
>
> Its fields are not necessarily Unpin. 'manager' could be any type,
> including !Unpin types.
>
> Adding #[pin_data] without marking any fields #[pin] is equivalent to
> the explicit impl Unpin the patch has now.
If the T or the slot data is !Unpin, then the SlotManager is !Unpin and that’s fine. This has been my point from the beginning.
I don’t see why we need to deviate from this default behavior. This is much clearer than deriving pin_data with 0 #[pin] fields.
>
>>>>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
>>>>> + // so that only slot.rs can change the seat state, but there might be better solutions
>>>>> + // to prevent that.
>>>>
>>>> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
>>>
>>> You could consider only implementing Clone. That way, copies do not
>>> happen unless you do so explicitly. Or add a private method on the type
>>> that has the same function as Clone if you do not wish to expose it.
>>
>> Please check my solution where clone is not needed.
>
> If you do not need to return it from the function, then that's fine too,
> of course.
>
> Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-20 16:55 ` Daniel Almeida
@ 2026-02-22 17:57 ` Boris Brezillon
2026-02-22 18:46 ` Daniel Almeida
0 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-22 17:57 UTC (permalink / raw)
To: Daniel Almeida
Cc: Deborah Brouwer, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
On Fri, 20 Feb 2026 13:55:31 -0300
Daniel Almeida <daniel.almeida@collabora.com> wrote:
> > On 20 Feb 2026, at 13:21, Boris Brezillon <boris.brezillon@collabora.com> wrote:
> >
> > On Fri, 20 Feb 2026 12:25:43 -0300
> > Daniel Almeida <daniel.almeida@collabora.com> wrote:
> >
> >>>> +
> >>>> + // Checks and updates the seat state based on the slot it points to
> >>>> + // (if any). Returns a Seat with a value matching the slot state.
> >>>> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> >>>> + let new_seat = match locked_seat.access(self) {
> >>>> + Seat::Active(seat_info) => {
> >>>> + let old_slot_idx = seat_info.slot as usize;
> >>>> + let slot = &self.slots[old_slot_idx];
> >>>> +
> >>>> + if kernel::warn_on!(
> >>>> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
> >>>> + ) {
> >>>> + Seat::NoSeat
> >>>> + } else {
> >>>> + Seat::Active(SeatInfo {
> >>>> + slot: seat_info.slot,
> >>>> + seqno: seat_info.seqno,
> >>>> + })
> >>>> + }
> >>>> + }
> >>>> +
> >>>> + Seat::Idle(seat_info) => {
> >>>> + let old_slot_idx = seat_info.slot as usize;
> >>>> + let slot = &self.slots[old_slot_idx];
> >>>> +
> >>>> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
> >>>> + Seat::NoSeat
> >>>> + } else {
> >>>> + Seat::Idle(SeatInfo {
> >>>> + slot: seat_info.slot,
> >>>> + seqno: seat_info.seqno,
> >>>> + })
> >>>> + }
> >>>> + }
> >>>> +
> >>>> + _ => Seat::NoSeat,
> >>>> + };
> >>>> +
> >>>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> >>>> + // so that only slot.rs can change the seat state, but there might be better solutions
> >>>> + // to prevent that.
> >>>
> >>> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
> >>
> >> Hm, I'd say we shouldn't implement Clone to avoid any possibility of holding on
> >> to stale state by duplicating it.
> >
> > Okay, so basically what we have now.
> >
> >>
> >> Why do we need to return Seat from this function? Can't we simply write
> >> locked_seat in place and not return anything?
> >
> > We do both actually. IIRC, the reason is that LockedBy borrows &self if
> > we want to read the locked_seat, which prevents us from calling methods
> > taking a &mut ref from a `match(locked_seat.access())`.
>
>
> I am referring to this change:
>
> --- a/drivers/gpu/drm/tyr/slot.rs
> +++ b/drivers/gpu/drm/tyr/slot.rs
> @@ -242,8 +242,8 @@ fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>
> }
>
> // Checks and updates the seat state based on the slot it points to
> - fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
> let new_seat = match locked_seat.access(self) {
> Seat::Active(seat_info) => {
> let old_slot_idx = seat_info.slot as usize;
> @@ -278,26 +278,7 @@ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> _ => Seat::NoSeat,
> };
>
> - // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> - // so that only slot.rs can change the seat state, but there might be better solutions
> - // to prevent that.
> - match &new_seat {
> - Seat::Active(seat_info) => {
> - *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> - slot: seat_info.slot,
> - seqno: seat_info.seqno,
> - })
> - }
> - Seat::Idle(seat_info) => {
> - *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
> - slot: seat_info.slot,
> - seqno: seat_info.seqno,
> - })
> - }
> - _ => *locked_seat.access_mut(self) = Seat::NoSeat,
> - }
> -
> - new_seat
> + *locked_seat.access_mut(self) = new_seat;
That one requires Copy support, or am I missing something?
> }
>
> Or even shorter:
>
> fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
> let (slot_idx, seqno, is_active) = match locked_seat.access(self) {
> Seat::Active(info) => (info.slot as usize, info.seqno, true),
> Seat::Idle(info) => (info.slot as usize, info.seqno, false),
> _ => return,
> };
>
> let valid = if is_active {
> !kernel::warn_on!(!matches!(&self.slots[slot_idx], Slot::Active(s) if s.seqno == seqno))
> } else {
> matches!(&self.slots[slot_idx], Slot::Idle(s) if s.seqno == seqno)
> };
>
> if !valid {
> *locked_seat.access_mut(self) = Seat::NoSeat;
> }
> }
Did you try that? Last I tried, I was hitting a wall because the caller
of check_seat() does a match on the updated seat, and inside this
match, it calls functions that need a &mut self, and the borrow checker
rightfully points the invalid &self then &mut self borrow pattern.
>
> access vs access_mut() does not matter here: since the owner is &mut self
> anyways we know we have exclusive access to the LockedSeat throughout the whole
> function.
I agree, but LockedBy is picky, and last I tried I couldn't make it
work without the annoying update+return-copy-of-seat dance you see
here. Maybe I missed something obvious and it does indeed work with
your suggested changes, dunno.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-21 9:09 ` Alice Ryhl
@ 2026-02-22 18:13 ` Boris Brezillon
2026-02-28 0:13 ` Deborah Brouwer
0 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-02-22 18:13 UTC (permalink / raw)
To: Alice Ryhl
Cc: Deborah Brouwer, dri-devel, rust-for-linux, daniel.almeida,
beata.michalska, lyude
On Sat, 21 Feb 2026 09:09:23 +0000
Alice Ryhl <aliceryhl@google.com> wrote:
> On Wed, Feb 11, 2026 at 05:37:06PM -0800, Deborah Brouwer wrote:
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Add register definitions and constants for managing MMU address space,
> > including:
> > - Address space translation configuration (page table format, attributes)
> > - Memory attributes (cacheability, shareability)
> > - Address space commands (update, lock, flush)
> > - AsRegister helper for per-AS register access
> >
> > These will be used by the MMU/VM manager to configure page tables and
> > control address space operations.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
>
> Reviewed-by: Alice Ryhl <aliceryhl@google.com>
>
> > +/// Maximum number of hardware address space slots.
> > +/// The actual number of slots available is usually much lower.
> > +pub(crate) const MAX_AS_REGISTERS: usize = 32;
>
> Not necessarily a problem, but this constant is not present in Panthor's
> header file.
BTW, I think the max is 16 not 32.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-22 17:57 ` Boris Brezillon
@ 2026-02-22 18:46 ` Daniel Almeida
2026-02-28 0:28 ` Deborah Brouwer
0 siblings, 1 reply; 63+ messages in thread
From: Daniel Almeida @ 2026-02-22 18:46 UTC (permalink / raw)
To: Boris Brezillon
Cc: Deborah Brouwer, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
> On 22 Feb 2026, at 14:57, Boris Brezillon <boris.brezillon@collabora.com> wrote:
>
> On Fri, 20 Feb 2026 13:55:31 -0300
> Daniel Almeida <daniel.almeida@collabora.com> wrote:
>
>>> On 20 Feb 2026, at 13:21, Boris Brezillon <boris.brezillon@collabora.com> wrote:
>>>
>>> On Fri, 20 Feb 2026 12:25:43 -0300
>>> Daniel Almeida <daniel.almeida@collabora.com> wrote:
>>>
>>>>>> +
>>>>>> + // Checks and updates the seat state based on the slot it points to
>>>>>> + // (if any). Returns a Seat with a value matching the slot state.
>>>>>> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
>>>>>> + let new_seat = match locked_seat.access(self) {
>>>>>> + Seat::Active(seat_info) => {
>>>>>> + let old_slot_idx = seat_info.slot as usize;
>>>>>> + let slot = &self.slots[old_slot_idx];
>>>>>> +
>>>>>> + if kernel::warn_on!(
>>>>>> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
>>>>>> + ) {
>>>>>> + Seat::NoSeat
>>>>>> + } else {
>>>>>> + Seat::Active(SeatInfo {
>>>>>> + slot: seat_info.slot,
>>>>>> + seqno: seat_info.seqno,
>>>>>> + })
>>>>>> + }
>>>>>> + }
>>>>>> +
>>>>>> + Seat::Idle(seat_info) => {
>>>>>> + let old_slot_idx = seat_info.slot as usize;
>>>>>> + let slot = &self.slots[old_slot_idx];
>>>>>> +
>>>>>> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
>>>>>> + Seat::NoSeat
>>>>>> + } else {
>>>>>> + Seat::Idle(SeatInfo {
>>>>>> + slot: seat_info.slot,
>>>>>> + seqno: seat_info.seqno,
>>>>>> + })
>>>>>> + }
>>>>>> + }
>>>>>> +
>>>>>> + _ => Seat::NoSeat,
>>>>>> + };
>>>>>> +
>>>>>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
>>>>>> + // so that only slot.rs can change the seat state, but there might be better solutions
>>>>>> + // to prevent that.
>>>>>
>>>>> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
>>>>
>>>> Hm, I'd say we shouldn't implement Clone to avoid any possibility of holding on
>>>> to stale state by duplicating it.
>>>
>>> Okay, so basically what we have now.
>>>
>>>>
>>>> Why do we need to return Seat from this function? Can't we simply write
>>>> locked_seat in place and not return anything?
>>>
>>> We do both actually. IIRC, the reason is that LockedBy borrows &self if
>>> we want to read the locked_seat, which prevents us from calling methods
>>> taking a &mut ref from a `match(locked_seat.access())`.
>>
>>
>> I am referring to this change:
>>
>> --- a/drivers/gpu/drm/tyr/slot.rs
>> +++ b/drivers/gpu/drm/tyr/slot.rs
>> @@ -242,8 +242,8 @@ fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>
>> }
>>
>> // Checks and updates the seat state based on the slot it points to
>> - fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
>> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
>> let new_seat = match locked_seat.access(self) {
>> Seat::Active(seat_info) => {
>> let old_slot_idx = seat_info.slot as usize;
>> @@ -278,26 +278,7 @@ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
>> _ => Seat::NoSeat,
>> };
>>
>> - // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
>> - // so that only slot.rs can change the seat state, but there might be better solutions
>> - // to prevent that.
>> - match &new_seat {
>> - Seat::Active(seat_info) => {
>> - *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
>> - slot: seat_info.slot,
>> - seqno: seat_info.seqno,
>> - })
>> - }
>> - Seat::Idle(seat_info) => {
>> - *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
>> - slot: seat_info.slot,
>> - seqno: seat_info.seqno,
>> - })
>> - }
>> - _ => *locked_seat.access_mut(self) = Seat::NoSeat,
>> - }
>> -
>> - new_seat
>> + *locked_seat.access_mut(self) = new_seat;
>
> That one requires Copy support, or am I missing something?
No. Copy and Clone produce a new value and both the old and new values remain
valid. The line above is moving a value into a new location, invalidating the
previous one. This means that Copy is not required.
>
>> }
>>
>> Or even shorter:
>>
>> fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
>> let (slot_idx, seqno, is_active) = match locked_seat.access(self) {
>> Seat::Active(info) => (info.slot as usize, info.seqno, true),
>> Seat::Idle(info) => (info.slot as usize, info.seqno, false),
>> _ => return,
>> };
>>
>> let valid = if is_active {
>> !kernel::warn_on!(!matches!(&self.slots[slot_idx], Slot::Active(s) if s.seqno == seqno))
>> } else {
>> matches!(&self.slots[slot_idx], Slot::Idle(s) if s.seqno == seqno)
>> };
>>
>> if !valid {
>> *locked_seat.access_mut(self) = Seat::NoSeat;
>> }
>> }
>
> Did you try that? Last I tried, I was hitting a wall because the caller
> of check_seat() does a match on the updated seat, and inside this
> match, it calls functions that need a &mut self, and the borrow checker
> rightfully points the invalid &self then &mut self borrow pattern.
Yes, I compiled-tested all changes I suggested. My changes compile because they
intentionally avoid doing what you said above.
The key here is that your borrows do not overlap anymore. The code I showed
first borrows immutably, and then returns this tuple: (slot_idx, seqno, is_active).
The immutable borrow then ends, since this tuple is copied (not borrowed) from
its source. This benefits from the fact that primitive types are Copy.
Note that you can borrow both mutably and immutably in the same scope just fine. The
borrows just can’t be alive at the same time. When the borrow checker rejects your code,
it shows you why incompatible borrows overlap (i.e.: check the “first used here…later
used here” part of the error).
>
>>
>> access vs access_mut() does not matter here: since the owner is &mut self
>> anyways we know we have exclusive access to the LockedSeat throughout the whole
>> function.
>
> I agree, but LockedBy is picky, and last I tried I couldn't make it
> work without the annoying update+return-copy-of-seat dance you see
> here. Maybe I missed something obvious and it does indeed work with
> your suggested changes, dunno.
Rewriting things so they pass the borrow checker is common in Rust. Sometimes
it can be done rather easily; other times the design is just broken and needs
to be reworked. Luckily this one fell in the first category.
This benefits from the fact that no one can race us between reading this tuple
(slot_idx, seqno, is_active)
..and using it. That’s because we’re taking &mut self as a proxy in LockedBy, so
we’re sure we have exclusive access in this scope.
If you don’t have any complaints about the code I sent (i.e.: convoluted, wrong
logic, etc), I suggest switching to it.
— Daniel
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-12 8:16 ` Boris Brezillon
@ 2026-02-28 0:12 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:12 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 09:16:24AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:06 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > From: Boris Brezillon <boris.brezillon@collabora.com>
>
> I'm pretty sure I got that from Daniel's branch, and only tweaked a few
> minor things to make it work (I probably messed up authorship when
> doing that). I'd prefer to attribute that work to Daniel, if you don't
> mind.
Sure, sorry I didn't clarify that, will change the author in v2.
>
> >
> > Add register definitions and constants for managing MMU address space,
> > including:
> > - Address space translation configuration (page table format, attributes)
> > - Memory attributes (cacheability, shareability)
> > - Address space commands (update, lock, flush)
> > - AsRegister helper for per-AS register access
> >
> > These will be used by the MMU/VM manager to configure page tables and
> > control address space operations.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/regs.rs | 101 +++++++++++++++++++++++++++++++++++-
> > 1 file changed, 100 insertions(+), 1 deletion(-)
> >
> > diff --git a/drivers/gpu/drm/tyr/regs.rs b/drivers/gpu/drm/tyr/regs.rs
> > index 611870c2e6af..9cb7ab0c806a 100644
> > --- a/drivers/gpu/drm/tyr/regs.rs
> > +++ b/drivers/gpu/drm/tyr/regs.rs
> > @@ -8,7 +8,10 @@
> > #![allow(dead_code)]
> >
> > use kernel::{
> > - bits::bit_u32,
> > + bits::{
> > + bit_u32,
> > + bit_u64, //
> > + },
> > device::{
> > Bound,
> > Device, //
> > @@ -111,3 +114,99 @@ pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u3
> > pub(crate) const MMU_IRQ_CLEAR: Register<0x2004> = Register;
> > pub(crate) const MMU_IRQ_MASK: Register<0x2008> = Register;
> > pub(crate) const MMU_IRQ_STAT: Register<0x200c> = Register;
> > +
> > +pub(crate) const AS_TRANSCFG_ADRMODE_UNMAPPED: u64 = bit_u64(0);
> > +pub(crate) const AS_TRANSCFG_ADRMODE_AARCH64_4K: u64 = bit_u64(2) | bit_u64(1);
> > +pub(crate) const AS_TRANSCFG_PTW_MEMATTR_WB: u64 = bit_u64(25);
> > +pub(crate) const AS_TRANSCFG_PTW_RA: u64 = bit_u64(30);
> > +
> > +pub(crate) const fn as_transcfg_ina_bits(x: u64) -> u64 {
> > + x << 6
> > +}
> > +
> > +pub(crate) const AS_MEMATTR_AARCH64_SH_MIDGARD_INNER: u32 = 0 << 4;
> > +pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_NC: u32 = 1 << 6;
> > +pub(crate) const AS_MEMATTR_AARCH64_INNER_OUTER_WB: u32 = 2 << 6;
> > +
> > +pub(crate) fn as_memattr_aarch64_inner_alloc_expl(w: bool, r: bool) -> u32 {
> > + (3 << 2) | (u32::from(w)) | ((u32::from(r)) << 1)
> > +}
> > +
> > +pub(crate) const AS_COMMAND_UPDATE: u32 = 1;
> > +pub(crate) const AS_COMMAND_LOCK: u32 = 2;
> > +pub(crate) const AS_COMMAND_FLUSH_PT: u32 = 4;
> > +pub(crate) const AS_COMMAND_FLUSH_MEM: u32 = 5;
> > +
> > +pub(crate) const AS_STATUS_ACTIVE: u32 = bit_u32(0);
> > +
> > +pub(crate) const AS_LOCK_REGION_MIN_SIZE: u32 = bit_u32(15);
> > +
> > +/// Maximum number of hardware address space slots.
> > +/// The actual number of slots available is usually much lower.
> > +pub(crate) const MAX_AS_REGISTERS: usize = 32;
> > +
> > +const MMU_BASE: usize = 0x2400;
> > +const MMU_AS_SHIFT: usize = 6;
> > +
> > +const fn mmu_as(as_nr: usize) -> usize {
> > + MMU_BASE + (as_nr << MMU_AS_SHIFT)
> > +}
> > +
> > +pub(crate) struct AsRegister(usize);
> > +
> > +impl AsRegister {
> > + fn new(as_nr: usize, offset: usize) -> Result<Self> {
> > + Ok(AsRegister(mmu_as(as_nr) + offset))
> > + }
> > +
> > + #[inline]
> > + pub(crate) fn read(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>) -> Result<u32> {
> > + let value = (*iomem).access(dev)?.try_read32(self.0)?;
> > + Ok(value)
> > + }
> > +
> > + #[inline]
> > + pub(crate) fn write(&self, dev: &Device<Bound>, iomem: &Devres<IoMem>, value: u32) -> Result {
> > + (*iomem).access(dev)?.try_write32(value, self.0)?;
> > + Ok(())
> > + }
> > +}
> > +
> > +pub(crate) fn as_transtab_lo(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x0)
> > +}
> > +
> > +pub(crate) fn as_transtab_hi(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x4)
> > +}
> > +
> > +pub(crate) fn as_memattr_lo(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x8)
> > +}
> > +
> > +pub(crate) fn as_memattr_hi(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0xc)
> > +}
> > +
> > +pub(crate) fn as_lockaddr_lo(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x10)
> > +}
> > +
> > +pub(crate) fn as_lockaddr_hi(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x14)
> > +}
> > +
> > +pub(crate) fn as_command(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x18)
> > +}
> > +
> > +pub(crate) fn as_status(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x28)
> > +}
> > +
> > +pub(crate) fn as_transcfg_lo(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x30)
> > +}
> > +pub(crate) fn as_transcfg_hi(as_nr: usize) -> Result<AsRegister> {
> > + AsRegister::new(as_nr, 0x34)
> > +}
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 05/12] drm/tyr: add MMU address space registers
2026-02-22 18:13 ` Boris Brezillon
@ 2026-02-28 0:13 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:13 UTC (permalink / raw)
To: Boris Brezillon
Cc: Alice Ryhl, dri-devel, rust-for-linux, daniel.almeida,
beata.michalska, lyude
On Sun, Feb 22, 2026 at 07:13:18PM +0100, Boris Brezillon wrote:
> On Sat, 21 Feb 2026 09:09:23 +0000
> Alice Ryhl <aliceryhl@google.com> wrote:
>
> > On Wed, Feb 11, 2026 at 05:37:06PM -0800, Deborah Brouwer wrote:
> > > From: Boris Brezillon <boris.brezillon@collabora.com>
> > >
> > > Add register definitions and constants for managing MMU address space,
> > > including:
> > > - Address space translation configuration (page table format, attributes)
> > > - Memory attributes (cacheability, shareability)
> > > - Address space commands (update, lock, flush)
> > > - AsRegister helper for per-AS register access
> > >
> > > These will be used by the MMU/VM manager to configure page tables and
> > > control address space operations.
> > >
> > > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> >
> > Reviewed-by: Alice Ryhl <aliceryhl@google.com>
> >
> > > +/// Maximum number of hardware address space slots.
> > > +/// The actual number of slots available is usually much lower.
> > > +pub(crate) const MAX_AS_REGISTERS: usize = 32;
> >
> > Not necessarily a problem, but this constant is not present in Panthor's
> > header file.
>
> BTW, I think the max is 16 not 32.
Yeah, I double checked the architectural limit in the spec and it says the
Maximum number of address spaces is 16, so I will change this in v2.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-02-12 8:17 ` Boris Brezillon
@ 2026-02-28 0:15 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:15 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 09:17:34AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:07 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > Add support for GEM buffer objects backed by shared memory.
> >
> > This introduces the BoCreateArgs structure for passing creation parameters
> > including flags, and adds a flags field to BoData. A new_dummy_object()
> > helper is provided to create a dummy GEM object for use as a GPUVM root.
> >
> > The Bo type alias is added to simplify working with Tyr's shmem-backed
> > GEM objects throughout the driver.
> >
> > Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
> > 1 file changed, 44 insertions(+), 8 deletions(-)
> >
> > diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> > index c1208d332dea..6a58f2da88d3 100644
> > --- a/drivers/gpu/drm/tyr/gem.rs
> > +++ b/drivers/gpu/drm/tyr/gem.rs
> > @@ -1,28 +1,64 @@
> > // SPDX-License-Identifier: GPL-2.0 or MIT
> > +//! GEM buffer object management for the Tyr driver.
> > +//!
> > +//! This module provides buffer object (BO) management functionality using
> > +//! DRM's GEM subsystem with shmem backing.
> >
> > use kernel::{
> > drm::{
> > gem,
> > + gem::shmem,
> > DeviceContext, //
> > },
> > - prelude::*, //
> > + prelude::*,
> > + sync::aref::ARef, //
> > };
> >
> > -use crate::driver::TyrDrmDriver;
> > +use crate::driver::{
> > + TyrDrmDevice,
> > + TyrDrmDriver, //
> > +};
> >
> > -/// GEM Object inner driver data
> > +/// Tyr's DriverObject type for GEM objects.
> > #[pin_data]
> > -pub(crate) struct BoData {}
> > +pub(crate) struct BoData {
> > + flags: u32,
> > +}
> > +
> > +/// Provides a way to pass arguments when creating BoData
> > +/// as required by the gem::DriverObject trait.
> > +pub(crate) struct BoCreateArgs {
> > + flags: u32,
> > +}
> >
> > impl gem::DriverObject for BoData {
> > type Driver = TyrDrmDriver;
> > - type Args = ();
> > + type Args = BoCreateArgs;
> >
> > fn new<Ctx: DeviceContext>(
> > - _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
> > + _dev: &TyrDrmDevice<Ctx>,
> > _size: usize,
> > - _args: (),
> > + args: BoCreateArgs,
> > ) -> impl PinInit<Self, Error> {
> > - try_pin_init!(BoData {})
> > + try_pin_init!(Self { flags: args.flags })
> > }
> > }
> > +
> > +/// Type alias for Tyr GEM buffer objects.
> > +pub(crate) type Bo = gem::shmem::Object<BoData>;
> > +
> > +/// Creates a dummy GEM object to serve as the root of a GPUVM.
> > +#[expect(dead_code)]
> > +pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
> > + let bo = gem::shmem::Object::<BoData>::new(
> > + ddev,
> > + 4096,
> > + shmem::ObjectConfig {
> > + map_wc: true,
> > + parent_resv_obj: None,
> > + },
> > + BoCreateArgs { flags: 0 },
> > + )?;
> > +
> > + Ok(bo)
> > +}
>
> Nit: I'd probably move this new_dummy_object() addition to the commit
> introducing Vm support.
Ok that works too.
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-02-20 14:25 ` Daniel Almeida
@ 2026-02-28 0:17 ` Deborah Brouwer
2026-03-02 10:17 ` Boris Brezillon
0 siblings, 1 reply; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:17 UTC (permalink / raw)
To: Daniel Almeida
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
On Fri, Feb 20, 2026 at 11:25:47AM -0300, Daniel Almeida wrote:
>
>
> > On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> >
> > Add support for GEM buffer objects backed by shared memory.
> >
> > This introduces the BoCreateArgs structure for passing creation parameters
> > including flags, and adds a flags field to BoData. A new_dummy_object()
> > helper is provided to create a dummy GEM object for use as a GPUVM root.
> >
> > The Bo type alias is added to simplify working with Tyr's shmem-backed
> > GEM objects throughout the driver.
> >
> > Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
> > 1 file changed, 44 insertions(+), 8 deletions(-)
> >
> > diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> > index c1208d332dea..6a58f2da88d3 100644
> > --- a/drivers/gpu/drm/tyr/gem.rs
> > +++ b/drivers/gpu/drm/tyr/gem.rs
> > @@ -1,28 +1,64 @@
> > // SPDX-License-Identifier: GPL-2.0 or MIT
> > +//! GEM buffer object management for the Tyr driver.
> > +//!
> > +//! This module provides buffer object (BO) management functionality using
> > +//! DRM's GEM subsystem with shmem backing.
> >
> > use kernel::{
> > drm::{
> > gem,
> > + gem::shmem,
> > DeviceContext, //
> > },
> > - prelude::*, //
> > + prelude::*,
> > + sync::aref::ARef, //
> > };
> >
> > -use crate::driver::TyrDrmDriver;
> > +use crate::driver::{
> > + TyrDrmDevice,
> > + TyrDrmDriver, //
> > +};
> >
> > -/// GEM Object inner driver data
> > +/// Tyr's DriverObject type for GEM objects.
> > #[pin_data]
> > -pub(crate) struct BoData {}
> > +pub(crate) struct BoData {
> > + flags: u32,
> > +}
> > +
> > +/// Provides a way to pass arguments when creating BoData
> > +/// as required by the gem::DriverObject trait.
> > +pub(crate) struct BoCreateArgs {
> > + flags: u32,
> > +}
> >
> > impl gem::DriverObject for BoData {
> > type Driver = TyrDrmDriver;
> > - type Args = ();
> > + type Args = BoCreateArgs;
> >
> > fn new<Ctx: DeviceContext>(
> > - _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
> > + _dev: &TyrDrmDevice<Ctx>,
>
> Unrelated change?
I switched to use the convenience type alias `TyrDrmDevice<Ctx>`
here instead of using its full path. I can flag that in the commit
mesage if that is what you mean?
>
> > _size: usize,
> > - _args: (),
> > + args: BoCreateArgs,
> > ) -> impl PinInit<Self, Error> {
> > - try_pin_init!(BoData {})
> > + try_pin_init!(Self { flags: args.flags })
> > }
> > }
> > +
> > +/// Type alias for Tyr GEM buffer objects.
> > +pub(crate) type Bo = gem::shmem::Object<BoData>;
> > +
> > +/// Creates a dummy GEM object to serve as the root of a GPUVM.
> > +#[expect(dead_code)]
> > +pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
> > + let bo = gem::shmem::Object::<BoData>::new(
> > + ddev,
> > + 4096,
> > + shmem::ObjectConfig {
> > + map_wc: true,
> > + parent_resv_obj: None,
> > + },
> > + BoCreateArgs { flags: 0 },
> > + )?;
> > +
> > + Ok(bo)
> > +}
> > --
> > 2.52.0
> >
> >
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl
2026-02-12 8:12 ` Boris Brezillon
@ 2026-02-28 0:18 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:18 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 09:12:55AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:03 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > Currently Tyr disables its clocks from TyrDrmDeviceData::drop(), which
> > causes them to be shut down before any other fields in TyrDrmDeviceData
> > are dropped. This prevents us from using the clocks when dropping the
> > other fields in TyrDrmDeviceData.
> >
> > In order to better control when the clocks are dropped, move this cleanup
> > logic into a Drop implementation on the Clocks struct itself.
> >
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
>
> Maybe you should mention that Clocks is no longer considered pinned,
> because it's not needed in practice.
Ack will do.
>
> Reviewed-by: Boris Brezillon <boris.brezillon@collabora.com>
>
> > ---
> > drivers/gpu/drm/tyr/driver.rs | 23 +++++++++--------------
> > 1 file changed, 9 insertions(+), 14 deletions(-)
> >
> > diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> > index ae4daa12b3e5..9bc6ed56c45e 100644
> > --- a/drivers/gpu/drm/tyr/driver.rs
> > +++ b/drivers/gpu/drm/tyr/driver.rs
> > @@ -54,7 +54,7 @@ pub(crate) struct TyrPlatformDeviceData {
> > _device: ARef<TyrDrmDevice>,
> > }
> >
> > -#[pin_data(PinnedDrop)]
> > +#[pin_data]
> > pub(crate) struct TyrDrmDeviceData {
> > pub(crate) pdev: ARef<platform::Device>,
> >
> > @@ -168,17 +168,6 @@ impl PinnedDrop for TyrPlatformDeviceData {
> > fn drop(self: Pin<&mut Self>) {}
> > }
> >
> > -#[pinned_drop]
> > -impl PinnedDrop for TyrDrmDeviceData {
> > - fn drop(self: Pin<&mut Self>) {
> > - // TODO: the type-state pattern for Clks will fix this.
> > - let clks = self.clks.lock();
> > - clks.core.disable_unprepare();
> > - clks.stacks.disable_unprepare();
> > - clks.coregroup.disable_unprepare();
> > - }
> > -}
> > -
> > // We need to retain the name "panthor" to achieve drop-in compatibility with
> > // the C driver in the userspace stack.
> > const INFO: drm::DriverInfo = drm::DriverInfo {
> > @@ -202,14 +191,20 @@ impl drm::Driver for TyrDrmDriver {
> > }
> > }
> >
> > -#[pin_data]
> > struct Clocks {
> > core: Clk,
> > stacks: OptionalClk,
> > coregroup: OptionalClk,
> > }
> >
> > -#[pin_data]
> > +impl Drop for Clocks {
> > + fn drop(&mut self) {
> > + self.core.disable_unprepare();
> > + self.stacks.disable_unprepare();
> > + self.coregroup.disable_unprepare();
> > + }
> > +}
> > +
> > struct Regulators {
> > _mali: Regulator<regulator::Enabled>,
> > _sram: Regulator<regulator::Enabled>,
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-12 10:11 ` Boris Brezillon
` (2 preceding siblings ...)
2026-02-21 11:16 ` Alice Ryhl
@ 2026-02-28 0:25 ` Deborah Brouwer
3 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:25 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 11:11:13AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:08 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Introduce a generic slot manager to dynamically allocate limited hardware
> > slots to software "seats". It can be used for both address space (AS) and
> > command stream group (CSG) slots.
> >
> > The slot manager initially assigns seats to its free slots. It then
> > continues to reuse the same slot for a seat, as long as another seat
> > did not start to use the slot in the interim.
> >
> > When contention arises because all of the slots are allocated, the slot
> > manager will lazily evict and reuse slots that have become idle (if any).
> >
> > The seat state is protected using the LockedBy pattern with the same lock
> > that guards the SlotManager. This ensures the seat state stays consistent
> > across slot operations.
> >
> > Hardware specific behaviour can be customized using the slot manager's
> > `SlotOperations` trait.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/slot.rs | 359 ++++++++++++++++++++++++++++++++++++
> > drivers/gpu/drm/tyr/tyr.rs | 1 +
> > 2 files changed, 360 insertions(+)
> > create mode 100644 drivers/gpu/drm/tyr/slot.rs
> >
> > diff --git a/drivers/gpu/drm/tyr/slot.rs b/drivers/gpu/drm/tyr/slot.rs
> > new file mode 100644
> > index 000000000000..37bf8800a225
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/slot.rs
> > @@ -0,0 +1,359 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! Slot management abstraction for limited hardware resources.
> > +//!
> > +//! This module provides a generic [`SlotManager`] that assigns limited hardware
> > +//! slots to logical "seats". A seat represents an entity (such as a vm address
> > +//! space) that needs access to a hardware slot.
> > +//!
> > +//! The [`SlotManager`] tracks slot allocation using sequence numbers to detect
> > +//! when a seat's binding has been invalidated. When a seat requests activation,
> > +//! the manager will either reuse the seat's existing slot (if still valid),
> > +//! allocate a free slot (if any are available), or evict the oldest idle slot if any
> > +//! slots are idle.
> > +//!
> > +//! Hardware-specific behavior is customized by implementing the [`SlotOperations`]
> > +//! trait, which allows callbacks when slots are activated or evicted.
> > +//!
> > +//! This is primarily used for managing address space slots in the GPU, where
> > +//! the number of hardware address space slots is limited.
>
> I'd probably mention that we intend to use it for other stuff (Csg
> slots), hence the generalization done here.
Ack.
>
> > +//!
> > +//! [SlotOperations]: crate::slot::SlotOperations
> > +//! [SlotManager]: crate::slot::SlotManager
>
> Thanks a lot for adding some docs to my barebone initial implementation
> and fixing the stuff I got wrong along the way. :D
>
> > +#![allow(dead_code)]
> > +
> > +use core::{
> > + mem::take,
> > + ops::{
> > + Deref,
> > + DerefMut, //
> > + }, //
> > +};
> > +
> > +use kernel::{
> > + prelude::*,
> > + sync::LockedBy, //
> > +};
> > +
>
> Don't know what the doc rules are in rust, but for this sort of generic
> layer, maybe we should provide extensive docs around objects, fields
> and public functions. I see that most struct fields are documented, but
> not the struct themselves. the enum doesn't seem to be documented, and
> some of the public functions are not either. And that's all my fault,
> because I gave you this raw piece of code without much doc (you added a
> lot already). Just saying that, maybe now that things have settled
> down, is a good time to add proper doc where it's missing.
Ok i've got extensive documentation for this now and the whole rest of this
series and will include it in v2.
>
>
> /// Seat information.
> ///
> /// This can't be accessed directly by the element embedding a `Seat`,
> /// but is used by the generic slot manager logic to control residency
> /// of a certain object on a hardware slot.
> > +pub(crate) struct SeatInfo {
> > + /// Slot used by this seat.
> ///
> /// This index is only valid if the slot pointed by this index
> /// has its `SlotInfo::seqno` match SeatInfo::seqno. Otherwise,
> /// it means the object has been evicted from the hardware slot,
> /// and a new slot needs to be acquired to make this object
> /// resident again.
> > + slot: u8,
> > +
> > + /// Sequence number encoding the last time this seat was active.
> > + /// We also use it to check if a slot is still bound to a seat.
> > + seqno: u64,
> > +}
> > +
>
> /// Seat state
> ///
> /// This is meant to be embedded in the object that wants to acquire
> /// hardware slots. It also starts in the `Seat::NoSeat` state, and
> /// the slot manager will change the object value when an active/evict
> /// request to is issued.
> > +#[derive(Default)]
> > +pub(crate) enum Seat {
> > + #[expect(clippy::enum_variant_names)]
> /// Resource is not resident.
> ///
> /// All objects start with a seat in that state. The seat also
> /// gets back to that state if the user requests eviction. It
> /// can also end up in that state next time an operation is done
> /// on an `Seat::Idle` seat and the slot managers finds out this
> /// object has been evicted from the slot.
> > + #[default]
> > + NoSeat,
>
> /// Resource is actively used and resident.
> ///
> /// When a seat is in that state, it can't be evicted, and the
> /// slot pointed by `SlotInfo::slot` is guaranteed to be reserved
> /// for this object as long as the seat stays active.
> > + Active(SeatInfo),
>
> /// Resource is idle and might or might not be resident.
> ///
> /// When a seat is in that state, we can't know for sure if the
> /// object is resident or evicted until the next request we issue
> /// to the slot manager. This tells the slot manager it can
> /// reclaim the underlying slot if needed.
> /// In order for the hardware to use this object again, the seat
> /// needs to be turned into an `Seat::Active` state again
> /// with a `SlotManager::activate()` call.
> > + Idle(SeatInfo),
> > +}
> > +
> > +impl Seat {
> /// Get the slot index this seat is pointing to.
> ///
> /// If the seat is not `Seat::Active` we can't trust the
> /// `SeatInfo`. In that case `None` is returned, otherwise
> /// `Some(SeatInfo::slot)` is returned.
> > + pub(super) fn slot(&self) -> Option<u8> {
> > + match self {
> > + Self::Active(info) => Some(info.slot),
> > + _ => None,
> > + }
> > + }
> > +}
> > +
>
> /// Trait describing the slot-related operations.
> > +pub(crate) trait SlotOperations {
> > + type SlotData;
> > +
> > + /// Called when a slot is being activated for a seat.
> > + ///
> > + /// This callback allows hardware-specific actions to be performed when a slot
> > + /// becomes active, such as updating hardware registers or invalidating caches.
> > + fn activate(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> > + Ok(())
> > + }
> > +
> > + /// Called when a slot is being evicted and freed.
> > + ///
> > + /// This callback allows hardware-specific cleanup when a slot is being
> > + /// completely freed, either explicitly or when an idle slot is being
> > + /// reused for a different seat. Any hardware state should be invalidated.
> > + fn evict(&mut self, _slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> > + Ok(())
> > + }
> > +}
> > +
>
> /// Data attached to a slot.
> > +struct SlotInfo<T> {
> > + /// Type specific data attached to a slot
> > + slot_data: T,
> > +
> > + /// Sequence number from when this slot was last activated
> > + seqno: u64,
> > +}
> > +
>
> /// Slot state.
> > +#[derive(Default)]
> > +enum Slot<T> {
> /// Slot is free.
> ///
> /// All slots start in this state when the slot manager is created.
> > + #[default]
> > + Free,
>
> /// Slot is active.
> ///
> /// When is that state, the slot is guaranteed to stay active
> /// for as long as the resource bound to it has its seat in the
> /// `Seat::Active` state. No new resource can be bound to it.
> > + Active(SlotInfo<T>),
>
> /// Slot is idle.
> ///
> /// Happens when the underlying resource has been flagged
> /// `Seat::Idle`. When in that state, the slot manager is allowed
> /// to evict the resource and re-assign the slot to someone else.
> /// This process involves updating the `SlotInfo::seqno` which
> /// will be checked against the `SeatInfo::seqno` in case the idle
> /// resource wants to become active again.
> > + Idle(SlotInfo<T>),
> > +}
> > +
>
> /// Generic slot manager object.
> ///
> /// It abstracts away all the churn around activeness/idleness tracking
> /// and let the implementer of the SlotOperations trait focus on how to
> /// make a resource active or evict it.
> > +pub(crate) struct SlotManager<T: SlotOperations, const MAX_SLOTS: usize> {
> > + /// Manager specific data
> > + manager: T,
> > +
> > + /// Number of slots actually available
> > + slot_count: usize,
> > +
> > + /// Slots
> > + slots: [Slot<T::SlotData>; MAX_SLOTS],
> > +
> > + /// Sequence number incremented each time a Seat is successfully activated
> > + use_seqno: u64,
> > +}
> > +
> > +// A `Seat` protected by the same lock that is used to wrap the `SlotManager`.
>
> Should this be
>
> /// A `Seat` ....
>
> ?
I'll change it over to a document comment since it's an important point.
>
> > +type LockedSeat<T, const MAX_SLOTS: usize> = LockedBy<Seat, SlotManager<T, MAX_SLOTS>>;
> > +
> > +impl<T: SlotOperations, const MAX_SLOTS: usize> Unpin for SlotManager<T, MAX_SLOTS> {}
>
> Do we really need to explicitly flag this type Unpin? I thought this
> was the default if the struct is not pinned (and it's not AFAICT).
>
At some point we needed this to expressly impl Unpin, but now we don't,
so I've removed it in v2 and will otherwise rely on the default
behaviour for Unpin.
> > +
> > +impl<T: SlotOperations, const MAX_SLOTS: usize> SlotManager<T, MAX_SLOTS> {
> /// Creates a new slot manager.
> > + pub(crate) fn new(manager: T, slot_count: usize) -> Result<Self> {
> > + if slot_count == 0 {
> > + return Err(EINVAL);
> > + }
> > + if slot_count > MAX_SLOTS {
> > + return Err(EINVAL);
> > + }
> > + Ok(Self {
> > + manager,
> > + slot_count,
> > + slots: [const { Slot::Free }; MAX_SLOTS],
> > + use_seqno: 1,
> > + })
> > + }
> > +
> > + fn record_active_slot(
> > + &mut self,
> > + slot_idx: usize,
> > + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> > + slot_data: T::SlotData,
> > + ) -> Result {
> > + let cur_seqno = self.use_seqno;
> > +
> > + *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> > + slot: slot_idx as u8,
> > + seqno: cur_seqno,
> > + });
> > +
> > + self.slots[slot_idx] = Slot::Active(SlotInfo {
> > + slot_data,
> > + seqno: cur_seqno,
> > + });
> > +
> > + self.use_seqno += 1;
> > + Ok(())
> > + }
> > +
> > + fn activate_slot(
> > + &mut self,
> > + slot_idx: usize,
> > + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> > + slot_data: T::SlotData,
> > + ) -> Result {
> > + self.manager.activate(slot_idx, &slot_data)?;
> > + self.record_active_slot(slot_idx, locked_seat, slot_data)
> > + }
> > +
> > + fn allocate_slot(
> > + &mut self,
> > + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> > + slot_data: T::SlotData,
> > + ) -> Result {
> > + let slots = &self.slots[..self.slot_count];
> > +
> > + let mut idle_slot_idx = None;
> > + let mut idle_slot_seqno: u64 = 0;
> > +
> > + for (slot_idx, slot) in slots.iter().enumerate() {
> > + match slot {
> > + Slot::Free => {
> > + return self.activate_slot(slot_idx, locked_seat, slot_data);
> > + }
> > + Slot::Idle(slot_info) => {
> > + if idle_slot_idx.is_none() || slot_info.seqno < idle_slot_seqno {
> > + idle_slot_idx = Some(slot_idx);
> > + idle_slot_seqno = slot_info.seqno;
> > + }
> > + }
> > + Slot::Active(_) => (),
> > + }
> > + }
> > +
> > + match idle_slot_idx {
> > + Some(slot_idx) => {
> > + // Lazily evict idle slot just before it is reused
> > + if let Slot::Idle(slot_info) = &self.slots[slot_idx] {
> > + self.manager.evict(slot_idx, &slot_info.slot_data)?;
> > + }
> > + self.activate_slot(slot_idx, locked_seat, slot_data)
> > + }
> > + None => {
> > + pr_err!(
> > + "Slot allocation failed: all {} slots in use\n",
> > + self.slot_count
> > + );
> > + Err(EBUSY)
> > + }
> > + }
> > + }
> > +
> > + fn idle_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> > + let slot = take(&mut self.slots[slot_idx]);
> > +
> > + if let Slot::Active(slot_info) = slot {
> > + self.slots[slot_idx] = Slot::Idle(SlotInfo {
> > + slot_data: slot_info.slot_data,
> > + seqno: slot_info.seqno,
> > + })
> > + };
> > +
> > + *locked_seat.access_mut(self) = match locked_seat.access(self) {
> > + Seat::Active(seat_info) | Seat::Idle(seat_info) => Seat::Idle(SeatInfo {
> > + slot: seat_info.slot,
> > + seqno: seat_info.seqno,
> > + }),
> > + Seat::NoSeat => Seat::NoSeat,
> > + };
> > + Ok(())
> > + }
> > +
> > + fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> > + match &self.slots[slot_idx] {
> > + Slot::Active(slot_info) | Slot::Idle(slot_info) => {
> > + self.manager.evict(slot_idx, &slot_info.slot_data)?;
> > + take(&mut self.slots[slot_idx]);
> > + }
> > + _ => (),
> > + }
> > +
> > + *locked_seat.access_mut(self) = Seat::NoSeat;
> > + Ok(())
> > + }
> > +
> > + // Checks and updates the seat state based on the slot it points to
> > + // (if any). Returns a Seat with a value matching the slot state.
> > + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> > + let new_seat = match locked_seat.access(self) {
> > + Seat::Active(seat_info) => {
> > + let old_slot_idx = seat_info.slot as usize;
> > + let slot = &self.slots[old_slot_idx];
> > +
> > + if kernel::warn_on!(
> > + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
> > + ) {
> > + Seat::NoSeat
> > + } else {
> > + Seat::Active(SeatInfo {
> > + slot: seat_info.slot,
> > + seqno: seat_info.seqno,
> > + })
> > + }
> > + }
> > +
> > + Seat::Idle(seat_info) => {
> > + let old_slot_idx = seat_info.slot as usize;
> > + let slot = &self.slots[old_slot_idx];
> > +
> > + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
> > + Seat::NoSeat
> > + } else {
> > + Seat::Idle(SeatInfo {
> > + slot: seat_info.slot,
> > + seqno: seat_info.seqno,
> > + })
> > + }
> > + }
> > +
> > + _ => Seat::NoSeat,
> > + };
> > +
> > + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> > + // so that only slot.rs can change the seat state, but there might be better solutions
> > + // to prevent that.
>
> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
>
> > + match &new_seat {
> > + Seat::Active(seat_info) => {
> > + *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> > + slot: seat_info.slot,
> > + seqno: seat_info.seqno,
> > + })
> > + }
> > + Seat::Idle(seat_info) => {
> > + *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
> > + slot: seat_info.slot,
> > + seqno: seat_info.seqno,
> > + })
> > + }
> > + _ => *locked_seat.access_mut(self) = Seat::NoSeat,
> > + }
> > +
> > + new_seat
> > + }
> > +
>
> /// Make a resource active on any available/reclaimable slot.
> ///
> /// Will return an error if no slot is available/reclaimable, or if
> /// the reclaim failed.
> > + pub(crate) fn activate(
> > + &mut self,
> > + locked_seat: &LockedSeat<T, MAX_SLOTS>,
> > + slot_data: T::SlotData,
> > + ) -> Result {
> > + let seat = self.check_seat(locked_seat);
> > + match seat {
> > + Seat::Active(seat_info) | Seat::Idle(seat_info) => {
> > + // With lazy eviction, if seqno matches, the hardware state is still
> > + // valid for both Active and Idle slots, so just update our records
> > + self.record_active_slot(seat_info.slot as usize, locked_seat, slot_data)
> > + }
> > + _ => self.allocate_slot(locked_seat, slot_data),
> > + }
> > + }
> > +
> /// Flag a resource idle.
> ///
> /// The slot manager can decide to reclaim the slot this resource
> /// was bound to at any point after function returns.
> > + // The idle() method will be used when we start adding support for user VMs
> > + #[expect(dead_code)]
> > + pub(crate) fn idle(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> > + let seat = self.check_seat(locked_seat);
> > + if let Seat::Active(seat_info) = seat {
> > + self.idle_slot(seat_info.slot as usize, locked_seat)?;
> > + }
> > + Ok(())
> > + }
> > +
> > + /// Evict a seat from its slot, freeing up the hardware resource.
>
> I think I'd go:
>
> /// Evict a resource from its slot, and make this slot free again
> /// for other users.
>
> > + pub(crate) fn evict(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Result {
> > + let seat = self.check_seat(locked_seat);
> > +
> > + match seat {
> > + Seat::Active(seat_info) | Seat::Idle(seat_info) => {
> > + let slot_idx = seat_info.slot as usize;
> > +
> > + self.evict_slot(slot_idx, locked_seat)?;
> > + }
> > + _ => (),
> > + }
> > +
> > + Ok(())
> > + }
> > +}
> > +
> > +impl<T: SlotOperations, const MAX_SLOTS: usize> Deref for SlotManager<T, MAX_SLOTS> {
> > + type Target = T;
> > +
> > + fn deref(&self) -> &Self::Target {
> > + &self.manager
> > + }
> > +}
> > +
> > +impl<T: SlotOperations, const MAX_SLOTS: usize> DerefMut for SlotManager<T, MAX_SLOTS> {
> > + fn deref_mut(&mut self) -> &mut Self::Target {
> > + &mut self.manager
> > + }
> > +}
> > diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> > index 6eaa2135fe07..f54b997355e0 100644
> > --- a/drivers/gpu/drm/tyr/tyr.rs
> > +++ b/drivers/gpu/drm/tyr/tyr.rs
> > @@ -12,6 +12,7 @@
> > mod gem;
> > mod gpu;
> > mod regs;
> > +mod slot;
> >
> > kernel::module_platform_driver! {
> > type: TyrPlatformDeviceData,
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-22 18:46 ` Daniel Almeida
@ 2026-02-28 0:28 ` Deborah Brouwer
2026-03-02 10:10 ` Boris Brezillon
0 siblings, 1 reply; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:28 UTC (permalink / raw)
To: Daniel Almeida
Cc: Boris Brezillon, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
On Sun, Feb 22, 2026 at 03:46:48PM -0300, Daniel Almeida wrote:
>
>
> > On 22 Feb 2026, at 14:57, Boris Brezillon <boris.brezillon@collabora.com> wrote:
> >
> > On Fri, 20 Feb 2026 13:55:31 -0300
> > Daniel Almeida <daniel.almeida@collabora.com> wrote:
> >
> >>> On 20 Feb 2026, at 13:21, Boris Brezillon <boris.brezillon@collabora.com> wrote:
> >>>
> >>> On Fri, 20 Feb 2026 12:25:43 -0300
> >>> Daniel Almeida <daniel.almeida@collabora.com> wrote:
> >>>
> >>>>>> +
> >>>>>> + // Checks and updates the seat state based on the slot it points to
> >>>>>> + // (if any). Returns a Seat with a value matching the slot state.
> >>>>>> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> >>>>>> + let new_seat = match locked_seat.access(self) {
> >>>>>> + Seat::Active(seat_info) => {
> >>>>>> + let old_slot_idx = seat_info.slot as usize;
> >>>>>> + let slot = &self.slots[old_slot_idx];
> >>>>>> +
> >>>>>> + if kernel::warn_on!(
> >>>>>> + !matches!(slot, Slot::Active(slot_info) if slot_info.seqno == seat_info.seqno)
> >>>>>> + ) {
> >>>>>> + Seat::NoSeat
> >>>>>> + } else {
> >>>>>> + Seat::Active(SeatInfo {
> >>>>>> + slot: seat_info.slot,
> >>>>>> + seqno: seat_info.seqno,
> >>>>>> + })
> >>>>>> + }
> >>>>>> + }
> >>>>>> +
> >>>>>> + Seat::Idle(seat_info) => {
> >>>>>> + let old_slot_idx = seat_info.slot as usize;
> >>>>>> + let slot = &self.slots[old_slot_idx];
> >>>>>> +
> >>>>>> + if !matches!(slot, Slot::Idle(slot_info) if slot_info.seqno == seat_info.seqno) {
> >>>>>> + Seat::NoSeat
> >>>>>> + } else {
> >>>>>> + Seat::Idle(SeatInfo {
> >>>>>> + slot: seat_info.slot,
> >>>>>> + seqno: seat_info.seqno,
> >>>>>> + })
> >>>>>> + }
> >>>>>> + }
> >>>>>> +
> >>>>>> + _ => Seat::NoSeat,
> >>>>>> + };
> >>>>>> +
> >>>>>> + // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> >>>>>> + // so that only slot.rs can change the seat state, but there might be better solutions
> >>>>>> + // to prevent that.
> >>>>>
> >>>>> Okay, I guess we want some inputs from Daniel and/or Alice on that one.
> >>>>
> >>>> Hm, I'd say we shouldn't implement Clone to avoid any possibility of holding on
> >>>> to stale state by duplicating it.
> >>>
> >>> Okay, so basically what we have now.
> >>>
> >>>>
> >>>> Why do we need to return Seat from this function? Can't we simply write
> >>>> locked_seat in place and not return anything?
> >>>
> >>> We do both actually. IIRC, the reason is that LockedBy borrows &self if
> >>> we want to read the locked_seat, which prevents us from calling methods
> >>> taking a &mut ref from a `match(locked_seat.access())`.
> >>
> >>
> >> I am referring to this change:
> >>
> >> --- a/drivers/gpu/drm/tyr/slot.rs
> >> +++ b/drivers/gpu/drm/tyr/slot.rs
> >> @@ -242,8 +242,8 @@ fn evict_slot(&mut self, slot_idx: usize, locked_seat: &LockedSeat<T, MAX_SLOTS>
> >> }
> >>
> >> // Checks and updates the seat state based on the slot it points to
> >> - fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> >> + fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
> >> let new_seat = match locked_seat.access(self) {
> >> Seat::Active(seat_info) => {
> >> let old_slot_idx = seat_info.slot as usize;
> >> @@ -278,26 +278,7 @@ fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) -> Seat {
> >> _ => Seat::NoSeat,
> >> };
> >>
> >> - // FIXME: Annoying manual copy. The original idea was to not add Copy+Clone to SeatInfo,
> >> - // so that only slot.rs can change the seat state, but there might be better solutions
> >> - // to prevent that.
> >> - match &new_seat {
> >> - Seat::Active(seat_info) => {
> >> - *locked_seat.access_mut(self) = Seat::Active(SeatInfo {
> >> - slot: seat_info.slot,
> >> - seqno: seat_info.seqno,
> >> - })
> >> - }
> >> - Seat::Idle(seat_info) => {
> >> - *locked_seat.access_mut(self) = Seat::Idle(SeatInfo {
> >> - slot: seat_info.slot,
> >> - seqno: seat_info.seqno,
> >> - })
> >> - }
> >> - _ => *locked_seat.access_mut(self) = Seat::NoSeat,
> >> - }
> >> -
> >> - new_seat
> >> + *locked_seat.access_mut(self) = new_seat;
> >
> > That one requires Copy support, or am I missing something?
>
> No. Copy and Clone produce a new value and both the old and new values remain
> valid. The line above is moving a value into a new location, invalidating the
> previous one. This means that Copy is not required.
>
> >
> >> }
> >>
> >> Or even shorter:
> >>
> >> fn check_seat(&mut self, locked_seat: &LockedSeat<T, MAX_SLOTS>) {
> >> let (slot_idx, seqno, is_active) = match locked_seat.access(self) {
> >> Seat::Active(info) => (info.slot as usize, info.seqno, true),
> >> Seat::Idle(info) => (info.slot as usize, info.seqno, false),
> >> _ => return,
> >> };
> >>
> >> let valid = if is_active {
> >> !kernel::warn_on!(!matches!(&self.slots[slot_idx], Slot::Active(s) if s.seqno == seqno))
> >> } else {
> >> matches!(&self.slots[slot_idx], Slot::Idle(s) if s.seqno == seqno)
> >> };
> >>
> >> if !valid {
> >> *locked_seat.access_mut(self) = Seat::NoSeat;
> >> }
> >> }
> >
> > Did you try that? Last I tried, I was hitting a wall because the caller
> > of check_seat() does a match on the updated seat, and inside this
> > match, it calls functions that need a &mut self, and the borrow checker
> > rightfully points the invalid &self then &mut self borrow pattern.
>
> Yes, I compiled-tested all changes I suggested. My changes compile because they
> intentionally avoid doing what you said above.
>
> The key here is that your borrows do not overlap anymore. The code I showed
> first borrows immutably, and then returns this tuple: (slot_idx, seqno, is_active).
> The immutable borrow then ends, since this tuple is copied (not borrowed) from
> its source. This benefits from the fact that primitive types are Copy.
>
> Note that you can borrow both mutably and immutably in the same scope just fine. The
> borrows just can’t be alive at the same time. When the borrow checker rejects your code,
> it shows you why incompatible borrows overlap (i.e.: check the “first used here…later
> used here” part of the error).
>
> >
> >>
> >> access vs access_mut() does not matter here: since the owner is &mut self
> >> anyways we know we have exclusive access to the LockedSeat throughout the whole
> >> function.
> >
> > I agree, but LockedBy is picky, and last I tried I couldn't make it
> > work without the annoying update+return-copy-of-seat dance you see
> > here. Maybe I missed something obvious and it does indeed work with
> > your suggested changes, dunno.
>
> Rewriting things so they pass the borrow checker is common in Rust. Sometimes
> it can be done rather easily; other times the design is just broken and needs
> to be reworked. Luckily this one fell in the first category.
>
> This benefits from the fact that no one can race us between reading this tuple
>
> (slot_idx, seqno, is_active)
>
> ..and using it. That’s because we’re taking &mut self as a proxy in LockedBy, so
> we’re sure we have exclusive access in this scope.
>
> If you don’t have any complaints about the code I sent (i.e.: convoluted, wrong
> logic, etc), I suggest switching to it.
I’ve tested the short check_seat() function and can confirm that it correctly
compares Seat and Slot seqno as well as emits the kernel warning when we have a
mismatch on an active Seat. So I’ll the simplified check_seat() function in v2
unless there are any more issues to address.
>
>
> — Daniel
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-12 10:44 ` Boris Brezillon
@ 2026-02-28 0:31 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:31 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 11:44:18AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:09 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> > SlotManager for allocating hardware address space slots. The underlying
> > AddressSpaceManager performs MMU operations including enabling/disabling
> > address spaces, flushing page tables, and locking regions for page table
> > updates.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/driver.rs | 3 +
> > drivers/gpu/drm/tyr/mmu.rs | 91 +++++++
> > drivers/gpu/drm/tyr/mmu/address_space.rs | 322 +++++++++++++++++++++++
> > drivers/gpu/drm/tyr/tyr.rs | 1 +
> > 4 files changed, 417 insertions(+)
> > create mode 100644 drivers/gpu/drm/tyr/mmu.rs
> > create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
> >
> > diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> > index 2973a8b3cc09..ad5a765a6c2a 100644
> > --- a/drivers/gpu/drm/tyr/driver.rs
> > +++ b/drivers/gpu/drm/tyr/driver.rs
> > @@ -43,6 +43,7 @@
> > gem::BoData,
> > gpu,
> > gpu::GpuInfo,
> > + mmu::Mmu,
> > regs, //
> > };
> >
> > @@ -148,6 +149,8 @@ fn probe(
> > let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> > let platform: ARef<platform::Device> = pdev.into();
> >
> > + let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
> > +
> > let data = try_pin_init!(TyrDrmDeviceData {
> > pdev: platform.clone(),
> > clks <- new_mutex!(Clocks {
> > diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
> > new file mode 100644
> > index 000000000000..8e076c35f342
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/mmu.rs
> > @@ -0,0 +1,91 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! Memory Management Unit (MMU) driver for the Tyr GPU.
> > +//!
> > +//! This module manages GPU address spaces and virtual memory operations through
> > +//! hardware MMU slots. It provides functionality for flushing page tables and
> > +//! managing VM updates for active address spaces.
> > +//!
> > +//! The MMU coordinates with the [`AddressSpaceManager`] to handle hardware
> > +//! address space allocation and page table operations, using [`SlotManager`]
> > +//! to track which address spaces are currently active in hardware slots.
> > +//!
> > +//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
> > +//! [`SlotManager`]: crate::slot::SlotManager
> > +#![allow(dead_code)]
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + devres::Devres,
> > + new_mutex,
> > + platform,
> > + prelude::*,
> > + sync::{
> > + Arc,
> > + ArcBorrow,
> > + Mutex, //
> > + }, //
> > +};
> > +
> > +use crate::{
> > + driver::IoMem,
> > + gpu::GpuInfo,
> > + mmu::address_space::{
> > + AddressSpaceManager,
> > + VmAsData, //
> > + },
> > + regs::MAX_AS_REGISTERS,
> > + slot::{
> > + SlotManager, //
> > + }, //
>
> formatting nit:
> slot::SlotManager, //
>
> > +};
> > +
> > +pub(crate) mod address_space;
> > +
> > +pub(crate) type AsSlotManager = SlotManager<AddressSpaceManager, MAX_AS_REGISTERS>;
> > +
>
> /// MMU component of the GPU.
> ///
> /// This is used to bind VM objects to an AS (Address Space) slot
> /// and make the VM active on the GPU.
>
> > +#[pin_data]
> > +pub(crate) struct Mmu {
> > + /// Manages the allocation of hardware MMU slots to GPU address spaces.
> > + ///
> > + /// Tracks which address spaces are currently active in hardware slots and
> > + /// coordinates address space operations like flushing and VM updates.
> > + #[pin]
> > + pub(crate) as_manager: Mutex<AsSlotManager>,
> > +}
> > +
> > +impl Mmu {
>
> /// Create an MMU component for this device.
>
> > + pub(crate) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + gpu_info: &GpuInfo,
> > + ) -> Result<Arc<Mmu>> {
> > + let slot_count = gpu_info.as_present.count_ones().try_into()?;
> > + let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
> > + let mmu_init = try_pin_init!(Self{
> > + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
> > + });
> > + Arc::pin_init(mmu_init, GFP_KERNEL)
> > + }
> > +
>
> /// Make a VM active.
> ///
> /// This implies assigning the VM to an AS slot through the slot
> /// manager.
> > + pub(crate) fn activate_vm(&self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> > + self.as_manager.lock().activate_vm(vm)
> > + }
> > +
>
> /// Make the VM inactive.
> ///
> /// Evicts the VM from its AS slot through the slot manager.
>
> > + pub(crate) fn deactivate_vm(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().deactivate_vm(vm)
> > + }
> > +
>
> /// Flush caches after a VM update.
> ///
> /// If the VM is no longer resident, this is a NOP, otherwise, the
> /// AS manager will flush the GPU and MMU (TLB) caches.
> > + pub(crate) fn flush_vm(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().flush_vm(vm)
> > + }
> > +
>
> /// Flags the start of a VM update.
> ///
> /// If the VM is resident, any GPU access on the memory range being
> /// updated will be blocked until `Mmu::end_vm_update()` is called.
> /// This guarantees the atomicity of a VM update.
> /// If the VM is not resident, this is a NOP.
> > + pub(crate) fn start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result {
> > + self.as_manager.lock().start_vm_update(vm, region)
> > + }
> > +
>
> /// Flags the end of a VM update.
> ///
> /// If the VM is resident, this will let GPU accesse on the updated
> /// range go through, in case any of them were blocked.
> /// If the VM is not resident, this is a NOP.
> > + pub(crate) fn end_vm_update(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().end_vm_update(vm)
> > + }
> > +}
> > diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
> > new file mode 100644
> > index 000000000000..60e9a79112f0
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
> > @@ -0,0 +1,322 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! GPU address space management and hardware operations.
> > +//!
> > +//! This module manages GPU hardware address spaces, including configuration,
> > +//! command submission, and page table update regions. It handles the hardware
> > +//! interaction for MMU operations through MMIO register access.
> > +//!
> > +//! The [`AddressSpaceManager`] implements [`SlotOperations`] to integrate with
> > +//! the slot management system, enabling and configuring address spaces in the
> > +//! hardware slots as needed.
> > +//!
> > +//! [`SlotOperations`]: crate::slot::SlotOperations
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + bits::*,
> > + device::{
> > + Bound,
> > + Device, //
> > + },
> > + devres::Devres,
> > + error::Result,
> > + io,
> > + iommu::pgtable::{
> > + IoPageTable,
> > + ARM64LPAES1, //
> > + },
> > + platform,
> > + prelude::*,
> > + sync::{
> > + aref::ARef,
> > + Arc,
> > + ArcBorrow,
> > + LockedBy, //
> > + },
> > + time::Delta, //
> > +};
> > +
> > +use crate::{
> > + driver::IoMem,
> > + mmu::{
> > + AsSlotManager,
> > + Mmu, //
> > + },
> > + regs::*,
> > + slot::{
> > + Seat,
> > + SlotOperations, //
> > + }, //
> > +};
> > +
> > +/// Hardware address space configuration registers.
> > +///
> > +/// Contains the values to be written to the GPU's AS registers when
> > +/// activating this address space.
> > +#[derive(Clone, Copy)]
> > +pub(crate) struct AddressSpaceConfig {
> > + pub(crate) transcfg: u64,
> > + pub(crate) transtab: u64,
> > + pub(crate) memattr: u64,
>
> Don't know if we need to document those fields.
v2 will add docs.
>
> > +}
> > +
> > +/// Any resource/information that will be used by the AddressSpaceManager
> > +/// to make a VM active is present in VmAsData.
> > +///
> > +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> > +/// the slot to make sure the page table and the underlying resources
> > +/// (pages) used by the AS slot won't go away while the MMU points to
> > +/// those.
> > +pub(crate) struct VmAsData {
> > + /// Tracks this VM's binding to a hardware address space slot.
> > + as_seat: LockedBy<Seat, AsSlotManager>,
> > + /// Hardware configuration for this address space.
> > + as_config: AddressSpaceConfig,
> > + /// Page table (managed by devres).
> > + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> > +}
> > +
> > +impl VmAsData {
>
> Missing doc.
>
> > + pub(crate) fn new(
> > + mmu: &Mmu,
> > + as_config: AddressSpaceConfig,
> > + page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> > + ) -> VmAsData {
> > + Self {
> > + as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
> > + as_config,
> > + page_table,
> > + }
> > + }
> > +}
> > +
> > +/// Manages GPU hardware address spaces via MMIO register operations.
> > +pub(crate) struct AddressSpaceManager {
>
> Missing docs on the first two fields.
>
> > + pdev: ARef<platform::Device>,
> > + iomem: Arc<Devres<IoMem>>,
> > + /// Bitmask of available address space slots from GPU_AS_PRESENT register
> > + as_present: u32,
> > +}
> > +
> > +impl SlotOperations for AddressSpaceManager {
> > + type SlotData = Arc<VmAsData>;
> > +
> > + fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) -> Result {
> > + self.as_enable(slot_idx, &slot_data.as_config)
> > + }
> > +
> > + fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> > + if self.iomem.try_access().is_some() {
> > + let _ = self.as_flush(slot_idx);
> > + let _ = self.as_disable(slot_idx);
>
> I don't think we should ignore the errors returned by
> as_flush/disable(). Ultimately, what we should do is trigger a GPU
> reset and leave the slot 'busy' until we're sure no one will try
> to use it (the "stop everything" happening in the reset path). Once this
> is done, we can forcibly evict the VM (basically remove the VM from the
> slot without doing any HW transaction) and let the reset do its job.
>
> Since we're not yet at a point where we have a functional reset flow,
> I'd recommend failing the eviction if as_flush/disable() returns an
> error (shouldn't happen with just the FW-boot logic anyway). This means
> we might leave active slots behind and possibly leak resources when that
> happens, but that's better than the UAF we'd create if the AS still
> points to a page table that's been freed.
Ack v2 will propagate these errors.
>
> > + }
> > + Ok(())
> > + }
> > +}
> > +
> > +impl AddressSpaceManager {
>
> Missing docs.
>
> > + pub(super) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + as_present: u32,
> > + ) -> Result<AddressSpaceManager> {
> > + Ok(Self {
> > + pdev: pdev.into(),
> > + iomem: iomem.into(),
> > + as_present,
> > + })
> > + }
> > +
> > + fn dev(&self) -> &Device<Bound> {
> > + // SAFETY: pdev is a bound device.
> > + unsafe { self.pdev.as_ref().as_bound() }
> > + }
> > +
> > + fn validate_as_slot(&self, as_nr: usize) -> Result {
> > + if as_nr >= MAX_AS_REGISTERS {
> > + pr_err!(
> > + "AS slot {} out of valid range (max {})\n",
> > + as_nr,
> > + MAX_AS_REGISTERS
> > + );
> > + return Err(EINVAL);
> > + }
> > +
> > + if (self.as_present & (1 << as_nr)) == 0 {
> > + pr_err!(
> > + "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
> > + as_nr,
> > + self.as_present
> > + );
> > + return Err(EINVAL);
> > + }
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_wait_ready(&self, as_nr: usize) -> Result {
> > + let op = || as_status(as_nr)?.read(self.dev(), &self.iomem);
> > + let cond = |status: &u32| -> bool { *status & AS_STATUS_ACTIVE == 0 };
> > + let _ =
> > + io::poll::read_poll_timeout(op, cond, Delta::from_millis(0), Delta::from_millis(10))?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_send_cmd(&mut self, as_nr: usize, cmd: u32) -> Result {
> > + self.as_wait_ready(as_nr)?;
> > + as_command(as_nr)?.write(self.dev(), &self.iomem, cmd)?;
> > + Ok(())
> > + }
> > +
> > + fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: u32) -> Result {
> > + self.as_send_cmd(as_nr, cmd)?;
> > + self.as_wait_ready(as_nr)?;
> > + Ok(())
> > + }
> > +
> > + fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + let transtab = as_config.transtab;
> > + let transcfg = as_config.transcfg;
> > + let memattr = as_config.memattr;
> > +
> > + let transtab_lo = (transtab & 0xffffffff) as u32;
> > + let transtab_hi = (transtab >> 32) as u32;
> > +
> > + let transcfg_lo = (transcfg & 0xffffffff) as u32;
> > + let transcfg_hi = (transcfg >> 32) as u32;
> > +
> > + let memattr_lo = (memattr & 0xffffffff) as u32;
> > + let memattr_hi = (memattr >> 32) as u32;
> > +
> > + let dev = self.dev();
> > + as_transtab_lo(as_nr)?.write(dev, &self.iomem, transtab_lo)?;
> > + as_transtab_hi(as_nr)?.write(dev, &self.iomem, transtab_hi)?;
> > +
> > + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, transcfg_lo)?;
> > + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, transcfg_hi)?;
> > +
> > + as_memattr_lo(as_nr)?.write(dev, &self.iomem, memattr_lo)?;
> > + as_memattr_hi(as_nr)?.write(dev, &self.iomem, memattr_hi)?;
> > +
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_disable(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + // Flush AS before disabling
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_MEM)?;
> > +
> > + let dev = self.dev();
> > + as_transtab_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> > + as_transtab_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + as_memattr_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> > + as_memattr_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, AS_TRANSCFG_ADRMODE_UNMAPPED as u32)?;
> > + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + // The locked region is a naturally aligned power of 2 block encoded as
> > + // log2 minus(1).
> > + //
> > + // Calculate the desired start/end and look for the highest bit which
> > + // differs. The smallest naturally aligned block must include this bit
> > + // change, the desired region starts with this bit (and subsequent bits)
> > + // zeroed and ends with the bit (and subsequent bits) set to one.
> > + let region_width = core::cmp::max(
> > + 64 - (region.start ^ (region.end - 1)).leading_zeros() as u8,
> > + AS_LOCK_REGION_MIN_SIZE.trailing_zeros() as u8,
> > + ) - 1;
> > +
> > + // Mask off the low bits of region.start, which would be ignored by the
> > + // hardware anyways.
> > + let region_start =
> > + region.start & genmask_checked_u64(u32::from(region_width)..=63).ok_or(EINVAL)?;
> > +
> > + let region = (u64::from(region_width)) | region_start;
> > +
> > + let region_lo = (region & 0xffffffff) as u32;
> > + let region_hi = (region >> 32) as u32;
> > +
> > + // Lock the region that needs to be updated.
> > + let dev = self.dev();
> > + as_lockaddr_lo(as_nr)?.write(dev, &self.iomem, region_lo)?;
> > + as_lockaddr_hi(as_nr)?.write(dev, &self.iomem, region_hi)?;
> > +
> > + self.as_send_cmd(as_nr, AS_COMMAND_LOCK)
> > + }
> > +
> > + fn as_end_update(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_PT)
> > + }
> > +
> > + fn as_flush(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > + self.as_send_cmd(as_nr, AS_COMMAND_FLUSH_PT)
> > + }
> > +}
> > +
> > +impl AsSlotManager {
> > + /// Locks a region for page table updates if the VM has an active slot.
> > + pub(super) fn start_vm_update(&mut self, vm: &VmAsData, region: &Range<u64>) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_start_update(as_nr, region)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page table updates for a VM if it has an active slot.
> > + pub(super) fn end_vm_update(&mut self, vm: &VmAsData) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_end_update(as_nr)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn flush_vm(&mut self, vm: &VmAsData) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_flush(as_nr)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn activate_vm(&mut self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> > + self.activate(&vm.as_seat, vm.into())
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn deactivate_vm(&mut self, vm: &VmAsData) -> Result {
> > + self.evict(&vm.as_seat)
> > + }
> > +}
> > diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> > index f54b997355e0..ae435c7e80b1 100644
> > --- a/drivers/gpu/drm/tyr/tyr.rs
> > +++ b/drivers/gpu/drm/tyr/tyr.rs
> > @@ -11,6 +11,7 @@
> > mod file;
> > mod gem;
> > mod gpu;
> > +mod mmu;
> > mod regs;
> > mod slot;
> >
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-20 17:11 ` Daniel Almeida
@ 2026-02-28 0:46 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:46 UTC (permalink / raw)
To: Daniel Almeida
Cc: dri-devel, rust-for-linux, aliceryhl, boris.brezillon,
beata.michalska, lyude
On Fri, Feb 20, 2026 at 02:11:21PM -0300, Daniel Almeida wrote:
>
>
> > On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> >
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> > SlotManager for allocating hardware address space slots. The underlying
> > AddressSpaceManager performs MMU operations including enabling/disabling
> > address spaces, flushing page tables, and locking regions for page table
> > updates.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/driver.rs | 3 +
> > drivers/gpu/drm/tyr/mmu.rs | 91 +++++++
> > drivers/gpu/drm/tyr/mmu/address_space.rs | 322 +++++++++++++++++++++++
> > drivers/gpu/drm/tyr/tyr.rs | 1 +
> > 4 files changed, 417 insertions(+)
> > create mode 100644 drivers/gpu/drm/tyr/mmu.rs
> > create mode 100644 drivers/gpu/drm/tyr/mmu/address_space.rs
> >
> > diff --git a/drivers/gpu/drm/tyr/driver.rs b/drivers/gpu/drm/tyr/driver.rs
> > index 2973a8b3cc09..ad5a765a6c2a 100644
> > --- a/drivers/gpu/drm/tyr/driver.rs
> > +++ b/drivers/gpu/drm/tyr/driver.rs
> > @@ -43,6 +43,7 @@
> > gem::BoData,
> > gpu,
> > gpu::GpuInfo,
> > + mmu::Mmu,
> > regs, //
> > };
> >
> > @@ -148,6 +149,8 @@ fn probe(
> > let uninit_ddev = UnregisteredDevice::<TyrDrmDriver>::new(pdev.as_ref())?;
> > let platform: ARef<platform::Device> = pdev.into();
> >
> > + let _mmu = Mmu::new(pdev, iomem.as_arc_borrow(), &gpu_info)?;
> > +
>
> We need to store this or it will drop.
We store the mmu properly in struct firmware later in this series.
>
>
> > let data = try_pin_init!(TyrDrmDeviceData {
> > pdev: platform.clone(),
> > clks <- new_mutex!(Clocks {
> > diff --git a/drivers/gpu/drm/tyr/mmu.rs b/drivers/gpu/drm/tyr/mmu.rs
> > new file mode 100644
> > index 000000000000..8e076c35f342
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/mmu.rs
> > @@ -0,0 +1,91 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! Memory Management Unit (MMU) driver for the Tyr GPU.
> > +//!
> > +//! This module manages GPU address spaces and virtual memory operations through
> > +//! hardware MMU slots. It provides functionality for flushing page tables and
> > +//! managing VM updates for active address spaces.
> > +//!
> > +//! The MMU coordinates with the [`AddressSpaceManager`] to handle hardware
> > +//! address space allocation and page table operations, using [`SlotManager`]
> > +//! to track which address spaces are currently active in hardware slots.
> > +//!
> > +//! [`AddressSpaceManager`]: address_space::AddressSpaceManager
> > +//! [`SlotManager`]: crate::slot::SlotManager
> > +#![allow(dead_code)]
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + devres::Devres,
> > + new_mutex,
> > + platform,
> > + prelude::*,
> > + sync::{
> > + Arc,
> > + ArcBorrow,
> > + Mutex, //
> > + }, //
> > +};
> > +
> > +use crate::{
> > + driver::IoMem,
> > + gpu::GpuInfo,
> > + mmu::address_space::{
> > + AddressSpaceManager,
> > + VmAsData, //
> > + },
> > + regs::MAX_AS_REGISTERS,
> > + slot::{
> > + SlotManager, //
> > + }, //
> > +};
> > +
> > +pub(crate) mod address_space;
> > +
> > +pub(crate) type AsSlotManager = SlotManager<AddressSpaceManager, MAX_AS_REGISTERS>;
> > +
> > +#[pin_data]
> > +pub(crate) struct Mmu {
> > + /// Manages the allocation of hardware MMU slots to GPU address spaces.
> > + ///
> > + /// Tracks which address spaces are currently active in hardware slots and
> > + /// coordinates address space operations like flushing and VM updates.
> > + #[pin]
> > + pub(crate) as_manager: Mutex<AsSlotManager>,
> > +}
> > +
> > +impl Mmu {
> > + pub(crate) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + gpu_info: &GpuInfo,
> > + ) -> Result<Arc<Mmu>> {
> > + let slot_count = gpu_info.as_present.count_ones().try_into()?;
> > + let as_manager = AddressSpaceManager::new(pdev, iomem, gpu_info.as_present)?;
> > + let mmu_init = try_pin_init!(Self{
> > + as_manager <- new_mutex!(SlotManager::new(as_manager, slot_count)?),
> > + });
> > + Arc::pin_init(mmu_init, GFP_KERNEL)
> > + }
> > +
> > + pub(crate) fn activate_vm(&self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> > + self.as_manager.lock().activate_vm(vm)
> > + }
> > +
> > + pub(crate) fn deactivate_vm(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().deactivate_vm(vm)
> > + }
> > +
> > + pub(crate) fn flush_vm(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().flush_vm(vm)
> > + }
> > +
> > + pub(crate) fn start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result {
> > + self.as_manager.lock().start_vm_update(vm, region)
> > + }
>
> Please return a token type from this.
>
We implemented the RAII guard in PtUpdateContext which is in the vm patch.
> // if you want the whole vm to be locked while this update takes place
> struct VmUpdate<‘a> (Guard<‘a, AsSlotManager>);
>
> // if you don’t
> struct VmUpdate<‘a> (&’a AsSlotManager);
>
> The lifetime will be correctly elided as the lifetime of &self, i.e.:
>
> start_vm_update<‘a>(&’a self, vm: &VmAsData, region: &Range<u64>) -> Result<VmUpdate<‘a>>
>
> Which is just:
>
> start_vm_update(&self, vm: &VmAsData, region: &Range<u64>) -> Result<VmUpdate<‘_>>
>
>
> > +
> > + pub(crate) fn end_vm_update(&self, vm: &VmAsData) -> Result {
> > + self.as_manager.lock().end_vm_update(vm)
> > + }
>
> Move this to the token type’s Drop implementation.
>
> > +}
> > diff --git a/drivers/gpu/drm/tyr/mmu/address_space.rs b/drivers/gpu/drm/tyr/mmu/address_space.rs
> > new file mode 100644
> > index 000000000000..60e9a79112f0
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/mmu/address_space.rs
> > @@ -0,0 +1,322 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! GPU address space management and hardware operations.
> > +//!
> > +//! This module manages GPU hardware address spaces, including configuration,
> > +//! command submission, and page table update regions. It handles the hardware
> > +//! interaction for MMU operations through MMIO register access.
> > +//!
> > +//! The [`AddressSpaceManager`] implements [`SlotOperations`] to integrate with
> > +//! the slot management system, enabling and configuring address spaces in the
> > +//! hardware slots as needed.
> > +//!
> > +//! [`SlotOperations`]: crate::slot::SlotOperations
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + bits::*,
> > + device::{
> > + Bound,
> > + Device, //
> > + },
> > + devres::Devres,
> > + error::Result,
> > + io,
> > + iommu::pgtable::{
> > + IoPageTable,
> > + ARM64LPAES1, //
> > + },
> > + platform,
> > + prelude::*,
> > + sync::{
> > + aref::ARef,
> > + Arc,
> > + ArcBorrow,
> > + LockedBy, //
> > + },
> > + time::Delta, //
> > +};
> > +
> > +use crate::{
> > + driver::IoMem,
> > + mmu::{
> > + AsSlotManager,
> > + Mmu, //
> > + },
> > + regs::*,
> > + slot::{
> > + Seat,
> > + SlotOperations, //
> > + }, //
> > +};
> > +
> > +/// Hardware address space configuration registers.
> > +///
> > +/// Contains the values to be written to the GPU's AS registers when
> > +/// activating this address space.
> > +#[derive(Clone, Copy)]
> > +pub(crate) struct AddressSpaceConfig {
> > + pub(crate) transcfg: u64,
> > + pub(crate) transtab: u64,
> > + pub(crate) memattr: u64,
> > +}
>
> Do we need pub(crate) here?
We used to when Vm was creating the AddressSpaceConfig, but it won't
need this visibility in v2 so I will make it private.
>
> > +
> > +/// Any resource/information that will be used by the AddressSpaceManager
> > +/// to make a VM active is present in VmAsData.
> > +///
> > +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> > +/// the slot to make sure the page table and the underlying resources
> > +/// (pages) used by the AS slot won't go away while the MMU points to
> > +/// those.
> > +pub(crate) struct VmAsData {
> > + /// Tracks this VM's binding to a hardware address space slot.
> > + as_seat: LockedBy<Seat, AsSlotManager>,
> > + /// Hardware configuration for this address space.
> > + as_config: AddressSpaceConfig,
> > + /// Page table (managed by devres).
> > + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
>
> Do we need pub(crate) here?
>
> > +}
> > +
> > +impl VmAsData {
> > + pub(crate) fn new(
> > + mmu: &Mmu,
> > + as_config: AddressSpaceConfig,
> > + page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
> > + ) -> VmAsData {
> > + Self {
> > + as_seat: LockedBy::new(&mmu.as_manager, Seat::NoSeat),
> > + as_config,
> > + page_table,
> > + }
> > + }
> > +}
> > +
> > +/// Manages GPU hardware address spaces via MMIO register operations.
> > +pub(crate) struct AddressSpaceManager {
> > + pdev: ARef<platform::Device>,
> > + iomem: Arc<Devres<IoMem>>,
> > + /// Bitmask of available address space slots from GPU_AS_PRESENT register
> > + as_present: u32,
> > +}
> > +
> > +impl SlotOperations for AddressSpaceManager {
> > + type SlotData = Arc<VmAsData>;
> > +
> > + fn activate(&mut self, slot_idx: usize, slot_data: &Self::SlotData) -> Result {
> > + self.as_enable(slot_idx, &slot_data.as_config)
> > + }
> > +
> > + fn evict(&mut self, slot_idx: usize, _slot_data: &Self::SlotData) -> Result {
> > + if self.iomem.try_access().is_some() {
> > + let _ = self.as_flush(slot_idx);
> > + let _ = self.as_disable(slot_idx);
> > + }
> > + Ok(())
> > + }
> > +}
> > +
> > +impl AddressSpaceManager {
> > + pub(super) fn new(
> > + pdev: &platform::Device,
> > + iomem: ArcBorrow<'_, Devres<IoMem>>,
> > + as_present: u32,
> > + ) -> Result<AddressSpaceManager> {
> > + Ok(Self {
> > + pdev: pdev.into(),
> > + iomem: iomem.into(),
> > + as_present,
> > + })
> > + }
> > +
> > + fn dev(&self) -> &Device<Bound> {
> > + // SAFETY: pdev is a bound device.
>
> I don’t think we can say this for sure? I don’t think there is anything
> special about this scope that ensures Device<Bound>.
>
Could you suggest an alternative safety statement?
> > + unsafe { self.pdev.as_ref().as_bound() }
> > + }
> > +
> > + fn validate_as_slot(&self, as_nr: usize) -> Result {
> > + if as_nr >= MAX_AS_REGISTERS {
> > + pr_err!(
> > + "AS slot {} out of valid range (max {})\n",
> > + as_nr,
> > + MAX_AS_REGISTERS
> > + );
> > + return Err(EINVAL);
> > + }
> > +
> > + if (self.as_present & (1 << as_nr)) == 0 {
> > + pr_err!(
> > + "AS slot {} not present in hardware (AS_PRESENT={:#x})\n",
> > + as_nr,
> > + self.as_present
> > + );
> > + return Err(EINVAL);
> > + }
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_wait_ready(&self, as_nr: usize) -> Result {
> > + let op = || as_status(as_nr)?.read(self.dev(), &self.iomem);
> > + let cond = |status: &u32| -> bool { *status & AS_STATUS_ACTIVE == 0 };
> > + let _ =
> > + io::poll::read_poll_timeout(op, cond, Delta::from_millis(0), Delta::from_millis(10))?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_send_cmd(&mut self, as_nr: usize, cmd: u32) -> Result {
> > + self.as_wait_ready(as_nr)?;
> > + as_command(as_nr)?.write(self.dev(), &self.iomem, cmd)?;
> > + Ok(())
> > + }
> > +
> > + fn as_send_cmd_and_wait(&mut self, as_nr: usize, cmd: u32) -> Result {
> > + self.as_send_cmd(as_nr, cmd)?;
> > + self.as_wait_ready(as_nr)?;
> > + Ok(())
> > + }
> > +
> > + fn as_enable(&mut self, as_nr: usize, as_config: &AddressSpaceConfig) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + let transtab = as_config.transtab;
> > + let transcfg = as_config.transcfg;
> > + let memattr = as_config.memattr;
> > +
> > + let transtab_lo = (transtab & 0xffffffff) as u32;
> > + let transtab_hi = (transtab >> 32) as u32;
> > +
> > + let transcfg_lo = (transcfg & 0xffffffff) as u32;
> > + let transcfg_hi = (transcfg >> 32) as u32;
> > +
> > + let memattr_lo = (memattr & 0xffffffff) as u32;
> > + let memattr_hi = (memattr >> 32) as u32;
> > +
> > + let dev = self.dev();
> > + as_transtab_lo(as_nr)?.write(dev, &self.iomem, transtab_lo)?;
> > + as_transtab_hi(as_nr)?.write(dev, &self.iomem, transtab_hi)?;
> > +
> > + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, transcfg_lo)?;
> > + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, transcfg_hi)?;
> > +
> > + as_memattr_lo(as_nr)?.write(dev, &self.iomem, memattr_lo)?;
> > + as_memattr_hi(as_nr)?.write(dev, &self.iomem, memattr_hi)?;
> > +
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_disable(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + // Flush AS before disabling
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_MEM)?;
> > +
> > + let dev = self.dev();
> > + as_transtab_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> > + as_transtab_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + as_memattr_lo(as_nr)?.write(dev, &self.iomem, 0)?;
> > + as_memattr_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + as_transcfg_lo(as_nr)?.write(dev, &self.iomem, AS_TRANSCFG_ADRMODE_UNMAPPED as u32)?;
> > + as_transcfg_hi(as_nr)?.write(dev, &self.iomem, 0)?;
> > +
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_UPDATE)?;
> > +
> > + Ok(())
> > + }
> > +
> > + fn as_start_update(&mut self, as_nr: usize, region: &Range<u64>) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > +
> > + // The locked region is a naturally aligned power of 2 block encoded as
> > + // log2 minus(1).
> > + //
> > + // Calculate the desired start/end and look for the highest bit which
> > + // differs. The smallest naturally aligned block must include this bit
> > + // change, the desired region starts with this bit (and subsequent bits)
> > + // zeroed and ends with the bit (and subsequent bits) set to one.
> > + let region_width = core::cmp::max(
> > + 64 - (region.start ^ (region.end - 1)).leading_zeros() as u8,
> > + AS_LOCK_REGION_MIN_SIZE.trailing_zeros() as u8,
> > + ) - 1;
>
> Is it me, or did this change from the prototype branch?
>
Yes, I fixed an error in the calculation of the locking region width.
If you look at the panthor_mmu.c calculation it is:
region_width = max(fls64(region_start ^ (region_end - 1)),
const_ilog2(AS_LOCK_REGION_MIN_SIZE)) - 1;
The prototype code misinterpreted C's fls64() function. It was treating leading_zeros() directly
as a bit position, when it should be "64 - leading_zeros()".
Before this fix we had giant lock sizes 51 bits long, but now we have reasonable 32KB lock sizes.
> > +
> > + // Mask off the low bits of region.start, which would be ignored by the
> > + // hardware anyways.
> > + let region_start =
> > + region.start & genmask_checked_u64(u32::from(region_width)..=63).ok_or(EINVAL)?;
> > +
> > + let region = (u64::from(region_width)) | region_start;
> > +
> > + let region_lo = (region & 0xffffffff) as u32;
> > + let region_hi = (region >> 32) as u32;
> > +
> > + // Lock the region that needs to be updated.
> > + let dev = self.dev();
> > + as_lockaddr_lo(as_nr)?.write(dev, &self.iomem, region_lo)?;
> > + as_lockaddr_hi(as_nr)?.write(dev, &self.iomem, region_hi)?;
> > +
> > + self.as_send_cmd(as_nr, AS_COMMAND_LOCK)
> > + }
> > +
> > + fn as_end_update(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > + self.as_send_cmd_and_wait(as_nr, AS_COMMAND_FLUSH_PT)
> > + }
> > +
> > + fn as_flush(&mut self, as_nr: usize) -> Result {
> > + self.validate_as_slot(as_nr)?;
> > + self.as_send_cmd(as_nr, AS_COMMAND_FLUSH_PT)
> > + }
> > +}
> > +
> > +impl AsSlotManager {
> > + /// Locks a region for page table updates if the VM has an active slot.
> > + pub(super) fn start_vm_update(&mut self, vm: &VmAsData, region: &Range<u64>) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_start_update(as_nr, region)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page table updates for a VM if it has an active slot.
> > + pub(super) fn end_vm_update(&mut self, vm: &VmAsData) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_end_update(as_nr)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn flush_vm(&mut self, vm: &VmAsData) -> Result {
> > + let seat = vm.as_seat.access(self);
> > + match seat.slot() {
> > + Some(slot) => {
> > + let as_nr = slot as usize;
> > + self.as_flush(as_nr)
> > + }
> > + _ => Ok(()),
> > + }
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn activate_vm(&mut self, vm: ArcBorrow<'_, VmAsData>) -> Result {
> > + self.activate(&vm.as_seat, vm.into())
> > + }
> > +
> > + /// Flushes page tables for a VM if it has an active slot.
> > + pub(super) fn deactivate_vm(&mut self, vm: &VmAsData) -> Result {
> > + self.evict(&vm.as_seat)
> > + }
> > +}
> > diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> > index f54b997355e0..ae435c7e80b1 100644
> > --- a/drivers/gpu/drm/tyr/tyr.rs
> > +++ b/drivers/gpu/drm/tyr/tyr.rs
> > @@ -11,6 +11,7 @@
> > mod file;
> > mod gem;
> > mod gpu;
> > +mod mmu;
> > mod regs;
> > mod slot;
> >
> > --
> > 2.52.0
> >
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 08/12] drm/tyr: add MMU module
2026-02-21 11:20 ` Alice Ryhl
@ 2026-02-28 0:49 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:49 UTC (permalink / raw)
To: Alice Ryhl
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Sat, Feb 21, 2026 at 11:20:17AM +0000, Alice Ryhl wrote:
> On Wed, Feb 11, 2026 at 05:37:09PM -0800, Deborah Brouwer wrote:
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Add a Memory Management Unit (MMU) driver for Tyr. The MMU wraps a
> > SlotManager for allocating hardware address space slots. The underlying
> > AddressSpaceManager performs MMU operations including enabling/disabling
> > address spaces, flushing page tables, and locking regions for page table
> > updates.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
>
> > +/// Any resource/information that will be used by the AddressSpaceManager
> > +/// to make a VM active is present in VmAsData.
> > +///
> > +/// On activation, we will pass an Arc<VmAsData> that will be stored in
> > +/// the slot to make sure the page table and the underlying resources
> > +/// (pages) used by the AS slot won't go away while the MMU points to
> > +/// those.
> > +pub(crate) struct VmAsData {
> > + /// Tracks this VM's binding to a hardware address space slot.
> > + as_seat: LockedBy<Seat, AsSlotManager>,
> > + /// Hardware configuration for this address space.
> > + as_config: AddressSpaceConfig,
> > + /// Page table (managed by devres).
> > + pub(crate) page_table: Pin<KBox<Devres<IoPageTable<ARM64LPAES1>>>>,
>
> I don't think the Box is needed if you #[pin] this field.
Ok, for v2, I moved the page table initialization from struct Vm to struct VmAsData
in the address_space.rs which allows us to construct it in place and #[pin] it.
>
> Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 09/12] drm/tyr: add GPU virtual memory module
2026-02-12 10:54 ` Boris Brezillon
@ 2026-02-28 0:52 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 0:52 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 11:54:33AM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:10 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > From: Boris Brezillon <boris.brezillon@collabora.com>
> >
> > Add GPU virtual address space management using the DRM GPUVM framework.
> > Each virtual memory (VM) space is backed by ARM64 LPAE Stage 1 page tables
> > and can be mapped into hardware address space (AS) slots for GPU execution.
> >
> > The implementation provides memory isolation and virtual address
> > allocation. VMs support mapping GEM buffer objects with configurable
> > protection flags (readonly, noexec, uncached) and handle both 4KB and 2MB
> > page sizes.
> >
> > The vm module integrates with the MMU for address space activation and
> > provides map/unmap/remap operations with page table synchronization.
> >
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Co-developed-by: Daniel Almeida <daniel.almeida@collabora.com>
> > Signed-off-by: Daniel Almeida <daniel.almeida@collabora.com>
> > Co-developed-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/gem.rs | 1 -
> > drivers/gpu/drm/tyr/gpu.rs | 1 -
> > drivers/gpu/drm/tyr/tyr.rs | 1 +
> > drivers/gpu/drm/tyr/vm.rs | 783 +++++++++++++++++++++++++++++++++++++
> > 4 files changed, 784 insertions(+), 2 deletions(-)
> > create mode 100644 drivers/gpu/drm/tyr/vm.rs
> >
> > diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> > index 6a58f2da88d3..111acf33993f 100644
> > --- a/drivers/gpu/drm/tyr/gem.rs
> > +++ b/drivers/gpu/drm/tyr/gem.rs
> > @@ -48,7 +48,6 @@ fn new<Ctx: DeviceContext>(
> > pub(crate) type Bo = gem::shmem::Object<BoData>;
> >
> > /// Creates a dummy GEM object to serve as the root of a GPUVM.
> > -#[expect(dead_code)]
> > pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) -> Result<ARef<Bo>> {
> > let bo = gem::shmem::Object::<BoData>::new(
> > ddev,
> > diff --git a/drivers/gpu/drm/tyr/gpu.rs b/drivers/gpu/drm/tyr/gpu.rs
> > index b5f11bc96fa0..f5e7086ff73c 100644
> > --- a/drivers/gpu/drm/tyr/gpu.rs
> > +++ b/drivers/gpu/drm/tyr/gpu.rs
> > @@ -135,7 +135,6 @@ pub(crate) fn log(&self, pdev: &platform::Device) {
> > }
> >
> > /// Returns the number of virtual address bits supported by the GPU.
> > - #[expect(dead_code)]
> > pub(crate) fn va_bits(&self) -> u32 {
> > self.mmu_features & genmask_u32(0..=7)
> > }
> > diff --git a/drivers/gpu/drm/tyr/tyr.rs b/drivers/gpu/drm/tyr/tyr.rs
> > index ae435c7e80b1..8e73db3a080a 100644
> > --- a/drivers/gpu/drm/tyr/tyr.rs
> > +++ b/drivers/gpu/drm/tyr/tyr.rs
> > @@ -14,6 +14,7 @@
> > mod mmu;
> > mod regs;
> > mod slot;
> > +mod vm;
> >
> > kernel::module_platform_driver! {
> > type: TyrPlatformDeviceData,
> > diff --git a/drivers/gpu/drm/tyr/vm.rs b/drivers/gpu/drm/tyr/vm.rs
> > new file mode 100644
> > index 000000000000..806bc4e587d6
> > --- /dev/null
> > +++ b/drivers/gpu/drm/tyr/vm.rs
> > @@ -0,0 +1,783 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +
> > +//! GPU virtual memory management using the DRM GPUVM framework.
> > +//!
> > +//! This module manages GPU virtual address spaces, providing memory isolation and
> > +//! the illusion of owning the entire VA range, similar to CPU virtual memory. Each
> > +//! VM is backed by ARM64 LPAE Stage 1 page tables and can be mapped into hardware
> > +//! address space (AS) slots for GPU execution.
> > +#![allow(dead_code)]
> > +
> > +use core::ops::Range;
> > +
> > +use kernel::{
> > + alloc::KBox,
> > + c_str,
> > + drm::{
> > + gpuvm::{
> > + DriverGpuVm,
> > + GpuVaAlloc,
> > + GpuVm,
> > + GpuVmBoRegistered,
> > + GpuVmCore,
> > + OpMap,
> > + OpMapRequest,
> > + OpMapped,
> > + OpRemap,
> > + OpRemapped,
> > + OpUnmap,
> > + OpUnmapped, //
> > + },
> > + DeviceContext, //
> > + },
> > + impl_flags,
> > + iommu::pgtable::{
> > + prot,
> > + Config,
> > + IoPageTable,
> > + ARM64LPAES1, //
> > + },
> > + new_mutex,
> > + platform,
> > + prelude::*,
> > + sizes::{
> > + SZ_1G,
> > + SZ_2M,
> > + SZ_4K, //
> > + },
> > + sync::{
> > + aref::ARef,
> > + Arc,
> > + ArcBorrow,
> > + Mutex, //
> > + },
> > + uapi, //
> > +};
> > +
> > +use crate::{
> > + driver::{
> > + TyrDrmDevice,
> > + TyrDrmDriver, //
> > + },
> > + gem,
> > + gem::Bo,
> > + gpu::GpuInfo,
> > + mmu::{
> > + address_space::*,
> > + Mmu, //
> > + },
> > + regs::*, //
> > +};
> > +
> > +impl_flags!(
> > + #[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
> > + pub(crate) struct VmMapFlags(u32);
>
> Missing docs. I'll stop pointing those out, and let you check any
> undocumented pub struct/enum/field/function in v2. I'm here to help if
> you're struggling to find a good description.
Ack, I will include extensive docs for v2.
>
> > +
> > + #[derive(Debug, Clone, Copy, PartialEq, Eq)]
> > + pub(crate) enum VmFlag {
> > + Readonly = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY as u32,
> > + Noexec = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC as u32,
> > + Uncached = uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED as u32,
> > + }
> > +);
> > +
> > +impl VmMapFlags {
> > + /// Convert the flags to `pgtable::prot`.
> > + fn to_prot(self) -> u32 {
> > + let mut prot = 0;
> > +
> > + if self.contains(VmFlag::Readonly) {
> > + prot |= prot::READ;
> > + } else {
> > + prot |= prot::READ | prot::WRITE;
> > + }
> > +
> > + if self.contains(VmFlag::Noexec) {
> > + prot |= prot::NOEXEC;
> > + }
> > +
> > + if !self.contains(VmFlag::Uncached) {
> > + prot |= prot::CACHE;
> > + }
> > +
> > + prot
> > + }
> > +}
> > +
> > +impl core::fmt::Display for VmMapFlags {
> > + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
> > + let mut first = true;
> > +
> > + if self.contains(VmFlag::Readonly) {
> > + write!(f, "READONLY")?;
> > + first = false;
> > + }
> > + if self.contains(VmFlag::Noexec) {
> > + if !first {
> > + write!(f, " | ")?;
> > + }
> > + write!(f, "NOEXEC")?;
> > + first = false;
> > + }
> > +
> > + if self.contains(VmFlag::Uncached) {
> > + if !first {
> > + write!(f, " | ")?;
> > + }
> > + write!(f, "UNCACHED")?;
> > + }
> > +
> > + Ok(())
> > + }
> > +}
> > +
> > +impl TryFrom<u32> for VmMapFlags {
> > + type Error = Error;
> > +
> > + fn try_from(value: u32) -> core::result::Result<Self, Self::Error> {
> > + let valid = (kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_READONLY
> > + | kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_NOEXEC
> > + | kernel::uapi::drm_panthor_vm_bind_op_flags_DRM_PANTHOR_VM_BIND_OP_MAP_UNCACHED)
> > + as u32;
> > +
> > + if value & !valid != 0 {
> > + pr_err!("Invalid VM map flags: {:#x}\n", value);
> > + return Err(EINVAL);
> > + }
> > + Ok(Self(value))
> > + }
> > +}
> > +
> > +struct VmMapArgs {
> > + flags: VmMapFlags,
> > + vm_bo: GpuVmBoRegistered<GpuVmData>,
> > + bo_offset: u64,
> > +}
> > +
> > +enum VmOpType {
> > + Map(VmMapArgs),
> > + Unmap,
> > +}
> > +
> > +struct VmOpResources {
> > + /// This handles the remap case.
> > + ///
> > + /// Partial unmap requests or map requests overlapping existing mappings
> > + /// will trigger a remap call, which needs to register up to three VA
> > + /// objects (one for the new mapping, and two for the previous and next
> > + /// mappings).
> > + preallocated_gpuvas: [Option<GpuVaAlloc<GpuVmData>>; 3],
> > +}
> > +
> > +struct VmOpRequest {
> > + /// Request type.
> > + op_type: VmOpType,
> > +
> > + /// Region of the virtual address space covered by this request.
> > + region: Range<u64>,
> > +}
> > +
> > +struct PtMapArgs {
> > + /// Flags describing authorized accesses for this mapping.
> > + ///
> > + /// This is directly derived from the VmMapFlags.
> > + prot: u32,
> > +}
> > +
> > +enum PtOpType {
> > + Map(PtMapArgs),
> > + Unmap,
> > +}
> > +
> > +pub(crate) struct PtUpdateContext<'ctx> {
> > + /// Page table.
> > + pt: &'ctx IoPageTable<ARM64LPAES1>,
> > +
> > + /// MMU
> > + mmu: &'ctx Mmu,
> > +
> > + /// Reference to the AS data to pass to the MMU functions
> > + as_data: &'ctx VmAsData,
> > +
> > + /// Region of the virtual address space covered by this request.
> > + region: Range<u64>,
> > +
> > + /// Operation type.
> > + op_type: PtOpType,
> > +
> > + /// Pre-allocated resources that can be used when executing the request.
> > + resources: &'ctx mut VmOpResources,
> > +}
> > +
> > +impl<'ctx> PtUpdateContext<'ctx> {
> > + fn new(
> > + pt: &'ctx IoPageTable<ARM64LPAES1>,
> > + mmu: &'ctx Mmu,
> > + as_data: &'ctx VmAsData,
> > + region: Range<u64>,
> > + op_type: PtOpType,
> > + resources: &'ctx mut VmOpResources,
> > + ) -> Result<PtUpdateContext<'ctx>> {
> > + mmu.start_vm_update(as_data, ®ion)?;
> > +
> > + Ok(Self {
> > + pt,
> > + mmu,
> > + as_data,
> > + region,
> > + op_type,
> > + resources,
> > + })
> > + }
> > +
> > + /// Finds one of our pre-allocated VAs.
> > + ///
> > + /// It is a logic error to call this more than three times for a given
> > + /// PtUpdateContext.
> > + fn preallocated_gpuva(&mut self) -> Result<GpuVaAlloc<GpuVmData>> {
> > + self.resources
> > + .preallocated_gpuvas
> > + .iter_mut()
> > + .find_map(|f| f.take())
> > + .ok_or(EINVAL)
> > + }
> > +}
> > +
> > +impl Drop for PtUpdateContext<'_> {
> > + fn drop(&mut self) {
> > + if let Err(e) = self.mmu.end_vm_update(self.as_data) {
> > + pr_err!("Failed to end VM update {:?}\n", e);
> > + }
> > +
> > + if let Err(e) = self.mmu.flush_vm(self.as_data) {
> > + pr_err!("Failed to flush VM {:?}\n", e);
> > + }
> > + }
> > +}
> > +
> > +pub(crate) struct GpuVmData {}
> > +
> > +/// GPU virtual address space.
> > +///
> > +/// Each VM can be mapped into a hardware address space slot.
> > +#[pin_data]
> > +pub(crate) struct Vm {
> > + /// Data referenced by an AS when the VM is active
> > + as_data: Arc<VmAsData>,
> > + /// MMU manager.
> > + mmu: Arc<Mmu>,
> > + /// Platform device reference (needed to access the page table through devres).
> > + pdev: ARef<platform::Device>,
> > + /// DRM GPUVM core for managing virtual address space.
> > + #[pin]
> > + gpuvm_core: Mutex<GpuVmCore<GpuVmData>>,
> > + /// Non-core part of the GPUVM. Can be used for stuff that doesn't modify the
> > + /// internal mapping tree, like GpuVm::obtain()
> > + gpuvm: ARef<GpuVm<GpuVmData>>,
> > + /// VA range for this VM.
> > + va_range: Range<u64>,
> > +}
> > +
> > +impl Vm {
> > + pub(crate) fn new<Ctx: DeviceContext>(
> > + pdev: &platform::Device,
> > + ddev: &TyrDrmDevice<Ctx>,
> > + mmu: ArcBorrow<'_, Mmu>,
> > + gpu_info: &GpuInfo,
> > + ) -> Result<Arc<Vm>> {
> > + let va_bits = gpu_info.va_bits();
> > + let pa_bits = gpu_info.pa_bits();
> > +
> > + let pt_config = Config {
> > + quirks: 0,
> > + pgsize_bitmap: SZ_4K | SZ_2M,
> > + ias: va_bits,
> > + oas: pa_bits,
> > + coherent_walk: false,
> > + };
> > +
> > + // SAFETY: pdev is a bound device.
> > + let dev = unsafe { pdev.as_ref().as_bound() };
> > + let page_table_init = IoPageTable::new(dev, pt_config);
> > + let page_table = KBox::pin_init(page_table_init, GFP_KERNEL).inspect_err(|e| {
> > + pr_err!("Failed to initialize page table: {:?}\n", e);
> > + })?;
> > + let pt = page_table.access(dev).inspect_err(|e| {
> > + pr_err!("Failed to access page table: {:?}\n", e);
> > + })?;
> > +
> > + let as_config = AddressSpaceConfig {
> > + transcfg: AS_TRANSCFG_PTW_MEMATTR_WB
> > + | AS_TRANSCFG_PTW_RA
> > + | AS_TRANSCFG_ADRMODE_AARCH64_4K
> > + | as_transcfg_ina_bits(u64::from(55 - va_bits)),
> > + // SAFETY: Vm::drop() evicts the address space and performs deferred
> > + // cleanup before dropping the page_table Arc. This ensures that
> > + // the device stops using the page table before it is dropped
> > + transtab: unsafe { pt.ttbr() },
> > + memattr: mair_to_memattr(pt.mair()),
> > + };
> > +
> > + let range = 0..(1u64 << va_bits);
> > + let reserve_range = 0..0u64;
> > +
> > + let dummy_obj = gem::new_dummy_object(ddev).inspect_err(|e| {
> > + pr_err!("Failed to create dummy GEM object: {:?}\n", e);
> > + })?;
> > +
> > + let gpuvm_core = kernel::drm::gpuvm::GpuVm::new::<Error, _>(
> > + c_str!("Tyr::GpuVm"),
> > + ddev,
> > + &*dummy_obj,
> > + range.clone(),
> > + reserve_range,
> > + GpuVmData {},
> > + )
> > + .inspect_err(|e| {
> > + pr_err!("Failed to create GpuVm: {:?}\n", e);
> > + })?;
> > + let gpuvm = ARef::from(&*gpuvm_core);
> > +
> > + let as_data = Arc::new(VmAsData::new(&mmu, as_config, page_table), GFP_KERNEL)?;
> > +
> > + let vm = Arc::pin_init(
> > + pin_init!(Self{
> > + as_data: as_data,
> > + pdev: pdev.into(),
> > + mmu: mmu.into(),
> > + gpuvm: gpuvm,
> > + gpuvm_core <- new_mutex!(gpuvm_core),
> > + va_range: range,
> > + }),
> > + GFP_KERNEL,
> > + )?;
> > +
> > + Ok(vm)
> > + }
> > +
> > + /// Activate the VM in a hardware address space slot.
> > + pub(crate) fn activate(&self) -> Result {
> > + self.mmu
> > + .activate_vm(self.as_data.as_arc_borrow())
> > + .inspect_err(|e| {
> > + pr_err!("Failed to activate VM: {:?}\n", e);
> > + })
> > + }
> > +
> > + /// Deactivate the VM by evicting it from its address space slot.
> > + fn deactivate(&self) -> Result {
> > + self.mmu.deactivate_vm(&self.as_data).inspect_err(|e| {
> > + pr_err!("Failed to deactivate VM: {:?}\n", e);
> > + })
> > + }
> > +
> > + pub(crate) fn kill(&self) {
> > + // TODO: Turn the VM into a state where it can't be used.
>
> Should we address this TODO before it gets merged? I also believe this
> should be done under some lock (the gpuvm lock?) to prevent concurrent
> map/unmap operations.
>
Could you suggest an approach? In our previous drafts I implemented a
bool to track whether the vm was killed, but I don't think that was what
you are thinking of.
> > + let _ = self.deactivate().inspect_err(|e| {
> > + pr_err!("Failed to deactivate VM: {:?}\n", e);
> > + });
> > + let _ = self
> > + .unmap_range(self.va_range.start, self.va_range.end - self.va_range.start)
> > + .inspect_err(|e| {
> > + pr_err!("Failed to unmap range during deactivate: {:?}\n", e);
> > + });
> > + }
> > +
> > + fn exec_op(
> > + &self,
> > + gpuvm_core: &mut GpuVmCore<GpuVmData>,
> > + req: VmOpRequest,
> > + resources: &mut VmOpResources,
> > + ) -> Result {
> > + let pt = self
> > + .as_data
> > + .page_table
> > + // SAFETY: pdev is a bound device.
> > + .access(unsafe { self.pdev.as_ref().as_bound() })
> > + .inspect_err(|e| {
> > + pr_err!("Failed to access page table while mapping pages: {:?}\n", e);
> > + })?;
> > +
> > + match req.op_type {
> > + VmOpType::Map(args) => {
> > + let mut pt_upd = PtUpdateContext::new(
> > + pt,
> > + &self.mmu,
> > + &self.as_data,
> > + req.region,
> > + PtOpType::Map(PtMapArgs {
> > + prot: args.flags.to_prot(),
> > + }),
> > + resources,
> > + )?;
> > +
> > + gpuvm_core.sm_map(OpMapRequest {
> > + addr: pt_upd.region.start,
> > + range: pt_upd.region.end - pt_upd.region.start,
> > + gem_offset: args.bo_offset,
> > + vm_bo: args.vm_bo,
> > + context: &mut pt_upd,
> > + })
> > + //PtUpdateContext drops here flushing the page table
> > + }
> > + VmOpType::Unmap => {
> > + let mut pt_upd = PtUpdateContext::new(
> > + pt,
> > + &self.mmu,
> > + &self.as_data,
> > + req.region,
> > + PtOpType::Unmap,
> > + resources,
> > + )?;
> > +
> > + gpuvm_core.sm_unmap(
> > + pt_upd.region.start,
> > + pt_upd.region.end - pt_upd.region.start,
> > + &mut pt_upd,
> > + )
> > + //PtUpdateContext drops here flushing the page table
> > + }
> > + }
> > + }
> > +
> > + /// Map a GEM object range into the VM.
> > + pub(crate) fn map_bo_range(
> > + &self,
> > + bo: &Bo,
> > + bo_offset: u64,
> > + size: u64,
> > + va: u64,
> > + flags: VmMapFlags,
> > + ) -> Result {
> > + let req = VmOpRequest {
> > + op_type: VmOpType::Map(VmMapArgs {
> > + vm_bo: self.gpuvm.obtain(bo, ())?,
> > + flags,
> > + bo_offset,
> > + }),
> > + region: va..(va + size),
> > + };
> > + let mut resources = VmOpResources {
> > + preallocated_gpuvas: [
> > + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> > + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> > + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> > + ],
> > + };
> > + let mut gpuvm_core = self.gpuvm_core.lock();
> > +
> > + self.exec_op(gpuvm_core.as_mut().get_mut(), req, &mut resources)?;
> > +
> > + // We flush the defer cleanup list now. Things will be different in
> > + // the asynchronous VM_BIND path, where we want the cleanup to
> > + // happen outside the DMA signalling path.
> > + self.gpuvm.deferred_cleanup();
> > + Ok(())
> > + }
> > +
> > + pub(crate) fn unmap_range(&self, va: u64, size: u64) -> Result {
> > + let req = VmOpRequest {
> > + op_type: VmOpType::Unmap,
> > + region: va..(va + size),
> > + };
> > + let mut resources = VmOpResources {
> > + preallocated_gpuvas: [
> > + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> > + Some(GpuVaAlloc::<GpuVmData>::new(GFP_KERNEL)?),
> > + None,
> > + ],
> > + };
> > + let mut gpuvm_core = self.gpuvm_core.lock();
> > +
> > + self.exec_op(gpuvm_core.as_mut().get_mut(), req, &mut resources)?;
> > +
> > + // We flush the defer cleanup list now. Things will be different in
> > + // the asynchronous VM_BIND path, where we want the cleanup to
> > + // happen outside the DMA signalling path.
> > + self.gpuvm.deferred_cleanup();
> > + Ok(())
> > + }
> > +}
> > +
> > +impl DriverGpuVm for GpuVmData {
> > + type Driver = TyrDrmDriver;
> > + type Object = Bo;
> > + type VmBoData = ();
> > + type VaData = ();
> > + type SmContext<'ctx> = PtUpdateContext<'ctx>;
> > +
> > + fn sm_step_map<'op>(
> > + &mut self,
> > + op: OpMap<'op, Self>,
> > + context: &mut Self::SmContext<'_>,
> > + ) -> Result<OpMapped<'op, Self>, Error> {
> > + let start_iova = op.addr();
> > + let mut iova = start_iova;
> > + let mut bytes_left_to_map = op.length();
> > + let mut gem_offset = op.gem_offset();
> > + let sgt = op.obj().sg_table().inspect_err(|e| {
> > + pr_err!("Failed to get sg_table: {:?}\n", e);
> > + })?;
> > + let prot = match &context.op_type {
> > + PtOpType::Map(args) => args.prot,
> > + _ => {
> > + return Err(EINVAL);
> > + }
> > + };
> > +
> > + for sgt_entry in sgt.iter() {
> > + let mut paddr = sgt_entry.dma_address();
> > + let mut sgt_entry_length: u64 = sgt_entry.dma_len();
> > +
> > + if bytes_left_to_map == 0 {
> > + break;
> > + }
> > +
> > + if gem_offset > 0 {
> > + // Skip the entire SGT entry if the gem_offset exceeds its length
> > + let skip = sgt_entry_length.min(gem_offset);
> > + paddr += skip;
> > + sgt_entry_length -= skip;
> > + gem_offset -= skip;
> > + }
> > +
> > + if sgt_entry_length == 0 {
> > + continue;
> > + }
> > +
> > + if gem_offset != 0 {
> > + pr_err!("Invalid gem_offset {} in page table mapping.\n", gem_offset);
> > + return Err(EINVAL);
> > + }
> > + let len = sgt_entry_length.min(bytes_left_to_map);
> > +
> > + let segment_mapped = match pt_map(context.pt, iova, paddr, len, prot) {
> > + Ok(segment_mapped) => segment_mapped,
> > + Err(e) => {
> > + // clean up any successful mappings from previous SGT entries.
> > + let total_mapped = iova - start_iova;
> > + if total_mapped > 0 {
> > + pt_unmap(context.pt, start_iova..(start_iova + total_mapped)).ok();
> > + }
> > + return Err(e);
> > + }
> > + };
> > +
> > + // Since there could be a partial mapping, only advance by the actual amount mapped
> > + bytes_left_to_map -= segment_mapped;
> > + iova += segment_mapped;
> > + }
> > +
> > + let gpuva = context.preallocated_gpuva()?;
> > + let op = op.insert(gpuva, pin_init::init_zeroed());
> > +
> > + Ok(op)
> > + }
> > +
> > + fn sm_step_unmap<'op>(
> > + &mut self,
> > + op: OpUnmap<'op, Self>,
> > + context: &mut Self::SmContext<'_>,
> > + ) -> Result<OpUnmapped<'op, Self>, Error> {
> > + let start_iova = op.va().addr();
> > + let length = op.va().length();
> > +
> > + let region = start_iova..(start_iova + length);
> > + pt_unmap(context.pt, region.clone()).inspect_err(|e| {
> > + pr_err!(
> > + "Failed to unmap region {:#x}..{:#x}: {:?}\n",
> > + region.start,
> > + region.end,
> > + e
> > + );
> > + })?;
> > +
> > + let (op_unmapped, _va_removed) = op.remove();
> > +
> > + Ok(op_unmapped)
> > + }
> > +
> > + fn sm_step_remap<'op>(
> > + &mut self,
> > + op: OpRemap<'op, Self>,
> > + context: &mut Self::SmContext<'_>,
> > + ) -> Result<OpRemapped<'op, Self>, Error> {
> > + let unmap_start = if let Some(prev) = op.prev() {
> > + prev.addr() + prev.length()
> > + } else {
> > + op.va_to_unmap().addr()
> > + };
> > +
> > + let unmap_end = if let Some(next) = op.next() {
> > + next.addr()
> > + } else {
> > + op.va_to_unmap().addr() + op.va_to_unmap().length()
> > + };
> > +
> > + let unmap_length = unmap_end - unmap_start;
> > +
> > + if unmap_length > 0 {
> > + let region = unmap_start..(unmap_start + unmap_length);
> > + pt_unmap(context.pt, region.clone()).inspect_err(|e| {
> > + pr_err!(
> > + "Failed to unmap remap region {:#x}..{:#x}: {:?}\n",
> > + region.start,
> > + region.end,
> > + e
> > + );
> > + })?;
> > + }
> > +
> > + let prev_va = context.preallocated_gpuva()?;
> > + let next_va = context.preallocated_gpuva()?;
> > +
> > + let (op_remapped, _remap_ret) = op.remap(
> > + [prev_va, next_va],
> > + pin_init::init_zeroed(),
> > + pin_init::init_zeroed(),
> > + );
> > +
> > + Ok(op_remapped)
> > + }
> > +}
> > +
> > +fn mair_to_memattr(mair: u64) -> u64 {
> > + let mut memattr: u64 = 0;
> > +
> > + for i in 0..8 {
> > + let in_attr = (mair >> (8 * i)) as u8;
> > + let outer = in_attr >> 4;
> > + let inner = in_attr & 0xf;
> > +
> > + // For caching to be enabled, inner and outer caching policy
> > + // have to be both write-back, if one of them is write-through
> > + // or non-cacheable, we just choose non-cacheable. Device
> > + // memory is also translated to non-cacheable.
> > + let out_attr = if (outer & 3 == 0) || (outer & 4 == 0) || (inner & 4 == 0) {
> > + AS_MEMATTR_AARCH64_INNER_OUTER_NC
> > + | AS_MEMATTR_AARCH64_SH_MIDGARD_INNER
> > + | as_memattr_aarch64_inner_alloc_expl(false, false)
> > + } else {
> > + // Use SH_CPU_INNER mode so SH_IS, which is used when
> > + // IOMMU_CACHE is set, actually maps to the standard
> > + // definition of inner-shareable and not Mali's
> > + // internal-shareable mode.
> > + //
> > + // TODO: this assumes a non-coherent system.
> > + AS_MEMATTR_AARCH64_INNER_OUTER_WB
> > + | AS_MEMATTR_AARCH64_SH_MIDGARD_INNER
> > + | as_memattr_aarch64_inner_alloc_expl(inner & 1 != 0, inner & 2 != 0)
> > + };
> > +
> > + memattr |= (u64::from(out_attr)) << (8 * i);
> > + }
> > +
> > + memattr
> > +}
> > +
> > +// We can map multiple pages at once but we can't exceed the size of the
> > +// table entry itself. So, if mapping 4KB pages, figure out how many pages
> > +// can be mapped before we hit the 2MB boundary. Or, if mapping 2MB pages,
> > +// figure out how many pages can be mapped before hitting the 1GB boundary
> > +// Returns the page size (4KB or 2MB) and the number of pages that can be mapped at that size.
> > +fn get_pgsize(addr: u64, size: u64) -> (u64, u64) {
> > + // Get the distance to the next boundary of 2MB block
> > + let blk_offset_2m = addr.wrapping_neg() % (SZ_2M as u64);
> > +
> > + // Use 4K blocks if the address is not 2MB aligned, or we have less than 2MB to map
> > + if blk_offset_2m != 0 || size < SZ_2M as u64 {
> > + let pgcount = if blk_offset_2m == 0 {
> > + size / SZ_4K as u64
> > + } else {
> > + blk_offset_2m.min(size) / SZ_4K as u64
> > + };
> > + return (SZ_4K as u64, pgcount);
> > + }
> > +
> > + let blk_offset_1g = addr.wrapping_neg() % (SZ_1G as u64);
> > + let blk_offset = if blk_offset_1g == 0 {
> > + SZ_1G as u64
> > + } else {
> > + blk_offset_1g
> > + };
> > + let pgcount = blk_offset.min(size) / SZ_2M as u64;
> > +
> > + (SZ_2M as u64, pgcount)
> > +}
> > +
> > +fn pt_map(
> > + pt: &IoPageTable<ARM64LPAES1>,
> > + iova: u64,
> > + paddr: u64,
> > + len: u64,
> > + prot: u32,
> > +) -> Result<u64> {
> > + let mut segment_mapped = 0u64;
> > + while segment_mapped < len {
> > + let remaining = len - segment_mapped;
> > + let curr_iova = iova + segment_mapped;
> > + let curr_paddr = paddr + segment_mapped;
> > +
> > + let (pgsize, pgcount) = get_pgsize(curr_iova | curr_paddr, remaining);
> > +
> > + // SAFETY: Exclusive access to the page table is ensured because
> > + // the pt reference comes from PtUpdateContext, which was
> > + // created while holding &mut Vm, preventing any other access to the
> > + // page table for the duration of this operation.
> > + let (mapped, result) = unsafe {
> > + pt.map_pages(
> > + curr_iova as usize,
> > + (curr_paddr as usize).try_into().unwrap(),
> > + pgsize as usize,
> > + pgcount as usize,
> > + prot,
> > + GFP_KERNEL,
> > + )
> > + };
> > +
> > + if let Err(e) = result {
> > + pr_err!("pt.map_pages failed at iova {:#x}: {:?}\n", curr_iova, e);
> > + if segment_mapped > 0 {
> > + pt_unmap(pt, iova..(iova + segment_mapped)).ok();
> > + }
> > + return Err(e);
> > + }
> > +
> > + if mapped == 0 {
> > + pr_err!("Failed to map any pages at iova {:#x}\n", curr_iova);
> > + if segment_mapped > 0 {
> > + pt_unmap(pt, iova..(iova + segment_mapped)).ok();
> > + }
> > + return Err(ENOMEM);
> > + }
> > +
> > + segment_mapped += mapped as u64;
> > + }
> > +
> > + Ok(segment_mapped)
> > +}
> > +
> > +fn pt_unmap(pt: &IoPageTable<ARM64LPAES1>, range: Range<u64>) -> Result {
> > + let mut iova = range.start;
> > + let mut bytes_left_to_unmap = range.end - range.start;
> > +
> > + while bytes_left_to_unmap > 0 {
> > + let (pgsize, pgcount) = get_pgsize(iova, bytes_left_to_unmap);
> > +
> > + // SAFETY: Exclusive access to the page table is ensured because
> > + // the pt reference comes from PtUpdateContext, which was
> > + // created while holding &mut Vm, preventing any other access to the
> > + // page table for the duration of this operation.
> > + let unmapped = unsafe { pt.unmap_pages(iova as usize, pgsize as usize, pgcount as usize) };
> > +
> > + if unmapped == 0 {
> > + pr_err!("Failed to unmap any bytes at iova {:#x}\n", iova);
> > + return Err(EINVAL);
> > + }
> > +
> > + bytes_left_to_unmap -= unmapped as u64;
> > + iova += unmapped as u64;
> > + }
> > +
> > + Ok(())
> > +}
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 10/12] drm/tyr: add a kernel buffer object
2026-02-12 11:00 ` Boris Brezillon
@ 2026-02-28 1:01 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 1:01 UTC (permalink / raw)
To: Boris Brezillon
Cc: dri-devel, rust-for-linux, daniel.almeida, aliceryhl,
beata.michalska, lyude
On Thu, Feb 12, 2026 at 12:00:43PM +0100, Boris Brezillon wrote:
> On Wed, 11 Feb 2026 17:37:11 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > Introduce a buffer object type (KernelBo) for internal driver allocations
> > that are managed by the kernel rather than userspace.
> >
> > KernelBo wraps a GEM shmem object and automatically handles GPU virtual
> > address space mapping during creation and unmapping on drop. This provides
> > a safe and convenient way for the driver to both allocate and clean up
> > internal buffers for kernel-managed resources.
> >
> > Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > ---
> > drivers/gpu/drm/tyr/gem.rs | 74 +++++++++++++++++++++++++++++++++++---
> > 1 file changed, 70 insertions(+), 4 deletions(-)
> >
> > diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> > index 111acf33993f..3807810be7ea 100644
> > --- a/drivers/gpu/drm/tyr/gem.rs
> > +++ b/drivers/gpu/drm/tyr/gem.rs
> > @@ -4,6 +4,8 @@
> > //! This module provides buffer object (BO) management functionality using
> > //! DRM's GEM subsystem with shmem backing.
> >
> > +use core::ops::Range;
> > +
> > use kernel::{
> > drm::{
> > gem,
> > @@ -11,12 +13,22 @@
> > DeviceContext, //
> > },
> > prelude::*,
> > - sync::aref::ARef, //
> > + sync::{
> > + aref::ARef,
> > + Arc,
> > + ArcBorrow, //
> > + },
> > };
> >
> > -use crate::driver::{
> > - TyrDrmDevice,
> > - TyrDrmDriver, //
> > +use crate::{
> > + driver::{
> > + TyrDrmDevice,
> > + TyrDrmDriver, //
> > + },
> > + vm::{
> > + Vm,
> > + VmMapFlags, //
> > + },
> > };
> >
> > /// Tyr's DriverObject type for GEM objects.
> > @@ -61,3 +73,57 @@ pub(crate) fn new_dummy_object<Ctx: DeviceContext>(ddev: &TyrDrmDevice<Ctx>) ->
> >
> > Ok(bo)
> > }
> > +
> > +/// A buffer object that is owned and managed by Tyr rather than userspace.
> > +pub(crate) struct KernelBo {
> > + #[expect(dead_code)]
> > + pub(crate) bo: ARef<Bo>,
> > + vm: Arc<Vm>,
> > + va_range: Range<u64>,
>
> Missing docs for all those fields.
Ack.
>
> > +}
> > +
> > +impl KernelBo {
> > + /// Creates a new kernel-owned buffer object and maps it into GPU VA space.
> > + #[expect(dead_code)]
> > + pub(crate) fn new<Ctx: DeviceContext>(
> > + ddev: &TyrDrmDevice<Ctx>,
> > + vm: ArcBorrow<'_, Vm>,
> > + size: u64,
> > + va: u64,
>
> I'm already thinking about the next step (automatic VA-range
> allocation), and I'd be tempted to go directly for:
>
> enum KernelBoVaAlloc {
> Explicit(u64),
> }
>
> so we can easily extend it with
>
> enum KernelBoVaAlloc {
> Auto,
> Explicit(u64),
> }
>
> when we have to.
>
Sure that is easily done.
> > + flags: VmMapFlags,
> > + ) -> Result<Self> {
> > + let bo = gem::shmem::Object::<BoData>::new(
> > + ddev,
> > + size as usize,
> > + shmem::ObjectConfig {
> > + map_wc: true,
> > + parent_resv_obj: None,
> > + },
> > + BoCreateArgs { flags: 0 },
> > + )?;
> > +
> > + vm.map_bo_range(&bo, 0, size, va, flags)?;
> > +
> > + Ok(KernelBo {
> > + bo,
> > + vm: vm.into(),
> > + va_range: va..(va + size),
> > + })
> > + }
> > +}
> > +
> > +impl Drop for KernelBo {
> > + fn drop(&mut self) {
> > + let va = self.va_range.start;
> > + let size = self.va_range.end - self.va_range.start;
> > +
> > + if let Err(e) = self.vm.unmap_range(va, size) {
> > + pr_err!(
> > + "Failed to unmap KernelBo range {:#x}..{:#x}: {:?}\n",
> > + self.va_range.start,
> > + self.va_range.end,
> > + e
> > + );
> > + }
> > + }
> > +}
>
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support
2026-02-21 11:25 ` Alice Ryhl
@ 2026-02-28 1:02 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-02-28 1:02 UTC (permalink / raw)
To: Alice Ryhl
Cc: dri-devel, rust-for-linux, daniel.almeida, boris.brezillon,
beata.michalska, lyude
On Sat, Feb 21, 2026 at 11:25:00AM +0000, Alice Ryhl wrote:
> On Wed, Feb 11, 2026 at 05:37:13PM -0800, Deborah Brouwer wrote:
> > Add firmware loading and management for the Mali CSF GPU. This introduces
> > the fw module that loads the Mali GPU firmware binary, parses it into
> > sections, and maps those sections into the MCU VM at the required
> > virtual addresses.
> >
> > On probe, the firmware is loaded, its sections are mapped and populated,
> > the MCU VM is activated, and the MCU is booted.
> >
> > Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
>
> > +/// Loaded firmware with sections mapped into MCU VM.
> > +pub(crate) struct Firmware {
> > + /// Platform device reference (needed to access the MCU JOB_IRQ registers).
> > + pdev: ARef<platform::Device>,
> > +
> > + /// Iomem need to access registers.
> > + iomem: Arc<Devres<IoMem>>,
> > +
> > + /// MCU VM.
> > + vm: Arc<Vm>,
> > +
> > + /// List of firmware sections.
> > + #[expect(dead_code)]
> > + sections: KVec<KBox<Section>>,
>
> Why the box?
Hm I don’t actually remember why we originally needed KBox, but we don’t need the section
addresses to stay stable anymore so I’ll remove it.
>
> > + let section_start = parsed.data_range.start as usize;
> > + let section_end = parsed.data_range.end as usize;
> > + let mut data = KVec::new();
> > + data.extend_from_slice(&fw.data()[section_start..section_end], GFP_KERNEL)?;
>
> Could this access be out of bounds?
Yes, that could go out of bounds. I’ll add a defensive bounds check and return
EINVAL if the range is invalid.
>
> Alice
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 07/12] drm/tyr: Add generic slot manager
2026-02-28 0:28 ` Deborah Brouwer
@ 2026-03-02 10:10 ` Boris Brezillon
0 siblings, 0 replies; 63+ messages in thread
From: Boris Brezillon @ 2026-03-02 10:10 UTC (permalink / raw)
To: Deborah Brouwer
Cc: Daniel Almeida, dri-devel, rust-for-linux, aliceryhl,
beata.michalska, lyude
On Fri, 27 Feb 2026 16:28:51 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> > >
> > >>
> > >> access vs access_mut() does not matter here: since the owner is &mut self
> > >> anyways we know we have exclusive access to the LockedSeat throughout the whole
> > >> function.
> > >
> > > I agree, but LockedBy is picky, and last I tried I couldn't make it
> > > work without the annoying update+return-copy-of-seat dance you see
> > > here. Maybe I missed something obvious and it does indeed work with
> > > your suggested changes, dunno.
> >
> > Rewriting things so they pass the borrow checker is common in Rust. Sometimes
> > it can be done rather easily; other times the design is just broken and needs
> > to be reworked. Luckily this one fell in the first category.
> >
> > This benefits from the fact that no one can race us between reading this tuple
> >
> > (slot_idx, seqno, is_active)
> >
> > ..and using it. That’s because we’re taking &mut self as a proxy in LockedBy, so
> > we’re sure we have exclusive access in this scope.
Yep, I know that, but I seem to remember it wasn't working when I was doing
match locked_seat.access(self) {
Seat::Active(seat_info) | Seat::Idle(seat_info) => {
// With lazy eviction, if seqno matches, the hardware state is still
// valid for both Active and Idle slots, so just update our records
self.record_active_slot(seat_info.slot as usize, locked_seat, slot_data)
}
_ => self.allocate_slot(locked_seat, slot_data),
}
in ::activate(), which is why I made check_seat() return a Seat and
did the match against this returned seat. Oh well, I must have
misunderstood the problem reported by the compiler.
> >
> > If you don’t have any complaints about the code I sent (i.e.: convoluted, wrong
> > logic, etc), I suggest switching to it.
>
> I’ve tested the short check_seat() function and can confirm that it correctly
> compares Seat and Slot seqno as well as emits the kernel warning when we have a
> mismatch on an active Seat. So I’ll the simplified check_seat() function in v2
> unless there are any more issues to address.
If things still compile/work as expected, no objection on my end.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-02-28 0:17 ` Deborah Brouwer
@ 2026-03-02 10:17 ` Boris Brezillon
2026-03-02 17:03 ` Deborah Brouwer
0 siblings, 1 reply; 63+ messages in thread
From: Boris Brezillon @ 2026-03-02 10:17 UTC (permalink / raw)
To: Deborah Brouwer, rust-for-linux
Cc: Daniel Almeida, dri-devel, aliceryhl, beata.michalska, lyude
On Fri, 27 Feb 2026 16:17:35 -0800
Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> On Fri, Feb 20, 2026 at 11:25:47AM -0300, Daniel Almeida wrote:
> >
> >
> > > On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> > >
> > > Add support for GEM buffer objects backed by shared memory.
> > >
> > > This introduces the BoCreateArgs structure for passing creation parameters
> > > including flags, and adds a flags field to BoData. A new_dummy_object()
> > > helper is provided to create a dummy GEM object for use as a GPUVM root.
> > >
> > > The Bo type alias is added to simplify working with Tyr's shmem-backed
> > > GEM objects throughout the driver.
> > >
> > > Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> > > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > > ---
> > > drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
> > > 1 file changed, 44 insertions(+), 8 deletions(-)
> > >
> > > diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> > > index c1208d332dea..6a58f2da88d3 100644
> > > --- a/drivers/gpu/drm/tyr/gem.rs
> > > +++ b/drivers/gpu/drm/tyr/gem.rs
> > > @@ -1,28 +1,64 @@
> > > // SPDX-License-Identifier: GPL-2.0 or MIT
> > > +//! GEM buffer object management for the Tyr driver.
> > > +//!
> > > +//! This module provides buffer object (BO) management functionality using
> > > +//! DRM's GEM subsystem with shmem backing.
> > >
> > > use kernel::{
> > > drm::{
> > > gem,
> > > + gem::shmem,
> > > DeviceContext, //
> > > },
> > > - prelude::*, //
> > > + prelude::*,
> > > + sync::aref::ARef, //
> > > };
> > >
> > > -use crate::driver::TyrDrmDriver;
> > > +use crate::driver::{
> > > + TyrDrmDevice,
> > > + TyrDrmDriver, //
> > > +};
> > >
> > > -/// GEM Object inner driver data
> > > +/// Tyr's DriverObject type for GEM objects.
> > > #[pin_data]
> > > -pub(crate) struct BoData {}
> > > +pub(crate) struct BoData {
> > > + flags: u32,
> > > +}
> > > +
> > > +/// Provides a way to pass arguments when creating BoData
> > > +/// as required by the gem::DriverObject trait.
> > > +pub(crate) struct BoCreateArgs {
> > > + flags: u32,
> > > +}
> > >
> > > impl gem::DriverObject for BoData {
> > > type Driver = TyrDrmDriver;
> > > - type Args = ();
> > > + type Args = BoCreateArgs;
> > >
> > > fn new<Ctx: DeviceContext>(
> > > - _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
> > > + _dev: &TyrDrmDevice<Ctx>,
> >
> > Unrelated change?
>
> I switched to use the convenience type alias `TyrDrmDevice<Ctx>`
> here instead of using its full path. I can flag that in the commit
> mesage if that is what you mean?
I'd probably do that in a separate commit, like Daniel suggested, even
if that means introducing a one-line commit just for that cosmetic
change.
^ permalink raw reply [flat|nested] 63+ messages in thread
* Re: [PATCH 06/12] drm/tyr: add shmem backing for GEM objects
2026-03-02 10:17 ` Boris Brezillon
@ 2026-03-02 17:03 ` Deborah Brouwer
0 siblings, 0 replies; 63+ messages in thread
From: Deborah Brouwer @ 2026-03-02 17:03 UTC (permalink / raw)
To: Boris Brezillon
Cc: rust-for-linux, Daniel Almeida, dri-devel, aliceryhl,
beata.michalska, lyude
On Mon, Mar 02, 2026 at 11:17:57AM +0100, Boris Brezillon wrote:
> On Fri, 27 Feb 2026 16:17:35 -0800
> Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
>
> > On Fri, Feb 20, 2026 at 11:25:47AM -0300, Daniel Almeida wrote:
> > >
> > >
> > > > On 11 Feb 2026, at 22:37, Deborah Brouwer <deborah.brouwer@collabora.com> wrote:
> > > >
> > > > Add support for GEM buffer objects backed by shared memory.
> > > >
> > > > This introduces the BoCreateArgs structure for passing creation parameters
> > > > including flags, and adds a flags field to BoData. A new_dummy_object()
> > > > helper is provided to create a dummy GEM object for use as a GPUVM root.
> > > >
> > > > The Bo type alias is added to simplify working with Tyr's shmem-backed
> > > > GEM objects throughout the driver.
> > > >
> > > > Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
> > > > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > > > Signed-off-by: Deborah Brouwer <deborah.brouwer@collabora.com>
> > > > ---
> > > > drivers/gpu/drm/tyr/gem.rs | 52 ++++++++++++++++++++++++++++++++------
> > > > 1 file changed, 44 insertions(+), 8 deletions(-)
> > > >
> > > > diff --git a/drivers/gpu/drm/tyr/gem.rs b/drivers/gpu/drm/tyr/gem.rs
> > > > index c1208d332dea..6a58f2da88d3 100644
> > > > --- a/drivers/gpu/drm/tyr/gem.rs
> > > > +++ b/drivers/gpu/drm/tyr/gem.rs
> > > > @@ -1,28 +1,64 @@
> > > > // SPDX-License-Identifier: GPL-2.0 or MIT
> > > > +//! GEM buffer object management for the Tyr driver.
> > > > +//!
> > > > +//! This module provides buffer object (BO) management functionality using
> > > > +//! DRM's GEM subsystem with shmem backing.
> > > >
> > > > use kernel::{
> > > > drm::{
> > > > gem,
> > > > + gem::shmem,
> > > > DeviceContext, //
> > > > },
> > > > - prelude::*, //
> > > > + prelude::*,
> > > > + sync::aref::ARef, //
> > > > };
> > > >
> > > > -use crate::driver::TyrDrmDriver;
> > > > +use crate::driver::{
> > > > + TyrDrmDevice,
> > > > + TyrDrmDriver, //
> > > > +};
> > > >
> > > > -/// GEM Object inner driver data
> > > > +/// Tyr's DriverObject type for GEM objects.
> > > > #[pin_data]
> > > > -pub(crate) struct BoData {}
> > > > +pub(crate) struct BoData {
> > > > + flags: u32,
> > > > +}
> > > > +
> > > > +/// Provides a way to pass arguments when creating BoData
> > > > +/// as required by the gem::DriverObject trait.
> > > > +pub(crate) struct BoCreateArgs {
> > > > + flags: u32,
> > > > +}
> > > >
> > > > impl gem::DriverObject for BoData {
> > > > type Driver = TyrDrmDriver;
> > > > - type Args = ();
> > > > + type Args = BoCreateArgs;
> > > >
> > > > fn new<Ctx: DeviceContext>(
> > > > - _dev: &kernel::drm::Device<TyrDrmDriver, Ctx>,
> > > > + _dev: &TyrDrmDevice<Ctx>,
> > >
> > > Unrelated change?
> >
> > I switched to use the convenience type alias `TyrDrmDevice<Ctx>`
> > here instead of using its full path. I can flag that in the commit
> > mesage if that is what you mean?
>
> I'd probably do that in a separate commit, like Daniel suggested, even
> if that means introducing a one-line commit just for that cosmetic
> change.
Sure, i'll check to make sure i haven't done this anywhere else, and
gather them all into a separate commit.
^ permalink raw reply [flat|nested] 63+ messages in thread
end of thread, other threads:[~2026-03-02 17:03 UTC | newest]
Thread overview: 63+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-12 1:37 [PATCH 0/12] drm/tyr: firmware loading and MCU boot support Deborah Brouwer
2026-02-12 1:37 ` [PATCH 01/12] drm/tyr: select DRM abstractions in Kconfig Deborah Brouwer
2026-02-12 1:37 ` [PATCH 02/12] drm/tyr: move clock cleanup into Clocks Drop impl Deborah Brouwer
2026-02-12 8:12 ` Boris Brezillon
2026-02-28 0:18 ` Deborah Brouwer
2026-02-20 14:03 ` Daniel Almeida
2026-02-21 9:01 ` Alice Ryhl
2026-02-12 1:37 ` [PATCH 03/12] drm/tyr: rename TyrObject to BoData Deborah Brouwer
2026-02-20 14:04 ` Daniel Almeida
2026-02-21 9:01 ` Alice Ryhl
2026-02-12 1:37 ` [PATCH 04/12] drm/tyr: set DMA mask using GPU physical address Deborah Brouwer
2026-02-12 10:16 ` Boris Brezillon
2026-02-20 14:19 ` Daniel Almeida
2026-02-21 9:03 ` Alice Ryhl
2026-02-12 1:37 ` [PATCH 05/12] drm/tyr: add MMU address space registers Deborah Brouwer
2026-02-12 8:16 ` Boris Brezillon
2026-02-28 0:12 ` Deborah Brouwer
2026-02-20 14:21 ` Daniel Almeida
2026-02-21 9:09 ` Alice Ryhl
2026-02-22 18:13 ` Boris Brezillon
2026-02-28 0:13 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 06/12] drm/tyr: add shmem backing for GEM objects Deborah Brouwer
2026-02-12 8:17 ` Boris Brezillon
2026-02-28 0:15 ` Deborah Brouwer
2026-02-20 14:25 ` Daniel Almeida
2026-02-28 0:17 ` Deborah Brouwer
2026-03-02 10:17 ` Boris Brezillon
2026-03-02 17:03 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 07/12] drm/tyr: Add generic slot manager Deborah Brouwer
2026-02-12 10:11 ` Boris Brezillon
2026-02-12 10:45 ` Miguel Ojeda
2026-02-20 15:25 ` Daniel Almeida
2026-02-20 16:21 ` Boris Brezillon
2026-02-20 16:55 ` Daniel Almeida
2026-02-22 17:57 ` Boris Brezillon
2026-02-22 18:46 ` Daniel Almeida
2026-02-28 0:28 ` Deborah Brouwer
2026-03-02 10:10 ` Boris Brezillon
2026-02-21 11:16 ` Alice Ryhl
2026-02-21 12:44 ` Daniel Almeida
2026-02-21 13:40 ` Alice Ryhl
2026-02-21 13:48 ` Daniel Almeida
2026-02-28 0:25 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 08/12] drm/tyr: add MMU module Deborah Brouwer
2026-02-12 10:44 ` Boris Brezillon
2026-02-28 0:31 ` Deborah Brouwer
2026-02-12 11:05 ` Boris Brezillon
2026-02-20 15:41 ` Daniel Almeida
2026-02-21 11:17 ` Alice Ryhl
2026-02-20 17:11 ` Daniel Almeida
2026-02-28 0:46 ` Deborah Brouwer
2026-02-21 11:20 ` Alice Ryhl
2026-02-28 0:49 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 09/12] drm/tyr: add GPU virtual memory module Deborah Brouwer
2026-02-12 10:54 ` Boris Brezillon
2026-02-28 0:52 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 10/12] drm/tyr: add a kernel buffer object Deborah Brouwer
2026-02-12 11:00 ` Boris Brezillon
2026-02-28 1:01 ` Deborah Brouwer
2026-02-12 1:37 ` [PATCH 11/12] drm/tyr: add parser for firmware binary Deborah Brouwer
2026-02-12 1:37 ` [PATCH 12/12] drm/tyr: add firmware loading and MCU boot support Deborah Brouwer
2026-02-21 11:25 ` Alice Ryhl
2026-02-28 1:02 ` Deborah Brouwer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox