public inbox for drm-ai-reviews@public-inbox.freedesktop.org
 help / color / mirror / Atom feed
From: Boris Brezillon <boris.brezillon@collabora.com>
To: Boris Brezillon <boris.brezillon@collabora.com>,
	Steven Price <steven.price@arm.com>,
	Liviu Dudau <liviu.dudau@arm.com>,
	Adrián Larumbe <adrian.larumbe@collabora.com>
Cc: dri-devel@lists.freedesktop.org, David Airlie <airlied@gmail.com>,
	Simona Vetter <simona@ffwll.ch>, Akash Goel <akash.goel@arm.com>,
	Rob Clark <robin.clark@oss.qualcomm.com>,
	Sean Paul <sean@poorly.run>,
	Konrad Dybcio <konradybcio@kernel.org>,
	Akhil P Oommen <akhilpo@oss.qualcomm.com>,
	Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
	Maxime Ripard <mripard@kernel.org>,
	Thomas Zimmermann <tzimmermann@suse.de>,
	Dmitry Osipenko <dmitry.osipenko@collabora.com>,
	Chris Diamand <chris.diamand@arm.com>,
	Danilo Krummrich <dakr@kernel.org>,
	Matthew Brost <matthew.brost@intel.com>,
	Thomas Hellström <thomas.hellstrom@linux.intel.com>,
	Alice Ryhl <aliceryhl@google.com>, Chia-I Wu <olvaffe@gmail.com>,
	kernel@collabora.com
Subject: [PATCH v5 9/9] drm/panthor: Add a GEM shrinker
Date: Mon,  9 Mar 2026 16:11:19 +0100	[thread overview]
Message-ID: <20260309151119.290217-10-boris.brezillon@collabora.com> (raw)
In-Reply-To: <20260309151119.290217-1-boris.brezillon@collabora.com>

From: Akash Goel <akash.goel@arm.com>

This implementation is losely based on the MSM shrinker, and it's
relying on the drm_gpuvm eviction/validation infrastructure.

Right now we only support swapout/eviction, but we could add an extra
flag to specify when buffer content doesn't need to be preserved to
avoid the swapout/swapin dance.

Locking is a bit of a nightmare, but using _trylock() all the way in
the reclaim path seems to make lockdep happy. And yes, we might be
missing opportunities to reclaim when the system is under heavy GPU
load/heavy memory pressure/heavy GPU VM activity, but that's better
than no reclaim at all.

v2:
- Move gpu_mapped_shared next to the mmapped LRU
- Add a bunch of missing is_[vm_bo,vma]_evicted() tests
- Only test mmap_count to check if a BO is mmaped
- Remove stale comment about shrinker not being a thing
- Allow pin_count to be non-zero in panthor_gem_swapin_locked()
- Fix panthor_gem_sync() to check for BO residency before doing the CPU sync
- Fix the value returned by panthor_gem_shrinker_count() in case some
  memory has been released
- Check drmm_mutex_init() ret code
- Explicitly mention that PANTHOR_GEM_UNRECLAIMABLE is the initial state
  of all BOs

v3:
- Make panthor_gem_try_evict() static
- Collect {A,R}-bs

v4:
- Update the reclaim_state in panthor_gem_mmap()
- Don't reclaim GPU-mapped BOs if can_block() returns false
- Skip evicited vm_bos in panthor_vm_update_bo_reclaim_lru_locked() to
  avoid spurious WARN_ON()s
- Explain why we have to do this
  select_evicted_vma/repopulate_evicted_vma dance

v5:
- Properly report the reclaimable size in panthor_gem_debugfs_print_bos()
- Check panthor_vm_lock_region() errors in
  panthor_vm_evict_bo_mappings_locked()
- Fix lock order inversion (dma_resv_wait_timeout() inside gpuva.lock)

Signed-off-by: Akash Goel <akash.goel@arm.com>
Co-developed-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
Acked-by: Liviu Dudau <liviu.dudau@arm.com>
Reviewed-by: Steven Price <steven.price@arm.com>
---
 drivers/gpu/drm/panthor/panthor_device.c |  11 +-
 drivers/gpu/drm/panthor/panthor_device.h |  73 ++++
 drivers/gpu/drm/panthor/panthor_gem.c    | 468 ++++++++++++++++++++++-
 drivers/gpu/drm/panthor/panthor_gem.h    |  68 ++++
 drivers/gpu/drm/panthor/panthor_mmu.c    | 359 ++++++++++++++++-
 drivers/gpu/drm/panthor/panthor_mmu.h    |   8 +
 6 files changed, 960 insertions(+), 27 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index 54fbb1aa07c5..bc62a498a8a8 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -2,6 +2,7 @@
 /* Copyright 2018 Marty E. Plummer <hanetzer@startmail.com> */
 /* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
 /* Copyright 2023 Collabora ltd. */
+/* Copyright 2025 ARM Limited. All rights reserved. */
 
 #include <linux/clk.h>
 #include <linux/mm.h>
@@ -122,6 +123,7 @@ void panthor_device_unplug(struct panthor_device *ptdev)
 	panthor_sched_unplug(ptdev);
 	panthor_fw_unplug(ptdev);
 	panthor_mmu_unplug(ptdev);
+	panthor_gem_shrinker_unplug(ptdev);
 	panthor_gpu_unplug(ptdev);
 	panthor_pwr_unplug(ptdev);
 
@@ -291,10 +293,14 @@ int panthor_device_init(struct panthor_device *ptdev)
 	if (ret)
 		goto err_unplug_gpu;
 
-	ret = panthor_mmu_init(ptdev);
+	ret = panthor_gem_shrinker_init(ptdev);
 	if (ret)
 		goto err_unplug_gpu;
 
+	ret = panthor_mmu_init(ptdev);
+	if (ret)
+		goto err_unplug_shrinker;
+
 	ret = panthor_fw_init(ptdev);
 	if (ret)
 		goto err_unplug_mmu;
@@ -326,6 +332,9 @@ int panthor_device_init(struct panthor_device *ptdev)
 err_unplug_mmu:
 	panthor_mmu_unplug(ptdev);
 
+err_unplug_shrinker:
+	panthor_gem_shrinker_unplug(ptdev);
+
 err_unplug_gpu:
 	panthor_gpu_unplug(ptdev);
 
diff --git a/drivers/gpu/drm/panthor/panthor_device.h b/drivers/gpu/drm/panthor/panthor_device.h
index b6696f73a536..5cba272f9b4d 100644
--- a/drivers/gpu/drm/panthor/panthor_device.h
+++ b/drivers/gpu/drm/panthor/panthor_device.h
@@ -14,6 +14,7 @@
 #include <linux/spinlock.h>
 
 #include <drm/drm_device.h>
+#include <drm/drm_gem.h>
 #include <drm/drm_mm.h>
 #include <drm/gpu_scheduler.h>
 #include <drm/panthor_drm.h>
@@ -178,6 +179,78 @@ struct panthor_device {
 	/** @devfreq: Device frequency scaling management data. */
 	struct panthor_devfreq *devfreq;
 
+	/** @reclaim: Reclaim related stuff */
+	struct {
+		/** @reclaim.shrinker: Shrinker instance */
+		struct shrinker *shrinker;
+
+		/** @reclaim.lock: Lock protecting all LRUs */
+		struct mutex lock;
+
+		/**
+		 * @reclaim.unused: BOs with unused pages
+		 *
+		 * Basically all buffers that got mmapped, vmapped or GPU mapped and
+		 * then unmapped. There should be no contention on these buffers,
+		 * making them ideal to reclaim.
+		 */
+		struct drm_gem_lru unused;
+
+		/**
+		 * @reclaim.mmapped: mmap()-ed buffers
+		 *
+		 * Those are relatively easy to reclaim since we don't need user
+		 * agreement, we can simply teardown the mapping and let it fault on
+		 * the next access.
+		 */
+		struct drm_gem_lru mmapped;
+
+		/**
+		 * @reclaim.gpu_mapped_shared: shared BO LRU list
+		 *
+		 * That's the most tricky BO type to reclaim, because it involves
+		 * tearing down all mappings in all VMs where this BO is mapped,
+		 * which increases the risk of contention and thus decreases the
+		 * likeliness of success.
+		 */
+		struct drm_gem_lru gpu_mapped_shared;
+
+		/**
+		 * @reclaim.vms: VM LRU list
+		 *
+		 * VMs that have reclaimable BOs only mapped to a single VM are placed
+		 * in this LRU. Reclaiming such BOs implies waiting for VM idleness
+		 * (no in-flight GPU jobs targeting this VM), meaning we can't reclaim
+		 * those if we're in a context where we can't block/sleep.
+		 */
+		struct list_head vms;
+
+		/**
+		 * @reclaim.gpu_mapped_count: Global counter of pages that are GPU mapped
+		 *
+		 * Allows us to get the number of reclaimable pages without walking
+		 * the vms and gpu_mapped_shared LRUs.
+		 */
+		long gpu_mapped_count;
+
+		/**
+		 * @reclaim.retry_count: Number of times we ran the shrinker without being
+		 * able to reclaim stuff
+		 *
+		 * Used to stop scanning GEMs when too many attempts were made
+		 * without progress.
+		 */
+		atomic_t retry_count;
+
+#ifdef CONFIG_DEBUG_FS
+		/**
+		 * @reclaim.nr_pages_reclaimed_on_last_scan: Number of pages reclaimed on the last
+		 * shrinker scan
+		 */
+		unsigned long nr_pages_reclaimed_on_last_scan;
+#endif
+	} reclaim;
+
 	/** @unplug: Device unplug related fields. */
 	struct {
 		/** @lock: Lock used to serialize unplug operations. */
diff --git a/drivers/gpu/drm/panthor/panthor_gem.c b/drivers/gpu/drm/panthor/panthor_gem.c
index 7c127d908c6f..855ce378afce 100644
--- a/drivers/gpu/drm/panthor/panthor_gem.c
+++ b/drivers/gpu/drm/panthor/panthor_gem.c
@@ -2,8 +2,10 @@
 /* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
 /* Copyright 2023 Collabora ltd. */
 /* Copyright 2025 Amazon.com, Inc. or its affiliates */
+/* Copyright 2025 ARM Limited. All rights reserved. */
 
 #include <linux/cleanup.h>
+#include <linux/debugfs.h>
 #include <linux/dma-buf.h>
 #include <linux/dma-mapping.h>
 #include <linux/err.h>
@@ -12,6 +14,8 @@
 
 #include <drm/drm_debugfs.h>
 #include <drm/drm_file.h>
+#include <drm/drm_gpuvm.h>
+#include <drm/drm_managed.h>
 #include <drm/drm_prime.h>
 #include <drm/drm_print.h>
 #include <drm/panthor_drm.h>
@@ -114,6 +118,103 @@ should_map_wc(struct panthor_gem_object *bo)
 	return true;
 }
 
+static bool is_gpu_mapped(struct panthor_gem_object *bo,
+			  enum panthor_gem_reclaim_state *state)
+{
+	struct drm_gpuvm *vm = NULL;
+	struct drm_gpuvm_bo *vm_bo;
+
+	drm_gem_for_each_gpuvm_bo(vm_bo, &bo->base) {
+		/* Skip evicted GPU mappings. */
+		if (vm_bo->evicted)
+			continue;
+
+		if (!vm) {
+			*state = PANTHOR_GEM_GPU_MAPPED_PRIVATE;
+			vm = vm_bo->vm;
+		} else if (vm != vm_bo->vm) {
+			*state = PANTHOR_GEM_GPU_MAPPED_SHARED;
+			break;
+		}
+	}
+
+	return !!vm;
+}
+
+static enum panthor_gem_reclaim_state
+panthor_gem_evaluate_reclaim_state_locked(struct panthor_gem_object *bo)
+{
+	enum panthor_gem_reclaim_state gpu_mapped_state;
+
+	dma_resv_assert_held(bo->base.resv);
+	lockdep_assert_held(&bo->base.gpuva.lock);
+
+	/* If pages have not been allocated, there's nothing to reclaim. */
+	if (!bo->backing.pages)
+		return PANTHOR_GEM_UNRECLAIMABLE;
+
+	/* If memory is pinned, we prevent reclaim. */
+	if (refcount_read(&bo->backing.pin_count))
+		return PANTHOR_GEM_UNRECLAIMABLE;
+
+	if (is_gpu_mapped(bo, &gpu_mapped_state))
+		return gpu_mapped_state;
+
+	if (refcount_read(&bo->cmap.mmap_count))
+		return PANTHOR_GEM_MMAPPED;
+
+	return PANTHOR_GEM_UNUSED;
+}
+
+void panthor_gem_update_reclaim_state_locked(struct panthor_gem_object *bo,
+					     enum panthor_gem_reclaim_state *old_statep)
+{
+	struct panthor_device *ptdev = container_of(bo->base.dev, struct panthor_device, base);
+	enum panthor_gem_reclaim_state old_state = bo->reclaim_state;
+	enum panthor_gem_reclaim_state new_state;
+	bool was_gpu_mapped, is_gpu_mapped;
+
+	if (old_statep)
+		*old_statep = old_state;
+
+	new_state = panthor_gem_evaluate_reclaim_state_locked(bo);
+	if (new_state == old_state)
+		return;
+
+	was_gpu_mapped = old_state == PANTHOR_GEM_GPU_MAPPED_SHARED ||
+			 old_state == PANTHOR_GEM_GPU_MAPPED_PRIVATE;
+	is_gpu_mapped = new_state == PANTHOR_GEM_GPU_MAPPED_SHARED ||
+			new_state == PANTHOR_GEM_GPU_MAPPED_PRIVATE;
+
+	if (is_gpu_mapped && !was_gpu_mapped)
+		ptdev->reclaim.gpu_mapped_count += bo->base.size >> PAGE_SHIFT;
+	else if (!is_gpu_mapped && was_gpu_mapped)
+		ptdev->reclaim.gpu_mapped_count -= bo->base.size >> PAGE_SHIFT;
+
+	switch (new_state) {
+	case PANTHOR_GEM_UNUSED:
+		drm_gem_lru_move_tail(&ptdev->reclaim.unused, &bo->base);
+		break;
+	case PANTHOR_GEM_MMAPPED:
+		drm_gem_lru_move_tail(&ptdev->reclaim.mmapped, &bo->base);
+		break;
+	case PANTHOR_GEM_GPU_MAPPED_PRIVATE:
+		panthor_vm_update_bo_reclaim_lru_locked(bo);
+		break;
+	case PANTHOR_GEM_GPU_MAPPED_SHARED:
+		drm_gem_lru_move_tail(&ptdev->reclaim.gpu_mapped_shared, &bo->base);
+		break;
+	case PANTHOR_GEM_UNRECLAIMABLE:
+		drm_gem_lru_remove(&bo->base);
+		break;
+	default:
+		drm_WARN(&ptdev->base, true, "invalid GEM reclaim state (%d)\n", new_state);
+		break;
+	}
+
+	bo->reclaim_state = new_state;
+}
+
 static void
 panthor_gem_backing_cleanup_locked(struct panthor_gem_object *bo)
 {
@@ -157,8 +258,12 @@ static int panthor_gem_backing_pin_locked(struct panthor_gem_object *bo)
 		return 0;
 
 	ret = panthor_gem_backing_get_pages_locked(bo);
-	if (!ret)
+	if (!ret) {
 		refcount_set(&bo->backing.pin_count, 1);
+		mutex_lock(&bo->base.gpuva.lock);
+		panthor_gem_update_reclaim_state_locked(bo, NULL);
+		mutex_unlock(&bo->base.gpuva.lock);
+	}
 
 	return ret;
 }
@@ -172,6 +277,9 @@ static void panthor_gem_backing_unpin_locked(struct panthor_gem_object *bo)
 		/* We don't release anything when pin_count drops to zero.
 		 * Pages stay there until an explicit cleanup is requested.
 		 */
+		mutex_lock(&bo->base.gpuva.lock);
+		panthor_gem_update_reclaim_state_locked(bo, NULL);
+		mutex_unlock(&bo->base.gpuva.lock);
 	}
 }
 
@@ -535,6 +643,46 @@ void panthor_gem_unpin(struct panthor_gem_object *bo)
 	dma_resv_unlock(bo->base.resv);
 }
 
+int panthor_gem_swapin_locked(struct panthor_gem_object *bo)
+{
+	struct sg_table *sgt;
+	int ret;
+
+	dma_resv_assert_held(bo->base.resv);
+
+	if (drm_WARN_ON_ONCE(bo->base.dev, drm_gem_is_imported(&bo->base)))
+		return -EINVAL;
+
+	ret = panthor_gem_backing_get_pages_locked(bo);
+	if (ret)
+		return ret;
+
+	sgt = panthor_gem_dev_map_get_sgt_locked(bo);
+	if (IS_ERR(sgt))
+		return PTR_ERR(sgt);
+
+	return 0;
+}
+
+static void panthor_gem_evict_locked(struct panthor_gem_object *bo)
+{
+	dma_resv_assert_held(bo->base.resv);
+	lockdep_assert_held(&bo->base.gpuva.lock);
+
+	if (drm_WARN_ON_ONCE(bo->base.dev, drm_gem_is_imported(&bo->base)))
+		return;
+
+	if (drm_WARN_ON_ONCE(bo->base.dev, refcount_read(&bo->backing.pin_count)))
+		return;
+
+	if (drm_WARN_ON_ONCE(bo->base.dev, !bo->backing.pages))
+		return;
+
+	panthor_gem_dev_map_cleanup_locked(bo);
+	panthor_gem_backing_cleanup_locked(bo);
+	panthor_gem_update_reclaim_state_locked(bo, NULL);
+}
+
 static struct sg_table *panthor_gem_get_sg_table(struct drm_gem_object *obj)
 {
 	struct panthor_gem_object *bo = to_panthor_bo(obj);
@@ -607,8 +755,12 @@ static int panthor_gem_mmap(struct drm_gem_object *obj, struct vm_area_struct *v
 
 	if (!refcount_inc_not_zero(&bo->cmap.mmap_count)) {
 		dma_resv_lock(obj->resv, NULL);
-		if (!refcount_inc_not_zero(&bo->cmap.mmap_count))
+		if (!refcount_inc_not_zero(&bo->cmap.mmap_count)) {
 			refcount_set(&bo->cmap.mmap_count, 1);
+			mutex_lock(&bo->base.gpuva.lock);
+			panthor_gem_update_reclaim_state_locked(bo, NULL);
+			mutex_unlock(&bo->base.gpuva.lock);
+		}
 		dma_resv_unlock(obj->resv);
 	}
 
@@ -690,6 +842,10 @@ static vm_fault_t blocking_page_setup(struct vm_fault *vmf,
 	} else {
 		struct page *page = bo->backing.pages[page_offset];
 
+		mutex_lock(&bo->base.gpuva.lock);
+		panthor_gem_update_reclaim_state_locked(bo, NULL);
+		mutex_unlock(&bo->base.gpuva.lock);
+
 		if (mmap_lock_held)
 			ret = insert_page(vmf, page);
 		else
@@ -763,7 +919,9 @@ static void panthor_gem_vm_close(struct vm_area_struct *vma)
 
 	dma_resv_lock(bo->base.resv, NULL);
 	if (refcount_dec_and_test(&bo->cmap.mmap_count)) {
-		/* Nothing to do, pages are reclaimed lazily. */
+		mutex_lock(&bo->base.gpuva.lock);
+		panthor_gem_update_reclaim_state_locked(bo, NULL);
+		mutex_unlock(&bo->base.gpuva.lock);
 	}
 	dma_resv_unlock(bo->base.resv);
 
@@ -800,6 +958,7 @@ panthor_gem_alloc_object(uint32_t flags)
 	if (!bo)
 		return ERR_PTR(-ENOMEM);
 
+	bo->reclaim_state = PANTHOR_GEM_UNRECLAIMABLE;
 	bo->base.funcs = &panthor_gem_funcs;
 	bo->flags = flags;
 	mutex_init(&bo->label.lock);
@@ -958,6 +1117,7 @@ panthor_gem_sync(struct drm_gem_object *obj, u32 type,
 	struct sg_table *sgt;
 	struct scatterlist *sgl;
 	unsigned int count;
+	int ret;
 
 	/* Make sure the range is in bounds. */
 	if (offset + size < offset || offset + size > bo->base.size)
@@ -984,9 +1144,21 @@ panthor_gem_sync(struct drm_gem_object *obj, u32 type,
 	if (size == 0)
 		return 0;
 
-	sgt = panthor_gem_get_dev_sgt(bo);
-	if (IS_ERR(sgt))
-		return PTR_ERR(sgt);
+	ret = dma_resv_lock_interruptible(bo->base.resv, NULL);
+	if (ret)
+		return ret;
+
+	/* If there's no pages, there's no point pulling those back, bail out early. */
+	if (!bo->backing.pages) {
+		ret = 0;
+		goto out_unlock;
+	}
+
+	sgt = panthor_gem_dev_map_get_sgt_locked(bo);
+	if (IS_ERR(sgt)) {
+		ret = PTR_ERR(sgt);
+		goto out_unlock;
+	}
 
 	for_each_sgtable_dma_sg(sgt, sgl, count) {
 		if (size == 0)
@@ -1030,7 +1202,11 @@ panthor_gem_sync(struct drm_gem_object *obj, u32 type,
 			dma_sync_single_for_cpu(dma_dev, paddr, len, DMA_FROM_DEVICE);
 	}
 
-	return 0;
+	ret = 0;
+
+out_unlock:
+	dma_resv_unlock(bo->base.resv);
+	return ret;
 }
 
 /**
@@ -1040,11 +1216,13 @@ panthor_gem_sync(struct drm_gem_object *obj, u32 type,
  */
 void panthor_kernel_bo_destroy(struct panthor_kernel_bo *bo)
 {
+	struct panthor_device *ptdev;
 	struct panthor_vm *vm;
 
 	if (IS_ERR_OR_NULL(bo))
 		return;
 
+	ptdev = container_of(bo->obj->dev, struct panthor_device, base);
 	vm = bo->vm;
 	panthor_kernel_bo_vunmap(bo);
 
@@ -1052,6 +1230,8 @@ void panthor_kernel_bo_destroy(struct panthor_kernel_bo *bo)
 		    to_panthor_bo(bo->obj)->exclusive_vm_root_gem != panthor_vm_root_gem(vm));
 	panthor_vm_unmap_range(vm, bo->va_node.start, bo->va_node.size);
 	panthor_vm_free_va(vm, &bo->va_node);
+	if (vm == panthor_fw_vm(ptdev))
+		panthor_gem_unpin(to_panthor_bo(bo->obj));
 	drm_gem_object_put(bo->obj);
 	panthor_vm_put(vm);
 	kfree(bo);
@@ -1100,6 +1280,12 @@ panthor_kernel_bo_create(struct panthor_device *ptdev, struct panthor_vm *vm,
 
 	kbo->obj = &bo->base;
 
+	if (vm == panthor_fw_vm(ptdev)) {
+		ret = panthor_gem_pin(bo);
+		if (ret)
+			goto err_put_obj;
+	}
+
 	panthor_gem_kernel_bo_set_label(kbo, name);
 
 	/* The system and GPU MMU page size might differ, which becomes a
@@ -1111,7 +1297,7 @@ panthor_kernel_bo_create(struct panthor_device *ptdev, struct panthor_vm *vm,
 	size = ALIGN(size, panthor_vm_page_size(vm));
 	ret = panthor_vm_alloc_va(vm, gpu_va, size, &kbo->va_node);
 	if (ret)
-		goto err_put_obj;
+		goto err_unpin;
 
 	ret = panthor_vm_map_bo_range(vm, bo, 0, size, kbo->va_node.start, vm_map_flags);
 	if (ret)
@@ -1123,6 +1309,10 @@ panthor_kernel_bo_create(struct panthor_device *ptdev, struct panthor_vm *vm,
 err_free_va:
 	panthor_vm_free_va(vm, &kbo->va_node);
 
+err_unpin:
+	if (vm == panthor_fw_vm(ptdev))
+		panthor_gem_unpin(bo);
+
 err_put_obj:
 	drm_gem_object_put(&bo->base);
 
@@ -1131,6 +1321,233 @@ panthor_kernel_bo_create(struct panthor_device *ptdev, struct panthor_vm *vm,
 	return ERR_PTR(ret);
 }
 
+static bool can_swap(void)
+{
+	return get_nr_swap_pages() > 0;
+}
+
+static bool can_block(struct shrink_control *sc)
+{
+	if (!(sc->gfp_mask & __GFP_DIRECT_RECLAIM))
+		return false;
+	return current_is_kswapd() || (sc->gfp_mask & __GFP_RECLAIM);
+}
+
+static unsigned long
+panthor_gem_shrinker_count(struct shrinker *shrinker, struct shrink_control *sc)
+{
+	struct panthor_device *ptdev = shrinker->private_data;
+	unsigned long count;
+
+	/* We currently don't have a flag to tell when the content of a
+	 * BO can be discarded.
+	 */
+	if (!can_swap())
+		return 0;
+
+	count = ptdev->reclaim.unused.count;
+	count += ptdev->reclaim.mmapped.count;
+
+	if (can_block(sc))
+		count += ptdev->reclaim.gpu_mapped_count;
+
+	return count ? count : SHRINK_EMPTY;
+}
+
+static bool panthor_gem_try_evict_no_resv_wait(struct drm_gem_object *obj,
+					       struct ww_acquire_ctx *ticket)
+{
+	/*
+	 * Track last locked entry for unwinding locks in error and
+	 * success paths
+	 */
+	struct panthor_gem_object *bo = to_panthor_bo(obj);
+	struct drm_gpuvm_bo *vm_bo, *last_locked = NULL;
+	enum panthor_gem_reclaim_state old_state;
+	int ret = 0;
+
+	/* To avoid potential lock ordering issue between bo_gpuva and
+	 * mapping->i_mmap_rwsem, unmap the pages from CPU side before
+	 * acquring the bo_gpuva lock. As the bo_resv lock is held, CPU
+	 * page fault handler won't be able to map in the pages whilst
+	 * eviction is in progress.
+	 */
+	drm_vma_node_unmap(&bo->base.vma_node, bo->base.dev->anon_inode->i_mapping);
+
+	/* We take this lock when walking the list to prevent
+	 * insertion/deletion.
+	 */
+	/* We can only trylock in that path, because
+	 * - allocation might happen while some of these locks are held
+	 * - lock ordering is different in other paths
+	 *     vm_resv -> bo_resv -> bo_gpuva
+	 *     vs
+	 *     bo_resv -> bo_gpuva -> vm_resv
+	 *
+	 * If we fail to lock that's fine, we back off and will get
+	 * back to it later.
+	 */
+	if (!mutex_trylock(&bo->base.gpuva.lock))
+		return false;
+
+	drm_gem_for_each_gpuvm_bo(vm_bo, obj) {
+		struct dma_resv *resv = drm_gpuvm_resv(vm_bo->vm);
+
+		if (resv == obj->resv)
+			continue;
+
+		if (!dma_resv_trylock(resv)) {
+			ret = -EDEADLK;
+			goto out_unlock;
+		}
+
+		last_locked = vm_bo;
+	}
+
+	/* Update the state before trying to evict the buffer, if the state was
+	 * updated to something that's harder to reclaim (higher value in the
+	 * enum), skip it (will be processed when the relevant LRU is).
+	 */
+	panthor_gem_update_reclaim_state_locked(bo, &old_state);
+	if (old_state < bo->reclaim_state) {
+		ret = -EAGAIN;
+		goto out_unlock;
+	}
+
+	/* Couldn't teardown the GPU mappings? Skip. */
+	ret = panthor_vm_evict_bo_mappings_locked(bo);
+	if (ret)
+		goto out_unlock;
+
+	/* If everything went fine, evict the object. */
+	panthor_gem_evict_locked(bo);
+
+out_unlock:
+	if (last_locked) {
+		drm_gem_for_each_gpuvm_bo(vm_bo, obj) {
+			struct dma_resv *resv = drm_gpuvm_resv(vm_bo->vm);
+
+			if (resv == obj->resv)
+				continue;
+
+			dma_resv_unlock(resv);
+
+			if (last_locked == vm_bo)
+				break;
+		}
+	}
+	mutex_unlock(&bo->base.gpuva.lock);
+
+	return ret == 0;
+}
+
+static bool panthor_gem_try_evict(struct drm_gem_object *obj,
+				  struct ww_acquire_ctx *ticket)
+{
+	struct panthor_gem_object *bo = to_panthor_bo(obj);
+
+	/* Wait was too long, skip. */
+	if (dma_resv_wait_timeout(obj->resv, DMA_RESV_USAGE_BOOKKEEP, false, 10) <= 0)
+		return false;
+
+	return panthor_gem_try_evict_no_resv_wait(&bo->base, ticket);
+}
+
+static unsigned long
+panthor_gem_shrinker_scan(struct shrinker *shrinker, struct shrink_control *sc)
+{
+	struct panthor_device *ptdev = shrinker->private_data;
+	unsigned long remaining = 0;
+	unsigned long freed = 0;
+
+	if (!can_swap())
+		goto out;
+
+	freed += drm_gem_lru_scan(&ptdev->reclaim.unused,
+				  sc->nr_to_scan - freed, &remaining,
+				  panthor_gem_try_evict_no_resv_wait, NULL);
+	if (freed >= sc->nr_to_scan)
+		goto out;
+
+	freed += drm_gem_lru_scan(&ptdev->reclaim.mmapped,
+				  sc->nr_to_scan - freed, &remaining,
+				  panthor_gem_try_evict_no_resv_wait, NULL);
+	if (freed >= sc->nr_to_scan)
+		goto out;
+
+	if (!can_block(sc))
+		goto out;
+
+	freed += panthor_mmu_reclaim_priv_bos(ptdev, sc->nr_to_scan - freed,
+					      &remaining, panthor_gem_try_evict);
+	if (freed >= sc->nr_to_scan)
+		goto out;
+
+	freed += drm_gem_lru_scan(&ptdev->reclaim.gpu_mapped_shared,
+				  sc->nr_to_scan - freed, &remaining,
+				  panthor_gem_try_evict, NULL);
+
+out:
+#ifdef CONFIG_DEBUG_FS
+	/* This is racy, but that's okay, because this is just debugfs
+	 * reporting and doesn't need to be accurate.
+	 */
+	ptdev->reclaim.nr_pages_reclaimed_on_last_scan = freed;
+#endif
+
+	/* If there are things to reclaim, try a couple times before giving up. */
+	if (!freed && remaining > 0 &&
+	    atomic_inc_return(&ptdev->reclaim.retry_count) < 2)
+		return 0;
+
+	atomic_set(&ptdev->reclaim.retry_count, 0);
+
+	if (freed)
+		return freed;
+
+	/* There's nothing left to reclaim, or the resources are contended. Give up now. */
+	return SHRINK_STOP;
+}
+
+int panthor_gem_shrinker_init(struct panthor_device *ptdev)
+{
+	struct shrinker *shrinker;
+	int ret;
+
+	ret = drmm_mutex_init(&ptdev->base, &ptdev->reclaim.lock);
+	if (ret)
+		return ret;
+
+	INIT_LIST_HEAD(&ptdev->reclaim.vms);
+	drm_gem_lru_init(&ptdev->reclaim.unused, &ptdev->reclaim.lock);
+	drm_gem_lru_init(&ptdev->reclaim.mmapped, &ptdev->reclaim.lock);
+	drm_gem_lru_init(&ptdev->reclaim.gpu_mapped_shared, &ptdev->reclaim.lock);
+	ptdev->reclaim.gpu_mapped_count = 0;
+
+	/* Teach lockdep about lock ordering wrt. shrinker: */
+	fs_reclaim_acquire(GFP_KERNEL);
+	might_lock(&ptdev->reclaim.lock);
+	fs_reclaim_release(GFP_KERNEL);
+
+	shrinker = shrinker_alloc(0, "drm-panthor-gem");
+	if (!shrinker)
+		return -ENOMEM;
+
+	shrinker->count_objects = panthor_gem_shrinker_count;
+	shrinker->scan_objects = panthor_gem_shrinker_scan;
+	shrinker->private_data = ptdev;
+	ptdev->reclaim.shrinker = shrinker;
+
+	shrinker_register(shrinker);
+	return 0;
+}
+
+void panthor_gem_shrinker_unplug(struct panthor_device *ptdev)
+{
+	if (ptdev->reclaim.shrinker)
+		shrinker_free(ptdev->reclaim.shrinker);
+}
+
 #ifdef CONFIG_DEBUG_FS
 struct gem_size_totals {
 	size_t size;
@@ -1174,6 +1591,7 @@ static void panthor_gem_debugfs_bo_print(struct panthor_gem_object *bo,
 					 struct seq_file *m,
 					 struct gem_size_totals *totals)
 {
+	enum panthor_gem_reclaim_state reclaim_state = bo->reclaim_state;
 	unsigned int refcount = kref_read(&bo->base.refcount);
 	char creator_info[32] = {};
 	size_t resident_size;
@@ -1209,6 +1627,8 @@ static void panthor_gem_debugfs_bo_print(struct panthor_gem_object *bo,
 
 	totals->size += bo->base.size;
 	totals->resident += resident_size;
+	if (reclaim_state != PANTHOR_GEM_UNRECLAIMABLE)
+		totals->reclaimable += resident_size;
 }
 
 static void panthor_gem_debugfs_print_bos(struct panthor_device *ptdev,
@@ -1249,10 +1669,42 @@ static struct drm_info_list panthor_gem_debugfs_list[] = {
 	{ "gems", panthor_gem_show_bos, 0, NULL },
 };
 
+static int shrink_get(void *data, u64 *val)
+{
+	struct panthor_device *ptdev =
+		container_of(data, struct panthor_device, base);
+
+	*val = ptdev->reclaim.nr_pages_reclaimed_on_last_scan;
+	return 0;
+}
+
+static int shrink_set(void *data, u64 val)
+{
+	struct panthor_device *ptdev =
+		container_of(data, struct panthor_device, base);
+	struct shrink_control sc = {
+		.gfp_mask = GFP_KERNEL,
+		.nr_to_scan = val,
+	};
+
+	fs_reclaim_acquire(GFP_KERNEL);
+	if (ptdev->reclaim.shrinker)
+		panthor_gem_shrinker_scan(ptdev->reclaim.shrinker, &sc);
+	fs_reclaim_release(GFP_KERNEL);
+
+	return 0;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(panthor_gem_debugfs_shrink_fops,
+			 shrink_get, shrink_set,
+			 "0x%08llx\n");
+
 void panthor_gem_debugfs_init(struct drm_minor *minor)
 {
 	drm_debugfs_create_files(panthor_gem_debugfs_list,
 				 ARRAY_SIZE(panthor_gem_debugfs_list),
 				 minor->debugfs_root, minor);
+	debugfs_create_file("shrink", 0600, minor->debugfs_root,
+			    minor->dev, &panthor_gem_debugfs_shrink_fops);
 }
 #endif
diff --git a/drivers/gpu/drm/panthor/panthor_gem.h b/drivers/gpu/drm/panthor/panthor_gem.h
index c0a18dca732c..30281fad7327 100644
--- a/drivers/gpu/drm/panthor/panthor_gem.h
+++ b/drivers/gpu/drm/panthor/panthor_gem.h
@@ -1,6 +1,7 @@
 /* SPDX-License-Identifier: GPL-2.0 or MIT */
 /* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
 /* Copyright 2023 Collabora ltd. */
+/* Copyright 2025 ARM Limited. All rights reserved. */
 
 #ifndef __PANTHOR_GEM_H__
 #define __PANTHOR_GEM_H__
@@ -93,6 +94,65 @@ struct panthor_gem_dev_map {
 	struct sg_table *sgt;
 };
 
+/**
+ * enum panthor_gem_reclaim_state - Reclaim state of a GEM object
+ *
+ * This is defined in descending reclaimability order and some part
+ * of the code depends on that.
+ */
+enum panthor_gem_reclaim_state {
+	/**
+	 * @PANTHOR_GEM_UNUSED: GEM is currently unused
+	 *
+	 * This can happen when the GEM was previously vmap-ed, mmap-ed,
+	 * and/or GPU mapped and got unmapped. Because pages are lazily
+	 * returned to the shmem layer, we want to keep a list of such
+	 * BOs, because they should be fairly easy to reclaim (no need
+	 * to wait for GPU to be done, and no need to tear down user
+	 * mappings either).
+	 */
+	PANTHOR_GEM_UNUSED,
+
+	/**
+	 * @PANTHOR_GEM_MMAPPED: GEM is currently mmap-ed
+	 *
+	 * When a GEM has pages allocated and the mmap_count is > 0, the
+	 * GEM is placed in the mmapped list. This comes right after
+	 * unused because we can relatively easily tear down user mappings.
+	 */
+	PANTHOR_GEM_MMAPPED,
+
+	/**
+	 * @PANTHOR_GEM_GPU_MAPPED_PRIVATE: GEM is GPU mapped to only one VM
+	 *
+	 * When a GEM is mapped to a single VM, reclaim requests have more
+	 * chances to succeed, because we only need to synchronize against
+	 * a single GPU context. This is more annoying than reclaiming
+	 * mmap-ed pages still, because we have to wait for in-flight jobs
+	 * to land, and we might not be able to acquire all necessary locks
+	 * at reclaim time either.
+	 */
+	PANTHOR_GEM_GPU_MAPPED_PRIVATE,
+
+	/**
+	 * @PANTHOR_GEM_GPU_MAPPED_SHARED: GEM is GPU mapped to multiple VMs
+	 *
+	 * Like PANTHOR_GEM_GPU_MAPPED_PRIVATE, but the synchronization across
+	 * VMs makes such BOs harder to reclaim.
+	 */
+	PANTHOR_GEM_GPU_MAPPED_SHARED,
+
+	/**
+	 * @PANTHOR_GEM_UNRECLAIMABLE: GEM can't be reclaimed
+	 *
+	 * Happens when the GEM memory is pinned. It's also the state all GEM
+	 * objects start in, because no memory is allocated until explicitly
+	 * requested by a CPU or GPU map, meaning there's nothing to reclaim
+	 * until such an allocation happens.
+	 */
+	PANTHOR_GEM_UNRECLAIMABLE,
+};
+
 /**
  * struct panthor_gem_object - Driver specific GEM object.
  */
@@ -109,6 +169,9 @@ struct panthor_gem_object {
 	/** @dmap: Device mapping state */
 	struct panthor_gem_dev_map dmap;
 
+	/** @reclaim_state: Cached reclaim state */
+	enum panthor_gem_reclaim_state reclaim_state;
+
 	/**
 	 * @exclusive_vm_root_gem: Root GEM of the exclusive VM this GEM object
 	 * is attached to.
@@ -190,6 +253,11 @@ struct sg_table *
 panthor_gem_get_dev_sgt(struct panthor_gem_object *bo);
 int panthor_gem_pin(struct panthor_gem_object *bo);
 void panthor_gem_unpin(struct panthor_gem_object *bo);
+int panthor_gem_swapin_locked(struct panthor_gem_object *bo);
+void panthor_gem_update_reclaim_state_locked(struct panthor_gem_object *bo,
+					     enum panthor_gem_reclaim_state *old_state);
+int panthor_gem_shrinker_init(struct panthor_device *ptdev);
+void panthor_gem_shrinker_unplug(struct panthor_device *ptdev);
 
 void panthor_gem_bo_set_label(struct drm_gem_object *obj, const char *label);
 void panthor_gem_kernel_bo_set_label(struct panthor_kernel_bo *bo, const char *label);
diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 5d07c1b96e0a..4b415aaedaa9 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0 or MIT
 /* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
 /* Copyright 2023 Collabora ltd. */
+/* Copyright 2025 ARM Limited. All rights reserved. */
 
 #include <drm/drm_debugfs.h>
 #include <drm/drm_drv.h>
@@ -131,6 +132,9 @@ struct panthor_vma {
 	 * Only map related flags are accepted.
 	 */
 	u32 flags;
+
+	/** @evicted: True if the VMA has been evicted. */
+	bool evicted;
 };
 
 /**
@@ -191,13 +195,8 @@ struct panthor_vm_op_ctx {
 		/** @map.bo_offset: Offset in the buffer object. */
 		u64 bo_offset;
 
-		/**
-		 * @map.sgt: sg-table pointing to pages backing the GEM object.
-		 *
-		 * This is gathered at job creation time, such that we don't have
-		 * to allocate in ::run_job().
-		 */
-		struct sg_table *sgt;
+		/** @map.bo: the BO being mapped. */
+		struct panthor_gem_object *bo;
 
 		/**
 		 * @map.new_vma: The new VMA object that will be inserted to the VA tree.
@@ -385,6 +384,18 @@ struct panthor_vm {
 		/** @locked_region.size: Size of the locked region. */
 		u64 size;
 	} locked_region;
+
+	/** @reclaim: Fields related to BO reclaim. */
+	struct {
+		/** @reclaim.lru: LRU of BOs that are only mapped to this VM. */
+		struct drm_gem_lru lru;
+
+		/**
+		 * @reclaim.lru_node: Node used to insert the VM in
+		 * panthor_device::reclaim::vms.
+		 */
+		struct list_head lru_node;
+	} reclaim;
 };
 
 /**
@@ -689,6 +700,16 @@ int panthor_vm_active(struct panthor_vm *vm)
 	if (refcount_inc_not_zero(&vm->as.active_cnt))
 		goto out_dev_exit;
 
+	/* As soon as active is called, we place the VM at the end of the VM LRU.
+	 * If something fails after that, the only downside is that this VM that
+	 * never became active in the first place will be reclaimed last, but
+	 * that's an acceptable trade-off.
+	 */
+	mutex_lock(&ptdev->reclaim.lock);
+	if (vm->reclaim.lru.count)
+		list_move_tail(&vm->reclaim.lru_node, &ptdev->reclaim.vms);
+	mutex_unlock(&ptdev->reclaim.lock);
+
 	/* Make sure we don't race with lock/unlock_region() calls
 	 * happening around VM bind operations.
 	 */
@@ -1084,7 +1105,15 @@ static void panthor_vm_bo_free(struct drm_gpuvm_bo *vm_bo)
 {
 	struct panthor_gem_object *bo = to_panthor_bo(vm_bo->obj);
 
-	panthor_gem_unpin(bo);
+	/* We couldn't call this when we unlinked, because the resv lock can't
+	 * be taken in the dma signalling path, so call it now.
+	 */
+	dma_resv_lock(bo->base.resv, NULL);
+	mutex_lock(&bo->base.gpuva.lock);
+	panthor_gem_update_reclaim_state_locked(bo, NULL);
+	mutex_unlock(&bo->base.gpuva.lock);
+	dma_resv_unlock(bo->base.resv);
+
 	kfree(vm_bo);
 }
 
@@ -1105,6 +1134,11 @@ static void panthor_vm_cleanup_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	if (op_ctx->map.vm_bo)
 		drm_gpuvm_bo_put_deferred(op_ctx->map.vm_bo);
 
+	if (op_ctx->map.bo) {
+		panthor_gem_unpin(op_ctx->map.bo);
+		drm_gem_object_put(&op_ctx->map.bo->base);
+	}
+
 	for (u32 i = 0; i < ARRAY_SIZE(op_ctx->preallocated_vmas); i++)
 		kfree(op_ctx->preallocated_vmas[i]);
 
@@ -1264,18 +1298,17 @@ static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	if (ret)
 		goto err_cleanup;
 
+	drm_gem_object_get(&bo->base);
+	op_ctx->map.bo = bo;
+
 	sgt = panthor_gem_get_dev_sgt(bo);
 	if (IS_ERR(sgt)) {
-		panthor_gem_unpin(bo);
 		ret = PTR_ERR(sgt);
 		goto err_cleanup;
 	}
 
-	op_ctx->map.sgt = sgt;
-
 	preallocated_vm_bo = drm_gpuvm_bo_create(&vm->base, &bo->base);
 	if (!preallocated_vm_bo) {
-		panthor_gem_unpin(bo);
 		ret = -ENOMEM;
 		goto err_cleanup;
 	}
@@ -1294,6 +1327,13 @@ static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 		dma_resv_unlock(panthor_vm_resv(vm));
 	}
 
+	/* And finally update the BO state. */
+	dma_resv_lock(bo->base.resv, NULL);
+	mutex_lock(&bo->base.gpuva.lock);
+	panthor_gem_update_reclaim_state_locked(bo, NULL);
+	mutex_unlock(&bo->base.gpuva.lock);
+	dma_resv_unlock(bo->base.resv);
+
 	return 0;
 
 err_cleanup:
@@ -1881,6 +1921,10 @@ static void panthor_vm_free(struct drm_gpuvm *gpuvm)
 	struct panthor_vm *vm = container_of(gpuvm, struct panthor_vm, base);
 	struct panthor_device *ptdev = vm->ptdev;
 
+	mutex_lock(&ptdev->reclaim.lock);
+	list_del_init(&vm->reclaim.lru_node);
+	mutex_unlock(&ptdev->reclaim.lock);
+
 	mutex_lock(&vm->heaps.lock);
 	if (drm_WARN_ON(&ptdev->base, vm->heaps.pool))
 		panthor_heap_pool_destroy(vm->heaps.pool);
@@ -2094,7 +2138,7 @@ static int panthor_gpuva_sm_step_map(struct drm_gpuva_op *op, void *priv)
 	panthor_vma_init(vma, op_ctx->flags & PANTHOR_VM_MAP_FLAGS);
 
 	ret = panthor_vm_map_pages(vm, op->map.va.addr, flags_to_prot(vma->flags),
-				   op_ctx->map.sgt, op->map.gem.offset,
+				   op_ctx->map.bo->dmap.sgt, op->map.gem.offset,
 				   op->map.va.range);
 	if (ret) {
 		panthor_vm_op_ctx_return_vma(op_ctx, vma);
@@ -2178,8 +2222,10 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 	 * atomicity. panthor_vm_lock_region() bails out early if the new region
 	 * is already part of the locked region, so no need to do this check here.
 	 */
-	panthor_vm_lock_region(vm, unmap_start, unmap_range);
-	panthor_vm_unmap_pages(vm, unmap_start, unmap_range);
+	if (!unmap_vma->evicted) {
+		panthor_vm_lock_region(vm, unmap_start, unmap_range);
+		panthor_vm_unmap_pages(vm, unmap_start, unmap_range);
+	}
 
 	if (op->remap.prev) {
 		struct panthor_gem_object *bo = to_panthor_bo(op->remap.prev->gem.obj);
@@ -2193,6 +2239,7 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 
 		prev_vma = panthor_vm_op_ctx_get_vma(op_ctx);
 		panthor_vma_init(prev_vma, unmap_vma->flags);
+		prev_vma->evicted = unmap_vma->evicted;
 	}
 
 	if (op->remap.next) {
@@ -2207,6 +2254,7 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 
 		next_vma = panthor_vm_op_ctx_get_vma(op_ctx);
 		panthor_vma_init(next_vma, unmap_vma->flags);
+		next_vma->evicted = unmap_vma->evicted;
 	}
 
 	drm_gpuva_remap(prev_vma ? &prev_vma->base : NULL,
@@ -2236,19 +2284,221 @@ static int panthor_gpuva_sm_step_unmap(struct drm_gpuva_op *op,
 	struct panthor_vma *unmap_vma = container_of(op->unmap.va, struct panthor_vma, base);
 	struct panthor_vm *vm = priv;
 
-	panthor_vm_unmap_pages(vm, unmap_vma->base.va.addr,
-			       unmap_vma->base.va.range);
+	if (!unmap_vma->evicted) {
+		panthor_vm_unmap_pages(vm, unmap_vma->base.va.addr,
+				       unmap_vma->base.va.range);
+	}
+
 	drm_gpuva_unmap(&op->unmap);
 	panthor_vma_unlink(unmap_vma);
 	return 0;
 }
 
+void panthor_vm_update_bo_reclaim_lru_locked(struct panthor_gem_object *bo)
+{
+	struct panthor_device *ptdev = container_of(bo->base.dev, struct panthor_device, base);
+	struct panthor_vm *vm = NULL;
+	struct drm_gpuvm_bo *vm_bo;
+
+	dma_resv_assert_held(bo->base.resv);
+	lockdep_assert_held(&bo->base.gpuva.lock);
+
+	drm_gem_for_each_gpuvm_bo(vm_bo, &bo->base) {
+		if (vm_bo->evicted)
+			continue;
+
+		/* We're only supposed to have one non-evicted vm_bo in the list if we get
+		 * there.
+		 */
+		drm_WARN_ON(&ptdev->base, vm);
+		vm = container_of(vm_bo->vm, struct panthor_vm, base);
+
+		mutex_lock(&ptdev->reclaim.lock);
+		drm_gem_lru_move_tail_locked(&vm->reclaim.lru, &bo->base);
+		if (list_empty(&vm->reclaim.lru_node))
+			list_move(&vm->reclaim.lru_node, &ptdev->reclaim.vms);
+		mutex_unlock(&ptdev->reclaim.lock);
+	}
+}
+
+int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo)
+{
+	struct drm_gpuvm_bo *vm_bo;
+	int ret = 0;
+
+	drm_gem_for_each_gpuvm_bo(vm_bo, &bo->base) {
+		struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+		struct drm_gpuva *va;
+
+		/* Skip already evicted GPU mappings. */
+		if (vm_bo->evicted)
+			continue;
+
+		if (!mutex_trylock(&vm->op_lock))
+			return -EDEADLK;
+
+		drm_gpuvm_bo_evict(vm_bo, true);
+		drm_gpuvm_bo_for_each_va(va, vm_bo) {
+			struct panthor_vma *vma = container_of(va, struct panthor_vma, base);
+
+			if (vma->evicted)
+				continue;
+
+			ret = panthor_vm_lock_region(vm, va->va.addr, va->va.range);
+			if (ret)
+				break;
+
+			panthor_vm_unmap_pages(vm, va->va.addr, va->va.range);
+			panthor_vm_unlock_region(vm);
+			vma->evicted = true;
+		}
+
+		mutex_unlock(&vm->op_lock);
+
+		if (ret)
+			break;
+	}
+
+	return ret;
+}
+
+static struct panthor_vma *select_evicted_vma(struct drm_gpuvm_bo *vm_bo,
+					      struct panthor_vm_op_ctx *op_ctx)
+{
+	struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+	struct panthor_vma *first_evicted_vma = NULL;
+	struct drm_gpuva *va;
+
+	/* Take op_lock to protect against va insertion/removal. */
+	mutex_lock(&vm->op_lock);
+	drm_gpuvm_bo_for_each_va(va, vm_bo) {
+		struct panthor_vma *vma = container_of(va, struct panthor_vma, base);
+
+		if (vma->evicted) {
+			first_evicted_vma = vma;
+			panthor_vm_init_op_ctx(op_ctx, va->va.range, va->va.addr, vma->flags);
+			op_ctx->map.bo_offset = va->gem.offset;
+			break;
+		}
+	}
+	mutex_unlock(&vm->op_lock);
+
+	return first_evicted_vma;
+}
+
+static int remap_evicted_vma(struct drm_gpuvm_bo *vm_bo,
+			     struct panthor_vma *evicted_vma,
+			     struct panthor_vm_op_ctx *op_ctx)
+{
+	struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+	struct panthor_gem_object *bo = to_panthor_bo(vm_bo->obj);
+	struct drm_gpuva *va;
+	bool found = false;
+	int ret;
+
+	ret = panthor_vm_op_ctx_prealloc_pts(op_ctx);
+	if (ret)
+		goto out_cleanup;
+
+	/* Take op_lock to protect against va insertion/removal. Note that the
+	 * evicted_vma selection was done with the same lock held, but we had
+	 * to release it so we can allocate PTs, because this very same lock
+	 * is taken in a DMA-signalling path.
+	 */
+	mutex_lock(&vm->op_lock);
+	drm_gpuvm_bo_for_each_va(va, vm_bo) {
+		struct panthor_vma *vma = container_of(va, struct panthor_vma, base);
+
+		if (vma != evicted_vma)
+			continue;
+
+		/* Because we had to release the lock between the evicted_vma selection
+		 * and its repopulation, we can't rely solely on pointer equality (the
+		 * VMA might have been freed and a new one allocated at the same address).
+		 * If the evicted bit is still set, we're sure it's our VMA, because
+		 * population/eviction is serialized with the BO resv lock.
+		 */
+		if (vma->evicted)
+			found = true;
+
+		break;
+	}
+
+	if (found) {
+		vm->op_ctx = op_ctx;
+		ret = panthor_vm_lock_region(vm, evicted_vma->base.va.addr,
+					     evicted_vma->base.va.range);
+		if (!ret) {
+			ret = panthor_vm_map_pages(vm, evicted_vma->base.va.addr,
+						   flags_to_prot(evicted_vma->flags),
+						   bo->dmap.sgt,
+						   evicted_vma->base.gem.offset,
+						   evicted_vma->base.va.range);
+		}
+
+		if (!ret)
+			evicted_vma->evicted = false;
+
+		panthor_vm_unlock_region(vm);
+		vm->op_ctx = NULL;
+	}
+
+	mutex_unlock(&vm->op_lock);
+
+out_cleanup:
+	panthor_vm_cleanup_op_ctx(op_ctx, vm);
+	return ret;
+}
+
+static int panthor_vm_restore_vmas(struct drm_gpuvm_bo *vm_bo)
+{
+	struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+	struct panthor_gem_object *bo = to_panthor_bo(vm_bo->obj);
+	struct panthor_vm_op_ctx op_ctx;
+
+	if (drm_WARN_ON_ONCE(&vm->ptdev->base, !bo->dmap.sgt))
+		return -EINVAL;
+
+	for (struct panthor_vma *vma = select_evicted_vma(vm_bo, &op_ctx);
+	     vma; vma = select_evicted_vma(vm_bo, &op_ctx)) {
+		int ret;
+
+		ret = remap_evicted_vma(vm_bo, vma, &op_ctx);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int panthor_vm_bo_validate(struct drm_gpuvm_bo *vm_bo,
+				  struct drm_exec *exec)
+{
+	struct panthor_gem_object *bo = to_panthor_bo(vm_bo->obj);
+	int ret;
+
+	ret = panthor_gem_swapin_locked(bo);
+	if (ret)
+		return ret;
+
+	ret = panthor_vm_restore_vmas(vm_bo);
+	if (ret)
+		return ret;
+
+	drm_gpuvm_bo_evict(vm_bo, false);
+	mutex_lock(&bo->base.gpuva.lock);
+	panthor_gem_update_reclaim_state_locked(bo, NULL);
+	mutex_unlock(&bo->base.gpuva.lock);
+	return 0;
+}
+
 static const struct drm_gpuvm_ops panthor_gpuvm_ops = {
 	.vm_free = panthor_vm_free,
 	.vm_bo_free = panthor_vm_bo_free,
 	.sm_step_map = panthor_gpuva_sm_step_map,
 	.sm_step_remap = panthor_gpuva_sm_step_remap,
 	.sm_step_unmap = panthor_gpuva_sm_step_unmap,
+	.vm_bo_validate = panthor_vm_bo_validate,
 };
 
 /**
@@ -2463,6 +2713,8 @@ panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 	vm->kernel_auto_va.start = auto_kernel_va_start;
 	vm->kernel_auto_va.end = vm->kernel_auto_va.start + auto_kernel_va_size - 1;
 
+	drm_gem_lru_init(&vm->reclaim.lru, &ptdev->reclaim.lock);
+	INIT_LIST_HEAD(&vm->reclaim.lru_node);
 	INIT_LIST_HEAD(&vm->node);
 	INIT_LIST_HEAD(&vm->as.lru_node);
 	vm->as.id = -1;
@@ -2810,7 +3062,78 @@ int panthor_vm_prepare_mapped_bos_resvs(struct drm_exec *exec, struct panthor_vm
 	if (ret)
 		return ret;
 
-	return drm_gpuvm_prepare_objects(&vm->base, exec, slot_count);
+	ret = drm_gpuvm_prepare_objects(&vm->base, exec, slot_count);
+	if (ret)
+		return ret;
+
+	return drm_gpuvm_validate(&vm->base, exec);
+}
+
+unsigned long
+panthor_mmu_reclaim_priv_bos(struct panthor_device *ptdev,
+			     unsigned int nr_to_scan, unsigned long *remaining,
+			     bool (*shrink)(struct drm_gem_object *,
+					    struct ww_acquire_ctx *))
+{
+	unsigned long freed = 0;
+	LIST_HEAD(remaining_vms);
+	LIST_HEAD(vms);
+
+	mutex_lock(&ptdev->reclaim.lock);
+	list_splice_init(&ptdev->reclaim.vms, &vms);
+
+	while (freed < nr_to_scan) {
+		struct panthor_vm *vm;
+
+		vm = list_first_entry_or_null(&vms, typeof(*vm),
+					      reclaim.lru_node);
+		if (!vm)
+			break;
+
+		if (!kref_get_unless_zero(&vm->base.kref)) {
+			list_del_init(&vm->reclaim.lru_node);
+			continue;
+		}
+
+		mutex_unlock(&ptdev->reclaim.lock);
+
+		freed += drm_gem_lru_scan(&vm->reclaim.lru, nr_to_scan - freed,
+					  remaining, shrink, NULL);
+
+		mutex_lock(&ptdev->reclaim.lock);
+
+		/* If the VM is still in the temporary list, remove it so we
+		 * can proceed with the next VM.
+		 */
+		if (vm->reclaim.lru_node.prev == &vms) {
+			list_del_init(&vm->reclaim.lru_node);
+
+			/* Keep the VM around if there are still things to
+			 * reclaim, so we can preserve the LRU order when
+			 * re-inserting in ptdev->reclaim.vms at the end.
+			 */
+			if (vm->reclaim.lru.count > 0)
+				list_add_tail(&vm->reclaim.lru_node, &remaining_vms);
+		}
+
+		mutex_unlock(&ptdev->reclaim.lock);
+
+		panthor_vm_put(vm);
+
+		mutex_lock(&ptdev->reclaim.lock);
+	}
+
+	/* Re-insert VMs with remaining data to reclaim at the beginning of
+	 * the LRU. Note that any activeness change on the VM that happened
+	 * while we were reclaiming would have moved the VM out of our
+	 * temporary [remaining_]vms list, meaning anything we re-insert here
+	 * preserves the LRU order.
+	 */
+	list_splice_tail(&vms, &remaining_vms);
+	list_splice(&remaining_vms, &ptdev->reclaim.vms);
+	mutex_unlock(&ptdev->reclaim.lock);
+
+	return freed;
 }
 
 /**
diff --git a/drivers/gpu/drm/panthor/panthor_mmu.h b/drivers/gpu/drm/panthor/panthor_mmu.h
index 0e268fdfdb2f..3522fbbce369 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.h
+++ b/drivers/gpu/drm/panthor/panthor_mmu.h
@@ -1,6 +1,7 @@
 /* SPDX-License-Identifier: GPL-2.0 or MIT */
 /* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
 /* Copyright 2023 Collabora ltd. */
+/* Copyright 2025 ARM Limited. All rights reserved. */
 
 #ifndef __PANTHOR_MMU_H__
 #define __PANTHOR_MMU_H__
@@ -46,6 +47,13 @@ struct panthor_vm *panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 				     u64 kernel_auto_va_start,
 				     u64 kernel_auto_va_size);
 
+void panthor_vm_update_bo_reclaim_lru_locked(struct panthor_gem_object *bo);
+int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo);
+unsigned long
+panthor_mmu_reclaim_priv_bos(struct panthor_device *ptdev,
+			     unsigned int nr_to_scan, unsigned long *remaining,
+			     bool (*shrink)(struct drm_gem_object *,
+					    struct ww_acquire_ctx *));
 int panthor_vm_prepare_mapped_bos_resvs(struct drm_exec *exec,
 					struct panthor_vm *vm,
 					u32 slot_count);
-- 
2.53.0


  parent reply	other threads:[~2026-03-09 15:11 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-09 15:11 [PATCH v5 0/9] drm/panthor: Add a GEM shrinker Boris Brezillon
2026-03-09 15:11 ` [PATCH v5 1/9] drm/gem: Consider GEM object reclaimable if shrinking fails Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 2/9] drm/panthor: Move panthor_gems_debugfs_init() to panthor_gem.c Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 3/9] drm/panthor: Group panthor_kernel_bo_xxx() helpers Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 4/9] drm/panthor: Don't call drm_gpuvm_bo_extobj_add() if the object is private Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 5/9] drm/panthor: Part ways with drm_gem_shmem_object Boris Brezillon
2026-03-09 15:34   ` Steven Price
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 6/9] drm/panthor: Lazily allocate pages on mmap() Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 7/9] drm/panthor: Split panthor_vm_prepare_map_op_ctx() to prepare for reclaim Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` [PATCH v5 8/9] drm/panthor: Track the number of mmap on a BO Boris Brezillon
2026-03-10  2:15   ` Claude review: " Claude Code Review Bot
2026-03-09 15:11 ` Boris Brezillon [this message]
2026-03-10  2:15   ` Claude review: drm/panthor: Add a GEM shrinker Claude Code Review Bot
2026-03-10  2:15 ` Claude Code Review Bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260309151119.290217-10-boris.brezillon@collabora.com \
    --to=boris.brezillon@collabora.com \
    --cc=adrian.larumbe@collabora.com \
    --cc=airlied@gmail.com \
    --cc=akash.goel@arm.com \
    --cc=akhilpo@oss.qualcomm.com \
    --cc=aliceryhl@google.com \
    --cc=chris.diamand@arm.com \
    --cc=dakr@kernel.org \
    --cc=dmitry.osipenko@collabora.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=kernel@collabora.com \
    --cc=konradybcio@kernel.org \
    --cc=liviu.dudau@arm.com \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=matthew.brost@intel.com \
    --cc=mripard@kernel.org \
    --cc=olvaffe@gmail.com \
    --cc=robin.clark@oss.qualcomm.com \
    --cc=sean@poorly.run \
    --cc=simona@ffwll.ch \
    --cc=steven.price@arm.com \
    --cc=thomas.hellstrom@linux.intel.com \
    --cc=tzimmermann@suse.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox