diff mbox series

[RFC,v2,3/6] drm/shmem: Implement sparse allocation of pages for shmem objects

Message ID 20250326021433.772196-4-adrian.larumbe@collabora.com (mailing list archive)
State New
Headers show
Series Introduce sparse DRM shmem object allocations | expand

Commit Message

Adrián Larumbe March 26, 2025, 2:14 a.m. UTC
Add a new function that lets drivers allocate pages for a subset of the
shmem object's virtual address range, and another function for obtaining
an SG table from those pages, so that memory can be mapped onto an MMU.

Add also a new function for putting the pages of a sparse page array.

The sparse allocation function allowed a gfp argument to allow
allocations other than GFP_KERNEL, in cases where memory allocation can
race with the shrinker's memory reclaim path

There is factorization potential with drm_gem_put_pages and
drm_get_pages_, but it is yet to be decided what this should look like.

Signed-off-by: Adrián Larumbe <adrian.larumbe@collabora.com>
---
 drivers/gpu/drm/drm_gem.c              | 117 ++++++++++++++++
 drivers/gpu/drm/drm_gem_shmem_helper.c | 182 ++++++++++++++++++++++++-
 include/drm/drm_gem.h                  |   6 +
 include/drm/drm_gem_shmem_helper.h     |   4 +
 4 files changed, 303 insertions(+), 6 deletions(-)

Comments

Dmitry Osipenko March 26, 2025, 7:54 p.m. UTC | #1
On 3/26/25 05:14, Adrián Larumbe wrote:
> +static struct sg_table *
> +drm_gem_shmem_sparse_get_sgt_range(struct drm_gem_shmem_object *shmem,
> +				   unsigned int n_pages, pgoff_t page_offset,
> +				   gfp_t gfp)
> +{
> +	struct drm_gem_object *obj = &shmem->base;
> +	struct sg_table *sgt;
> +	int ret;
> +
> +	if (drm_WARN_ON(obj->dev, !shmem->sparse))
> +		return ERR_PTR(-EINVAL);
> +
> +	/* If the page range wasn't allocated, then bail out immediately */
> +	if (xa_load(&shmem->xapages, page_offset) == NULL)
> +		return ERR_PTR(-EINVAL);
> +
> +	sgt = kzalloc(sizeof(*sgt), GFP_NOWAIT);

You likely meant to use the gfp arg here.
diff mbox series

Patch

diff --git a/drivers/gpu/drm/drm_gem.c b/drivers/gpu/drm/drm_gem.c
index c6240bab3fa5..fa9b3f01f9ac 100644
--- a/drivers/gpu/drm/drm_gem.c
+++ b/drivers/gpu/drm/drm_gem.c
@@ -679,6 +679,123 @@  void drm_gem_put_pages(struct drm_gem_object *obj, struct page **pages,
 }
 EXPORT_SYMBOL(drm_gem_put_pages);
 
+/**
+ * drm_get_pages_xarray - helper to allocate backing pages for a GEM object
+ * from shmem, and store them in an xarray.
+ * @obj: obj in question
+ * @pa: xarray that holds the backing pages
+ * @page_offset: shmem index of the very first page to allocate
+ * @npages: number of consecutive shmem pages to allocate
+ * @gfp: additional allocation flags
+ *
+ * This reads the page-array of the shmem-backing storage of the given gem
+ * object. The input xarray is where the pages are stored. If a page is not
+ * allocated or swapped-out, this will allocate/swap-in the required pages.
+ * Only the requested range is covered with physical pages.
+ *
+ * Use drm_gem_put_xarray_page_range() to release the same xarray subset of pages.
+ *
+ * This uses the GFP-mask set on the shmem-mapping (see mapping_set_gfp_mask()),
+ * and any further mask bits set in the gfp input parameter.
+ *
+ * This function is only valid on objects initialized with
+ * drm_gem_object_init(), but not for those initialized with
+ * drm_gem_private_object_init() only.
+ */
+int drm_get_pages_xarray(struct drm_gem_object *obj, struct xarray *pa,
+			 pgoff_t page_offset, unsigned int npages, gfp_t gfp)
+{
+	struct address_space *mapping;
+	struct page *page;
+	int ret = 0;
+	int i;
+
+	if (WARN_ON(!obj->filp))
+		return -EINVAL;
+
+	/* This is the shared memory object that backs the GEM resource */
+	mapping = obj->filp->f_mapping;
+
+	/* We already BUG_ON() for non-page-aligned sizes in
+	 * drm_gem_object_init(), so we should never hit this unless
+	 * driver author is doing something really wrong:
+	 */
+	WARN_ON((obj->size & (PAGE_SIZE - 1)) != 0);
+
+	mapping = obj->filp->f_mapping;
+	mapping_set_unevictable(mapping);
+
+	for (i = 0; i < npages; i++) {
+		page  = shmem_read_mapping_page_gfp(mapping, page_offset + i,
+						    mapping_gfp_mask(mapping) | gfp);
+		if (IS_ERR(page)) {
+			ret = PTR_ERR(page);
+			goto err_free_pages;
+		}
+
+		/* Add the page into the xarray */
+		ret = xa_err(xa_store(pa, page_offset + i, page, gfp));
+		if (ret) {
+			put_page(page);
+			goto err_free_pages;
+		}
+	}
+
+	return ret;
+
+err_free_pages:
+	while (--i) {
+		page = xa_erase(pa, page_offset + i);
+		if (drm_WARN_ON(obj->dev, !page))
+			continue;
+		put_page(page);
+	}
+
+	return ret;
+}
+EXPORT_SYMBOL(drm_get_pages_xarray);
+
+/**
+ * drm_gem_put_xarray_page_range - helper to free some backing pages for a
+ * sparse GEM object
+ * @pa: xarray that holds the backing pages
+ * @idx: xarray index of the first page tof ree
+ * @npages: number of consecutive pages in the xarray to free
+ * @dirty: if true, pages will be marked as dirty
+ * @accessed: if true, the pages will be marked as accessed
+ */
+void drm_gem_put_xarray_page_range(struct xarray *pa, unsigned long idx,
+				   unsigned int npages, bool dirty, bool accessed)
+{
+	struct folio_batch fbatch;
+	struct page *page;
+
+	folio_batch_init(&fbatch);
+
+	xa_for_each(pa, idx, page) {
+		struct folio *folio = page_folio(page);
+
+		if (dirty)
+			folio_mark_dirty(folio);
+		if (accessed)
+			folio_mark_accessed(folio);
+
+		/* Undo the reference we took when populating the table */
+		if (!folio_batch_add(&fbatch, folio))
+			drm_gem_check_release_batch(&fbatch);
+
+		xa_erase(pa, idx);
+
+		idx += folio_nr_pages(folio) - 1;
+	}
+
+	if (folio_batch_count(&fbatch))
+		drm_gem_check_release_batch(&fbatch);
+
+	WARN_ON((idx+1) != npages);
+}
+EXPORT_SYMBOL(drm_gem_put_xarray_page_range);
+
 static int objects_lookup(struct drm_file *filp, u32 *handle, int count,
 			  struct drm_gem_object **objs)
 {
diff --git a/drivers/gpu/drm/drm_gem_shmem_helper.c b/drivers/gpu/drm/drm_gem_shmem_helper.c
index 5f75eb1230f6..1bf33e5a1c4c 100644
--- a/drivers/gpu/drm/drm_gem_shmem_helper.c
+++ b/drivers/gpu/drm/drm_gem_shmem_helper.c
@@ -174,6 +174,34 @@  struct drm_gem_shmem_object *drm_gem_shmem_create_with_mnt(struct drm_device *de
 }
 EXPORT_SYMBOL_GPL(drm_gem_shmem_create_with_mnt);
 
+static void drm_gem_shmem_put_pages_sparse(struct drm_gem_shmem_object *shmem)
+{
+	struct page *page;
+	unsigned long idx;
+
+	if (drm_WARN_ON(shmem->base.dev, !shmem->sparse))
+		return;
+
+	idx = 0;
+	xa_for_each(&shmem->xapages, idx, page) {
+		unsigned long consecutive = 1;
+
+		if (!page)
+			continue;
+
+		while (xa_load(&shmem->xapages, idx + consecutive))
+			consecutive++;
+
+		drm_gem_put_xarray_page_range(&shmem->xapages, idx, consecutive,
+					      shmem->pages_mark_dirty_on_put,
+					      shmem->pages_mark_accessed_on_put);
+
+		idx += consecutive;
+	}
+
+	drm_WARN_ON(shmem->base.dev, !xa_empty(&shmem->xapages));
+}
+
 /**
  * drm_gem_shmem_free - Free resources associated with a shmem GEM object
  * @shmem: shmem GEM object to free
@@ -266,8 +294,8 @@  void drm_gem_shmem_put_pages(struct drm_gem_shmem_object *shmem)
 		if (!shmem->pages)
 			return;
 	} else {
-		/* Not implemented yet */
-		return;
+		if (xa_empty(&shmem->xapages))
+			return;
 	}
 
 	if (drm_WARN_ON_ONCE(obj->dev, !shmem->pages_use_count))
@@ -281,10 +309,15 @@  void drm_gem_shmem_put_pages(struct drm_gem_shmem_object *shmem)
 		set_pages_array_wb(shmem->pages, obj->size >> PAGE_SHIFT);
 #endif
 
-	drm_gem_put_pages(obj, shmem->pages,
-			  shmem->pages_mark_dirty_on_put,
-			  shmem->pages_mark_accessed_on_put);
-	shmem->pages = NULL;
+	if (!shmem->sparse) {
+		drm_gem_put_pages(obj, shmem->pages,
+				  shmem->pages_mark_dirty_on_put,
+				  shmem->pages_mark_accessed_on_put);
+		shmem->pages = NULL;
+	} else {
+		drm_gem_shmem_put_pages_sparse(shmem);
+		xa_destroy(&shmem->xapages);
+	}
 }
 EXPORT_SYMBOL(drm_gem_shmem_put_pages);
 
@@ -797,6 +830,103 @@  static struct sg_table *drm_gem_shmem_get_pages_sgt_locked(struct drm_gem_shmem_
 	return ERR_PTR(ret);
 }
 
+static int
+drm_gem_shmem_sparse_populate_locked(struct drm_gem_shmem_object *shmem,
+				     unsigned int n_pages, pgoff_t page_offset,
+				     gfp_t gfp)
+{
+	bool first_alloc;
+	int ret;
+
+	if (!shmem->sparse)
+		return -EINVAL;
+
+	dma_resv_assert_held(shmem->base.resv);
+
+	/* If the mapping exists, then bail out immediately */
+	if (xa_load(&shmem->xapages, page_offset) != NULL)
+		return -EEXIST;
+
+	first_alloc = xa_empty(&shmem->xapages);
+
+	ret = drm_get_pages_xarray(&shmem->base, &shmem->xapages,
+				   page_offset, n_pages, gfp);
+	if (ret)
+		return ret;
+
+	if (first_alloc)
+		shmem->pages_use_count = 1;
+
+	return 0;
+}
+
+static struct sg_table *
+drm_gem_shmem_sparse_get_sgt_range(struct drm_gem_shmem_object *shmem,
+				   unsigned int n_pages, pgoff_t page_offset,
+				   gfp_t gfp)
+{
+	struct drm_gem_object *obj = &shmem->base;
+	struct sg_table *sgt;
+	int ret;
+
+	if (drm_WARN_ON(obj->dev, !shmem->sparse))
+		return ERR_PTR(-EINVAL);
+
+	/* If the page range wasn't allocated, then bail out immediately */
+	if (xa_load(&shmem->xapages, page_offset) == NULL)
+		return ERR_PTR(-EINVAL);
+
+	sgt = kzalloc(sizeof(*sgt), GFP_NOWAIT);
+	if (!sgt)
+		return ERR_PTR(-ENOMEM);
+
+	ret = sg_alloc_table_from_xarray(sgt, &shmem->xapages, page_offset,
+					 n_pages, 0, n_pages * PAGE_SIZE, gfp);
+	if (ret)
+		goto err_free_sgtable;
+
+	ret = dma_map_sgtable(obj->dev->dev, sgt, DMA_BIDIRECTIONAL, 0);
+	if (ret)
+		goto err_free_sgtable;
+
+	return sgt;
+
+err_free_sgtable:
+	kfree(sgt);
+	return ERR_PTR(ret);
+}
+
+static struct sg_table *
+drm_gem_shmem_get_sparse_pages_locked(struct drm_gem_shmem_object *shmem,
+				      unsigned int n_pages, pgoff_t page_offset,
+				      gfp_t gfp)
+{
+	struct sg_table *sgt;
+	int ret;
+
+	if (!shmem->sparse)
+		return ERR_PTR(-EINVAL);
+
+	dma_resv_assert_held(shmem->base.resv);
+
+	ret = drm_gem_shmem_sparse_populate_locked(shmem, n_pages, page_offset, gfp);
+	if (ret)
+		return ERR_PTR(ret);
+
+	sgt = drm_gem_shmem_sparse_get_sgt_range(shmem, n_pages, page_offset, gfp);
+	if (IS_ERR(sgt)) {
+		ret = PTR_ERR(sgt);
+		goto err_free_pages;
+	}
+
+	return sgt;
+
+err_free_pages:
+	drm_gem_put_xarray_page_range(&shmem->xapages, page_offset,
+				      n_pages, false, false);
+	return ERR_PTR(ret);
+}
+
 /**
  * drm_gem_shmem_get_pages_sgt - Pin pages, dma map them, and return a
  *				 scatter/gather table for a shmem GEM object.
@@ -828,6 +958,46 @@  struct sg_table *drm_gem_shmem_get_pages_sgt(struct drm_gem_shmem_object *shmem)
 }
 EXPORT_SYMBOL_GPL(drm_gem_shmem_get_pages_sgt);
 
+/**
+ * drm_gem_shmem_get_sparse_pages_sgt - Pin pages, dma map them, and return a
+ *				 scatter/gather table for a sparse shmem GEM object.
+ * @shmem: shmem GEM object
+ * @n_pages: number of pages to pin and map
+ * @page_offset: shmem file index of the first page to allocate and map
+ * @gfp: Further allocation flags
+ *
+ * This function conceptually does the same thing as drm_gem_shmem_get_pages_sgt,
+ * but only for a contiguous subset of pages from the underlying shmem file.
+ * The allocation flags allows users to allocate pages with a mask other than
+ * GFP_KERNEL, in cases where it can race with shmem shrinkers.
+ *
+ * Returns:
+ * A pointer to the scatter/gather table of pinned pages or errno on failure.
+ */
+struct sg_table *
+drm_gem_shmem_get_sparse_pages_sgt(struct drm_gem_shmem_object *shmem,
+				   unsigned int n_pages, pgoff_t page_offset,
+				   gfp_t gfp)
+{
+	struct drm_gem_object *obj = &shmem->base;
+	struct sg_table *sgt;
+	int ret;
+
+	if (drm_WARN_ON(obj->dev, !shmem->sparse))
+		return ERR_PTR(-EINVAL);
+
+	ret = dma_resv_lock(shmem->base.resv, NULL);
+	if (ret)
+		return ERR_PTR(ret);
+
+	sgt = drm_gem_shmem_get_sparse_pages_locked(shmem, n_pages, page_offset, gfp);
+
+	dma_resv_unlock(shmem->base.resv);
+
+	return sgt;
+}
+EXPORT_SYMBOL_GPL(drm_gem_shmem_get_sparse_pages_sgt);
+
 /**
  * drm_gem_shmem_prime_import_sg_table - Produce a shmem GEM object from
  *                 another driver's scatter/gather table of pinned pages
diff --git a/include/drm/drm_gem.h b/include/drm/drm_gem.h
index 2bf893eabb4b..d8288a119bc3 100644
--- a/include/drm/drm_gem.h
+++ b/include/drm/drm_gem.h
@@ -39,6 +39,7 @@ 
 #include <linux/dma-resv.h>
 #include <linux/list.h>
 #include <linux/mutex.h>
+#include <linux/xarray.h>
 
 #include <drm/drm_vma_manager.h>
 
@@ -534,6 +535,11 @@  struct page **drm_gem_get_pages(struct drm_gem_object *obj);
 void drm_gem_put_pages(struct drm_gem_object *obj, struct page **pages,
 		bool dirty, bool accessed);
 
+int drm_get_pages_xarray(struct drm_gem_object *obj, struct xarray *pa,
+			 pgoff_t page_offset, unsigned int npages, gfp_t gfp);
+void drm_gem_put_xarray_page_range(struct xarray *pa, unsigned long idx,
+				   unsigned int npages, bool dirty, bool accessed);
+
 void drm_gem_lock(struct drm_gem_object *obj);
 void drm_gem_unlock(struct drm_gem_object *obj);
 
diff --git a/include/drm/drm_gem_shmem_helper.h b/include/drm/drm_gem_shmem_helper.h
index 00e47512b30f..cbe4548e3ff6 100644
--- a/include/drm/drm_gem_shmem_helper.h
+++ b/include/drm/drm_gem_shmem_helper.h
@@ -138,6 +138,10 @@  void drm_gem_shmem_purge(struct drm_gem_shmem_object *shmem);
 struct sg_table *drm_gem_shmem_get_sg_table(struct drm_gem_shmem_object *shmem);
 struct sg_table *drm_gem_shmem_get_pages_sgt(struct drm_gem_shmem_object *shmem);
 
+struct sg_table *drm_gem_shmem_get_sparse_pages_sgt(struct drm_gem_shmem_object *shmem,
+						     unsigned int n_pages, pgoff_t page_offset,
+						     gfp_t gfp);
+
 void drm_gem_shmem_print_info(const struct drm_gem_shmem_object *shmem,
 			      struct drm_printer *p, unsigned int indent);