| // SPDX-License-Identifier: GPL-2.0-only OR MIT |
| /* |
| * Copyright © 2025 Intel Corporation |
| */ |
| |
| #include <linux/slab.h> |
| |
| #include <drm/drm_drv.h> |
| #include <drm/drm_managed.h> |
| #include <drm/drm_pagemap.h> |
| #include <drm/drm_pagemap_util.h> |
| #include <drm/drm_print.h> |
| |
| /** |
| * struct drm_pagemap_cache - Lookup structure for pagemaps |
| * |
| * Structure to keep track of active (refcount > 1) and inactive |
| * (refcount == 0) pagemaps. Inactive pagemaps can be made active |
| * again by waiting for the @queued completion (indicating that the |
| * pagemap has been put on the @shrinker's list of shrinkable |
| * pagemaps, and then successfully removing it from @shrinker's |
| * list. The latter may fail if the shrinker is already in the |
| * process of freeing the pagemap. A struct drm_pagemap_cache can |
| * hold a single struct drm_pagemap. |
| */ |
| struct drm_pagemap_cache { |
| /** @lookup_mutex: Mutex making the lookup process atomic */ |
| struct mutex lookup_mutex; |
| /** @lock: Lock protecting the @dpagemap pointer */ |
| spinlock_t lock; |
| /** @shrinker: Pointer to the shrinker used for this cache. Immutable. */ |
| struct drm_pagemap_shrinker *shrinker; |
| /** @dpagemap: Non-refcounted pointer to the drm_pagemap */ |
| struct drm_pagemap *dpagemap; |
| /** |
| * @queued: Signals when an inactive drm_pagemap has been put on |
| * @shrinker's list. |
| */ |
| struct completion queued; |
| }; |
| |
| /** |
| * struct drm_pagemap_shrinker - Shrinker to remove unused pagemaps |
| */ |
| struct drm_pagemap_shrinker { |
| /** @drm: Pointer to the drm device. */ |
| struct drm_device *drm; |
| /** @lock: Spinlock to protect the @dpagemaps list. */ |
| spinlock_t lock; |
| /** @dpagemaps: List of unused dpagemaps. */ |
| struct list_head dpagemaps; |
| /** @num_dpagemaps: Number of unused dpagemaps in @dpagemaps. */ |
| atomic_t num_dpagemaps; |
| /** @shrink: Pointer to the struct shrinker. */ |
| struct shrinker *shrink; |
| }; |
| |
| static bool drm_pagemap_shrinker_cancel(struct drm_pagemap *dpagemap); |
| |
| static void drm_pagemap_cache_fini(void *arg) |
| { |
| struct drm_pagemap_cache *cache = arg; |
| struct drm_pagemap *dpagemap; |
| |
| drm_dbg(cache->shrinker->drm, "Destroying dpagemap cache.\n"); |
| spin_lock(&cache->lock); |
| dpagemap = cache->dpagemap; |
| cache->dpagemap = NULL; |
| if (dpagemap && !drm_pagemap_shrinker_cancel(dpagemap)) |
| dpagemap = NULL; |
| spin_unlock(&cache->lock); |
| |
| if (dpagemap) |
| drm_pagemap_destroy(dpagemap, false); |
| |
| mutex_destroy(&cache->lookup_mutex); |
| kfree(cache); |
| } |
| |
| /** |
| * drm_pagemap_cache_create_devm() - Create a drm_pagemap_cache |
| * @shrinker: Pointer to a struct drm_pagemap_shrinker. |
| * |
| * Create a device-managed drm_pagemap cache. The cache is automatically |
| * destroyed on struct device removal, at which point any *inactive* |
| * drm_pagemap's are destroyed. |
| * |
| * Return: Pointer to a struct drm_pagemap_cache on success. Error pointer |
| * on failure. |
| */ |
| struct drm_pagemap_cache *drm_pagemap_cache_create_devm(struct drm_pagemap_shrinker *shrinker) |
| { |
| struct drm_pagemap_cache *cache = kzalloc_obj(*cache); |
| int err; |
| |
| if (!cache) |
| return ERR_PTR(-ENOMEM); |
| |
| mutex_init(&cache->lookup_mutex); |
| spin_lock_init(&cache->lock); |
| cache->shrinker = shrinker; |
| init_completion(&cache->queued); |
| err = devm_add_action_or_reset(shrinker->drm->dev, drm_pagemap_cache_fini, cache); |
| if (err) |
| return ERR_PTR(err); |
| |
| return cache; |
| } |
| EXPORT_SYMBOL(drm_pagemap_cache_create_devm); |
| |
| /** |
| * DOC: Cache lookup |
| * |
| * Cache lookup should be done under a locked mutex, so that a |
| * failed drm_pagemap_get_from_cache() and a following |
| * drm_pagemap_cache_setpagemap() are carried out as an atomic |
| * operation WRT other lookups. Otherwise, racing lookups may |
| * unnecessarily concurrently create pagemaps to fulfill a |
| * failed lookup. The API provides two functions to perform this lock, |
| * drm_pagemap_lock_lookup() and drm_pagemap_unlock_lookup() and they |
| * should be used in the following way: |
| * |
| * .. code-block:: c |
| * |
| * drm_pagemap_lock_lookup(cache); |
| * dpagemap = drm_pagemap_get_from_cache(cache); |
| * if (dpagemap) |
| * goto out_unlock; |
| * |
| * dpagemap = driver_create_new_dpagemap(); |
| * if (!IS_ERR(dpagemap)) |
| * drm_pagemap_cache_set_pagemap(cache, dpagemap); |
| * |
| * out_unlock: |
| * drm_pagemap_unlock_lookup(cache); |
| */ |
| |
| /** |
| * drm_pagemap_cache_lock_lookup() - Lock a drm_pagemap_cache for lookup. |
| * @cache: The drm_pagemap_cache to lock. |
| * |
| * Return: %-EINTR if interrupted while blocking. %0 otherwise. |
| */ |
| int drm_pagemap_cache_lock_lookup(struct drm_pagemap_cache *cache) |
| { |
| return mutex_lock_interruptible(&cache->lookup_mutex); |
| } |
| EXPORT_SYMBOL(drm_pagemap_cache_lock_lookup); |
| |
| /** |
| * drm_pagemap_cache_unlock_lookup() - Unlock a drm_pagemap_cache after lookup. |
| * @cache: The drm_pagemap_cache to unlock. |
| */ |
| void drm_pagemap_cache_unlock_lookup(struct drm_pagemap_cache *cache) |
| { |
| mutex_unlock(&cache->lookup_mutex); |
| } |
| EXPORT_SYMBOL(drm_pagemap_cache_unlock_lookup); |
| |
| /** |
| * drm_pagemap_get_from_cache() - Lookup of drm_pagemaps. |
| * @cache: The cache used for lookup. |
| * |
| * If an active pagemap is present in the cache, it is immediately returned. |
| * If an inactive pagemap is present, it's removed from the shrinker list and |
| * an attempt is made to make it active. |
| * If no pagemap present or the attempt to make it active failed, %NULL is returned |
| * to indicate to the caller to create a new drm_pagemap and insert it into |
| * the cache. |
| * |
| * Return: A reference-counted pointer to a drm_pagemap if successful. An error |
| * pointer if an error occurred, or %NULL if no drm_pagemap was found and |
| * the caller should insert a new one. |
| */ |
| struct drm_pagemap *drm_pagemap_get_from_cache(struct drm_pagemap_cache *cache) |
| { |
| struct drm_pagemap *dpagemap; |
| int err; |
| |
| lockdep_assert_held(&cache->lookup_mutex); |
| retry: |
| spin_lock(&cache->lock); |
| dpagemap = cache->dpagemap; |
| if (drm_pagemap_get_unless_zero(dpagemap)) { |
| spin_unlock(&cache->lock); |
| return dpagemap; |
| } |
| |
| if (!dpagemap) { |
| spin_unlock(&cache->lock); |
| return NULL; |
| } |
| |
| if (!try_wait_for_completion(&cache->queued)) { |
| spin_unlock(&cache->lock); |
| err = wait_for_completion_interruptible(&cache->queued); |
| if (err) |
| return ERR_PTR(err); |
| goto retry; |
| } |
| |
| if (drm_pagemap_shrinker_cancel(dpagemap)) { |
| cache->dpagemap = NULL; |
| spin_unlock(&cache->lock); |
| err = drm_pagemap_reinit(dpagemap); |
| if (err) { |
| drm_pagemap_destroy(dpagemap, false); |
| return ERR_PTR(err); |
| } |
| drm_pagemap_cache_set_pagemap(cache, dpagemap); |
| } else { |
| cache->dpagemap = NULL; |
| spin_unlock(&cache->lock); |
| dpagemap = NULL; |
| } |
| |
| return dpagemap; |
| } |
| EXPORT_SYMBOL(drm_pagemap_get_from_cache); |
| |
| /** |
| * drm_pagemap_cache_set_pagemap() - Assign a drm_pagemap to a drm_pagemap_cache |
| * @cache: The cache to assign the drm_pagemap to. |
| * @dpagemap: The drm_pagemap to assign. |
| * |
| * The function must be called to populate a drm_pagemap_cache only |
| * after a call to drm_pagemap_get_from_cache() returns NULL. |
| */ |
| void drm_pagemap_cache_set_pagemap(struct drm_pagemap_cache *cache, struct drm_pagemap *dpagemap) |
| { |
| struct drm_device *drm = dpagemap->drm; |
| |
| lockdep_assert_held(&cache->lookup_mutex); |
| spin_lock(&cache->lock); |
| dpagemap->cache = cache; |
| swap(cache->dpagemap, dpagemap); |
| reinit_completion(&cache->queued); |
| spin_unlock(&cache->lock); |
| drm_WARN_ON(drm, !!dpagemap); |
| } |
| EXPORT_SYMBOL(drm_pagemap_cache_set_pagemap); |
| |
| /** |
| * drm_pagemap_get_from_cache_if_active() - Quick lookup of active drm_pagemaps |
| * @cache: The cache to lookup from. |
| * |
| * Function that should be used to lookup a drm_pagemap that is already active. |
| * (refcount > 0). |
| * |
| * Return: A pointer to the cache's drm_pagemap if it's active; %NULL otherwise. |
| */ |
| struct drm_pagemap *drm_pagemap_get_from_cache_if_active(struct drm_pagemap_cache *cache) |
| { |
| struct drm_pagemap *dpagemap; |
| |
| spin_lock(&cache->lock); |
| dpagemap = drm_pagemap_get_unless_zero(cache->dpagemap); |
| spin_unlock(&cache->lock); |
| |
| return dpagemap; |
| } |
| EXPORT_SYMBOL(drm_pagemap_get_from_cache_if_active); |
| |
| static bool drm_pagemap_shrinker_cancel(struct drm_pagemap *dpagemap) |
| { |
| struct drm_pagemap_cache *cache = dpagemap->cache; |
| struct drm_pagemap_shrinker *shrinker = cache->shrinker; |
| |
| spin_lock(&shrinker->lock); |
| if (list_empty(&dpagemap->shrink_link)) { |
| spin_unlock(&shrinker->lock); |
| return false; |
| } |
| |
| list_del_init(&dpagemap->shrink_link); |
| atomic_dec(&shrinker->num_dpagemaps); |
| spin_unlock(&shrinker->lock); |
| return true; |
| } |
| |
| #ifdef CONFIG_PROVE_LOCKING |
| /** |
| * drm_pagemap_shrinker_might_lock() - lockdep test for drm_pagemap_shrinker_add() |
| * @dpagemap: The drm pagemap. |
| * |
| * The drm_pagemap_shrinker_add() function performs some locking. |
| * This function can be called in code-paths that might |
| * call drm_pagemap_shrinker_add() to detect any lockdep problems early. |
| */ |
| void drm_pagemap_shrinker_might_lock(struct drm_pagemap *dpagemap) |
| { |
| int idx; |
| |
| if (drm_dev_enter(dpagemap->drm, &idx)) { |
| struct drm_pagemap_cache *cache = dpagemap->cache; |
| |
| if (cache) |
| might_lock(&cache->shrinker->lock); |
| |
| drm_dev_exit(idx); |
| } |
| } |
| #endif |
| |
| /** |
| * drm_pagemap_shrinker_add() - Add a drm_pagemap to the shrinker list or destroy |
| * @dpagemap: The drm_pagemap. |
| * |
| * If @dpagemap is associated with a &struct drm_pagemap_cache AND the |
| * struct device backing the drm device is still alive, add @dpagemap to |
| * the &struct drm_pagemap_shrinker list of shrinkable drm_pagemaps. |
| * |
| * Otherwise destroy the pagemap directly using drm_pagemap_destroy(). |
| * |
| * This is an internal function which is not intended to be exposed to drivers. |
| */ |
| void drm_pagemap_shrinker_add(struct drm_pagemap *dpagemap) |
| { |
| struct drm_pagemap_cache *cache; |
| struct drm_pagemap_shrinker *shrinker; |
| int idx; |
| |
| /* |
| * The pagemap cache and shrinker are disabled at |
| * pci device remove time. After that, dpagemaps |
| * are freed directly. |
| */ |
| if (!drm_dev_enter(dpagemap->drm, &idx)) |
| goto out_no_cache; |
| |
| cache = dpagemap->cache; |
| if (!cache) { |
| drm_dev_exit(idx); |
| goto out_no_cache; |
| } |
| |
| shrinker = cache->shrinker; |
| spin_lock(&shrinker->lock); |
| list_add_tail(&dpagemap->shrink_link, &shrinker->dpagemaps); |
| atomic_inc(&shrinker->num_dpagemaps); |
| spin_unlock(&shrinker->lock); |
| complete_all(&cache->queued); |
| drm_dev_exit(idx); |
| return; |
| |
| out_no_cache: |
| drm_pagemap_destroy(dpagemap, true); |
| } |
| |
| static unsigned long |
| drm_pagemap_shrinker_count(struct shrinker *shrink, struct shrink_control *sc) |
| { |
| struct drm_pagemap_shrinker *shrinker = shrink->private_data; |
| unsigned long count = atomic_read(&shrinker->num_dpagemaps); |
| |
| return count ? : SHRINK_EMPTY; |
| } |
| |
| static unsigned long |
| drm_pagemap_shrinker_scan(struct shrinker *shrink, struct shrink_control *sc) |
| { |
| struct drm_pagemap_shrinker *shrinker = shrink->private_data; |
| struct drm_pagemap *dpagemap; |
| struct drm_pagemap_cache *cache; |
| unsigned long nr_freed = 0; |
| |
| sc->nr_scanned = 0; |
| spin_lock(&shrinker->lock); |
| do { |
| dpagemap = list_first_entry_or_null(&shrinker->dpagemaps, typeof(*dpagemap), |
| shrink_link); |
| if (!dpagemap) |
| break; |
| |
| atomic_dec(&shrinker->num_dpagemaps); |
| list_del_init(&dpagemap->shrink_link); |
| spin_unlock(&shrinker->lock); |
| |
| sc->nr_scanned++; |
| nr_freed++; |
| |
| cache = dpagemap->cache; |
| spin_lock(&cache->lock); |
| cache->dpagemap = NULL; |
| spin_unlock(&cache->lock); |
| |
| drm_dbg(dpagemap->drm, "Shrinking dpagemap %p.\n", dpagemap); |
| drm_pagemap_destroy(dpagemap, true); |
| spin_lock(&shrinker->lock); |
| } while (sc->nr_scanned < sc->nr_to_scan); |
| spin_unlock(&shrinker->lock); |
| |
| return sc->nr_scanned ? nr_freed : SHRINK_STOP; |
| } |
| |
| static void drm_pagemap_shrinker_fini(void *arg) |
| { |
| struct drm_pagemap_shrinker *shrinker = arg; |
| |
| drm_dbg(shrinker->drm, "Destroying dpagemap shrinker.\n"); |
| drm_WARN_ON(shrinker->drm, !!atomic_read(&shrinker->num_dpagemaps)); |
| shrinker_free(shrinker->shrink); |
| kfree(shrinker); |
| } |
| |
| /** |
| * drm_pagemap_shrinker_create_devm() - Create and register a pagemap shrinker |
| * @drm: The drm device |
| * |
| * Create and register a pagemap shrinker that shrinks unused pagemaps |
| * and thereby reduces memory footprint. |
| * The shrinker is drm_device managed and unregisters itself when |
| * the drm device is removed. |
| * |
| * Return: %0 on success, negative error code on failure. |
| */ |
| struct drm_pagemap_shrinker *drm_pagemap_shrinker_create_devm(struct drm_device *drm) |
| { |
| struct drm_pagemap_shrinker *shrinker; |
| struct shrinker *shrink; |
| int err; |
| |
| shrinker = kzalloc_obj(*shrinker); |
| if (!shrinker) |
| return ERR_PTR(-ENOMEM); |
| |
| shrink = shrinker_alloc(0, "drm-drm_pagemap:%s", drm->unique); |
| if (!shrink) { |
| kfree(shrinker); |
| return ERR_PTR(-ENOMEM); |
| } |
| |
| spin_lock_init(&shrinker->lock); |
| INIT_LIST_HEAD(&shrinker->dpagemaps); |
| shrinker->drm = drm; |
| shrinker->shrink = shrink; |
| shrink->count_objects = drm_pagemap_shrinker_count; |
| shrink->scan_objects = drm_pagemap_shrinker_scan; |
| shrink->private_data = shrinker; |
| shrinker_register(shrink); |
| |
| err = devm_add_action_or_reset(drm->dev, drm_pagemap_shrinker_fini, shrinker); |
| if (err) |
| return ERR_PTR(err); |
| |
| return shrinker; |
| } |
| EXPORT_SYMBOL(drm_pagemap_shrinker_create_devm); |
| |
| /** |
| * struct drm_pagemap_owner - Device interconnect group |
| * @kref: Reference count. |
| * |
| * A struct drm_pagemap_owner identifies a device interconnect group. |
| */ |
| struct drm_pagemap_owner { |
| struct kref kref; |
| }; |
| |
| static void drm_pagemap_owner_release(struct kref *kref) |
| { |
| kfree(container_of(kref, struct drm_pagemap_owner, kref)); |
| } |
| |
| /** |
| * drm_pagemap_release_owner() - Stop participating in an interconnect group |
| * @peer: Pointer to the struct drm_pagemap_peer used when joining the group |
| * |
| * Stop participating in an interconnect group. This function is typically |
| * called when a pagemap is removed to indicate that it doesn't need to |
| * be taken into account. |
| */ |
| void drm_pagemap_release_owner(struct drm_pagemap_peer *peer) |
| { |
| struct drm_pagemap_owner_list *owner_list = peer->list; |
| |
| if (!owner_list) |
| return; |
| |
| mutex_lock(&owner_list->lock); |
| list_del(&peer->link); |
| kref_put(&peer->owner->kref, drm_pagemap_owner_release); |
| peer->owner = NULL; |
| mutex_unlock(&owner_list->lock); |
| } |
| EXPORT_SYMBOL(drm_pagemap_release_owner); |
| |
| /** |
| * typedef interconnect_fn - Callback function to identify fast interconnects |
| * @peer1: First endpoint. |
| * @peer2: Second endpont. |
| * |
| * The function returns %true iff @peer1 and @peer2 have a fast interconnect. |
| * Note that this is symmetrical. The function has no notion of client and provider, |
| * which may not be sufficient in some cases. However, since the callback is intended |
| * to guide in providing common pagemap owners, the notion of a common owner to |
| * indicate fast interconnects would then have to change as well. |
| * |
| * Return: %true iff @peer1 and @peer2 have a fast interconnect. Otherwise @false. |
| */ |
| typedef bool (*interconnect_fn)(struct drm_pagemap_peer *peer1, struct drm_pagemap_peer *peer2); |
| |
| /** |
| * drm_pagemap_acquire_owner() - Join an interconnect group |
| * @peer: A struct drm_pagemap_peer keeping track of the device interconnect |
| * @owner_list: Pointer to the owner_list, keeping track of all interconnects |
| * @has_interconnect: Callback function to determine whether two peers have a |
| * fast local interconnect. |
| * |
| * Repeatedly calls @has_interconnect for @peer and other peers on @owner_list to |
| * determine a set of peers for which @peer has a fast interconnect. That set will |
| * have common &struct drm_pagemap_owner, and upon successful return, @peer::owner |
| * will point to that struct, holding a reference, and @peer will be registered in |
| * @owner_list. If @peer doesn't have any fast interconnects to other @peers, a |
| * new unique &struct drm_pagemap_owner will be allocated for it, and that |
| * may be shared with other peers that, at a later point, are determined to have |
| * a fast interconnect with @peer. |
| * |
| * When @peer no longer participates in an interconnect group, |
| * drm_pagemap_release_owner() should be called to drop the reference on the |
| * struct drm_pagemap_owner. |
| * |
| * Return: %0 on success, negative error code on failure. |
| */ |
| int drm_pagemap_acquire_owner(struct drm_pagemap_peer *peer, |
| struct drm_pagemap_owner_list *owner_list, |
| interconnect_fn has_interconnect) |
| { |
| struct drm_pagemap_peer *cur_peer; |
| struct drm_pagemap_owner *owner = NULL; |
| bool interconnect = false; |
| |
| mutex_lock(&owner_list->lock); |
| might_alloc(GFP_KERNEL); |
| list_for_each_entry(cur_peer, &owner_list->peers, link) { |
| if (cur_peer->owner != owner) { |
| if (owner && interconnect) |
| break; |
| owner = cur_peer->owner; |
| interconnect = true; |
| } |
| if (interconnect && !has_interconnect(peer, cur_peer)) |
| interconnect = false; |
| } |
| |
| if (!interconnect) { |
| owner = kmalloc_obj(*owner); |
| if (!owner) { |
| mutex_unlock(&owner_list->lock); |
| return -ENOMEM; |
| } |
| kref_init(&owner->kref); |
| list_add_tail(&peer->link, &owner_list->peers); |
| } else { |
| kref_get(&owner->kref); |
| list_add_tail(&peer->link, &cur_peer->link); |
| } |
| peer->owner = owner; |
| peer->list = owner_list; |
| mutex_unlock(&owner_list->lock); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(drm_pagemap_acquire_owner); |