From: "Thomas Hellström" <thomas.hellstrom@linux.intel.com>
To: intel-xe@lists.freedesktop.org
Cc: "Thomas Hellström" <thomas.hellstrom@linux.intel.com>
Subject: [RFC PATCH] dma-buf: Add generic dma_resv wound-wait transaction API
Date: Fri, 5 Jun 2026 13:26:58 +0200 [thread overview]
Message-ID: <20260605112700.181040-2-thomas.hellstrom@linux.intel.com> (raw)
In-Reply-To: <20260605112700.181040-1-thomas.hellstrom@linux.intel.com>
Introduce dma_resv_txn, a subsystem-agnostic wound-wait locking
protocol for sets of dma_resv objects. It is a lower-level analogue
of drm_exec, usable with any object that embeds or references a
dma_resv — not just GEM buffers.
Each participant type provides an ops vtable with callbacks to retrieve
the associated dma_resv and release the object reference.
dma_resv_txn_force_retry() allows callers to trigger an unconditional
retry to recover from non-ww errors such as memory pressure.
Assisted-by: GitHub_Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
drivers/dma-buf/Makefile | 2 +-
drivers/dma-buf/dma-resv-txn.c | 300 +++++++++++++++++++++++++++++++++
include/linux/dma-resv-txn.h | 223 ++++++++++++++++++++++++
3 files changed, 524 insertions(+), 1 deletion(-)
create mode 100644 drivers/dma-buf/dma-resv-txn.c
create mode 100644 include/linux/dma-resv-txn.h
diff --git a/drivers/dma-buf/Makefile b/drivers/dma-buf/Makefile
index b25d7550bacf..6ea13c4bc896 100644
--- a/drivers/dma-buf/Makefile
+++ b/drivers/dma-buf/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
-obj-y := dma-buf.o dma-fence.o dma-fence-array.o dma-fence-chain.o \
+obj-y := dma-resv-txn.o dma-buf.o dma-fence.o dma-fence-array.o dma-fence-chain.o \
dma-fence-unwrap.o dma-resv.o dma-buf-mapping.o
obj-$(CONFIG_DMABUF_HEAPS) += dma-heap.o
obj-$(CONFIG_DMABUF_HEAPS) += heaps/
diff --git a/drivers/dma-buf/dma-resv-txn.c b/drivers/dma-buf/dma-resv-txn.c
new file mode 100644
index 000000000000..3f86add4f700
--- /dev/null
+++ b/drivers/dma-buf/dma-resv-txn.c
@@ -0,0 +1,300 @@
+// SPDX-License-Identifier: GPL-2.0 OR MIT
+/*
+ * Copyright (C) 2022 Christian König <christian.koenig@amd.com>
+ * Copyright (C) 2023 Rob Clark <robdclark@chromium.org>
+ * Copyright (C) 2023-2026 Thomas Hellström <thomas.hellstrom@linux.intel.com>
+ * Copyright (C) 2024 Danilo Krummrich <dakr@kernel.org>
+ * Copyright (C) 2025 Thomas Zimmermann <tzimmermann@suse.de>
+ *
+ * Based on drivers/gpu/drm/drm_exec.c
+ */
+
+#include <linux/dma-resv-txn.h>
+#include <linux/dma-resv.h>
+#include <linux/export.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+
+/**
+ * DOC: Overview
+ *
+ * dma_resv_txn provides a generic wound-wait locking protocol for sets of
+ * dma_resv objects. It is a lower-level, subsystem-agnostic analogue of
+ * drm_exec, usable with any object that embeds or references a dma_resv —
+ * not just GEM buffers.
+ *
+ * The caller must take a reference to each owning object before calling
+ * dma_resv_txn_lock(); that reference is unconditionally consumed by the
+ * call regardless of the return value:
+ *
+ * * On success the reference is stored in the array slot; the transaction
+ * releases it via @ops->put when the slot is removed.
+ * * On -EDEADLK the reference is transferred to @txn->contended and held
+ * across the unlock/retry cycle.
+ * * On all other returns the reference is released immediately via @ops->put.
+ */
+
+static void txn_obj_put(struct dma_resv_txn_obj *obj)
+{
+ if (obj->ops && obj->ops->put)
+ obj->ops->put(obj);
+}
+
+static void txn_unlock_all(struct dma_resv_txn *txn)
+{
+ while (txn->num_objects) {
+ struct dma_resv_txn_obj *obj = &txn->objects[--txn->num_objects];
+
+ dma_resv_unlock(obj->ops->resv(obj));
+ txn_obj_put(obj);
+ }
+
+ if (txn->prelocked.ops) {
+ dma_resv_unlock(txn->prelocked.ops->resv(&txn->prelocked));
+ txn_obj_put(&txn->prelocked);
+ txn->prelocked.ops = NULL;
+ }
+}
+
+static int txn_obj_append(struct dma_resv_txn *txn, void *object,
+ const struct dma_resv_txn_obj_ops *ops)
+{
+ if (unlikely(txn->num_objects == txn->max_objects)) {
+ size_t old_size = txn->max_objects * sizeof(*txn->objects);
+ void *tmp;
+
+ tmp = kvrealloc(txn->objects, old_size + PAGE_SIZE, GFP_KERNEL);
+ if (!tmp)
+ return -ENOMEM;
+
+ txn->objects = tmp;
+ txn->max_objects += PAGE_SIZE / sizeof(*txn->objects);
+ }
+
+ txn->objects[txn->num_objects++] = (struct dma_resv_txn_obj){
+ .ops = ops,
+ .object = object,
+ };
+ return 0;
+}
+
+/**
+ * dma_resv_txn_init() - initialize a transaction
+ * @txn: the transaction to initialize
+ * @flags: DMA_RESV_TXN_* control flags
+ * @nr: initial capacity hint; 0 means use a default
+ *
+ * Prepares the transaction for use.
+ */
+void dma_resv_txn_init(struct dma_resv_txn *txn, u32 flags, unsigned int nr)
+{
+ if (!nr)
+ nr = PAGE_SIZE / sizeof(*txn->objects);
+
+ txn->flags = flags;
+ txn->started = false;
+ txn->objects = kvmalloc_array(nr, sizeof(*txn->objects), GFP_KERNEL);
+ txn->max_objects = txn->objects ? nr : 0;
+ txn->num_objects = 0;
+ txn->contended.ops = NULL;
+ txn->prelocked.ops = NULL;
+}
+EXPORT_SYMBOL_GPL(dma_resv_txn_init);
+
+/**
+ * dma_resv_txn_fini() - finalize a transaction
+ * @txn: the transaction to finalize
+ *
+ * Unlocks all held objects, releases all references, and frees resources.
+ * Safe to call at any point after dma_resv_txn_init().
+ */
+void dma_resv_txn_fini(struct dma_resv_txn *txn)
+{
+ txn_unlock_all(txn);
+ kvfree(txn->objects);
+
+ if (txn->contended.ops) {
+ txn_obj_put(&txn->contended);
+ txn->contended.ops = NULL;
+ }
+
+ if (txn->started)
+ ww_acquire_fini(&txn->ctx);
+}
+EXPORT_SYMBOL_GPL(dma_resv_txn_fini);
+
+/**
+ * dma_resv_txn_cleanup() - loop condition for dma_resv_txn_until_all_locked()
+ * @txn: the transaction
+ *
+ * Not intended for direct use; drive the retry loop with
+ * dma_resv_txn_until_all_locked() instead.
+ *
+ * Return: true if the loop body should run, false when all locks are held.
+ */
+bool dma_resv_txn_cleanup(struct dma_resv_txn *txn)
+{
+ if (!txn->started) {
+ ww_acquire_init(&txn->ctx, &reservation_ww_class);
+ txn->started = true;
+ return true;
+ }
+
+ if (likely(!txn->contended.ops)) {
+ ww_acquire_done(&txn->ctx);
+ return false;
+ }
+
+ txn_unlock_all(txn);
+ return true;
+}
+EXPORT_SYMBOL_GPL(dma_resv_txn_cleanup);
+
+static int txn_lock_contended(struct dma_resv_txn *txn)
+{
+ struct dma_resv_txn_obj *cont = &txn->contended;
+ struct dma_resv *resv;
+ int ret;
+
+ if (!cont->ops)
+ return 0;
+
+ resv = cont->ops->resv(cont);
+
+ if (txn->flags & DMA_RESV_TXN_INTERRUPTIBLE) {
+ ret = dma_resv_lock_slow_interruptible(resv, &txn->ctx);
+ if (unlikely(ret)) {
+ txn_obj_put(cont);
+ cont->ops = NULL;
+ return ret;
+ }
+ } else {
+ dma_resv_lock_slow(resv, &txn->ctx);
+ }
+
+ txn->prelocked = *cont;
+ cont->ops = NULL;
+ return 0;
+}
+
+/**
+ * dma_resv_txn_lock() - lock a dma_resv object under this transaction
+ * @txn: the transaction
+ * @object: opaque pointer to the owning object; stored for iteration
+ * @ops: operations for this object, including the @resv accessor
+ *
+ * The caller must hold one reference to the owning object before calling.
+ * That reference is always consumed, regardless of the return value:
+ *
+ * * 0 — success; reference stored in the locked-objects array.
+ * * -EDEADLK — contention detected; reference transferred to
+ * @txn->contended. Call dma_resv_txn_retry_on_contention().
+ * * -EALREADY — the resv returned by @ops->resv() is already locked by this
+ * context; reference released via @ops->put. Suppress with
+ * DMA_RESV_TXN_IGNORE_DUPLICATES.
+ * * Other negative — interrupted or allocation failure; reference released
+ * via @ops->put.
+ *
+ * Return: 0 on success, negative error code otherwise.
+ */
+int dma_resv_txn_lock(struct dma_resv_txn *txn, void *object,
+ const struct dma_resv_txn_obj_ops *ops)
+{
+ struct dma_resv_txn_obj caller = { .ops = ops, .object = object };
+ struct dma_resv *resv = ops->resv(&caller);
+ int ret;
+
+ ret = txn_lock_contended(txn);
+ if (unlikely(ret)) {
+ txn_obj_put(&caller);
+ return ret;
+ }
+
+ /*
+ * If the prelocked resv matches, the lock is already held from the
+ * slow path. Consume the caller's ref into the array and release
+ * the separate ref held by the prelocked slot.
+ */
+ if (txn->prelocked.ops &&
+ txn->prelocked.ops->resv(&txn->prelocked) == resv) {
+ ret = txn_obj_append(txn, object, ops);
+ if (unlikely(ret)) {
+ dma_resv_unlock(resv);
+ txn_obj_put(&txn->prelocked);
+ txn->prelocked.ops = NULL;
+ txn_obj_put(&caller);
+ return ret;
+ }
+ txn_obj_put(&txn->prelocked);
+ txn->prelocked.ops = NULL;
+ return 0;
+ }
+
+ if (txn->flags & DMA_RESV_TXN_INTERRUPTIBLE)
+ ret = dma_resv_lock_interruptible(resv, &txn->ctx);
+ else
+ ret = dma_resv_lock(resv, &txn->ctx);
+
+ if (unlikely(ret == -EDEADLK)) {
+ /* Transfer caller's ref to the contended slot. */
+ txn->contended = caller;
+ return -EDEADLK;
+ }
+
+ if (unlikely(ret)) {
+ if (ret == -EALREADY &&
+ (txn->flags & DMA_RESV_TXN_IGNORE_DUPLICATES))
+ ret = 0;
+ txn_obj_put(&caller);
+ return ret;
+ }
+
+ ret = txn_obj_append(txn, object, ops);
+ if (unlikely(ret)) {
+ dma_resv_unlock(resv);
+ txn_obj_put(&caller);
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(dma_resv_txn_lock);
+
+/**
+ * dma_resv_txn_unlock() - unlock and remove a single object
+ * @txn: the transaction
+ * @object: opaque pointer identifying the object to unlock
+ *
+ * Scans the locked-objects array from the tail for an entry whose @object
+ * field matches @object, unlocks its resv, releases its reference via
+ * @ops->put, and compacts the array. Scanning from the tail is efficient
+ * for the common case of unlocking the most recently locked objects.
+ */
+void dma_resv_txn_unlock(struct dma_resv_txn *txn, void *object)
+{
+ unsigned int i;
+
+ for (i = txn->num_objects; i--;) {
+ struct dma_resv_txn_obj *obj = &txn->objects[i];
+
+ if (obj->object != object)
+ continue;
+
+ dma_resv_unlock(obj->ops->resv(obj));
+ txn_obj_put(obj);
+
+ /* Compact: shift remaining entries down by one. */
+ for (++i; i < txn->num_objects; ++i)
+ txn->objects[i - 1] = txn->objects[i];
+ --txn->num_objects;
+ return;
+ }
+
+ WARN(1, "%s: object %p not found in transaction\n", __func__, object);
+}
+EXPORT_SYMBOL_GPL(dma_resv_txn_unlock);
+
+MODULE_DESCRIPTION("dma-resv transaction locking");
+MODULE_LICENSE("Dual MIT/GPL");
+
diff --git a/include/linux/dma-resv-txn.h b/include/linux/dma-resv-txn.h
new file mode 100644
index 000000000000..26be05db51eb
--- /dev/null
+++ b/include/linux/dma-resv-txn.h
@@ -0,0 +1,223 @@
+/* SPDX-License-Identifier: GPL-2.0 OR MIT */
+/*
+ * Copyright (C) 2022 Christian König <christian.koenig@amd.com>
+ * Copyright (C) 2023 Rob Clark <robdclark@chromium.org>
+ * Copyright (C) 2023-2026 Thomas Hellström <thomas.hellstrom@linux.intel.com>
+ *
+ * Based on include/drm/drm_exec.h
+ */
+
+#ifndef __LINUX_DMA_RESV_TXN_H
+#define __LINUX_DMA_RESV_TXN_H
+
+#include <linux/compiler.h>
+#include <linux/types.h>
+#include <linux/ww_mutex.h>
+
+struct dma_resv;
+
+/**
+ * define DMA_RESV_TXN_INTERRUPTIBLE - use interruptible waits
+ *
+ * Use interruptible variants of the ww_mutex locking functions.
+ */
+#define DMA_RESV_TXN_INTERRUPTIBLE BIT(0)
+
+/**
+ * define DMA_RESV_TXN_IGNORE_DUPLICATES - silently skip already-locked resvs
+ *
+ * When a dma_resv is already held by this transaction (e.g. because multiple
+ * objects share the same dma_resv), return 0 instead of -EALREADY.
+ */
+#define DMA_RESV_TXN_IGNORE_DUPLICATES BIT(1)
+
+/**
+ * struct dma_resv_txn_obj - one participant in a transaction
+ *
+ * Represents an object registered with a transaction. @object is opaque
+ * to the transaction and available to the @ops callbacks.
+ */
+struct dma_resv_txn_obj {
+ /** @ops: operations for this object; NULL marks an unoccupied slot */
+ const struct dma_resv_txn_obj_ops *ops;
+ /** @object: opaque pointer to the owning object */
+ void *object;
+};
+
+/**
+ * struct dma_resv_txn_obj_ops - per-participant operations
+ *
+ * @resv is mandatory; @put is optional.
+ */
+struct dma_resv_txn_obj_ops {
+ /**
+ * @resv: return the &dma_resv associated with @obj.
+ *
+ * Must not return NULL. Required.
+ */
+ struct dma_resv *(*resv)(struct dma_resv_txn_obj *obj);
+ /**
+ * @put: release the caller's reference to the owning object.
+ *
+ * Use @obj->object to identify the owner. May be NULL.
+ */
+ void (*put)(struct dma_resv_txn_obj *obj);
+};
+
+/**
+ * struct dma_resv_txn - wound-wait transaction over dma_resv objects
+ *
+ * Manages the wound-wait retry loop required to acquire multiple dma_resv
+ * locks without deadlock. Drive with dma_resv_txn_until_all_locked().
+ *
+ * The caller must take a reference to each owning object before calling
+ * dma_resv_txn_lock(); that reference is unconditionally consumed by the
+ * call regardless of the return value.
+ */
+struct dma_resv_txn {
+ /** @flags: DMA_RESV_TXN_* control flags */
+ u32 flags;
+ /** @started: wound-wait context active */
+ bool started;
+ /** @ctx: wound-wait acquire context (ticket) */
+ struct ww_acquire_ctx ctx;
+ /**
+ * @num_objects: number of objects currently in @objects
+ */
+ unsigned int num_objects;
+ /**
+ * @max_objects: allocated capacity of @objects
+ */
+ unsigned int max_objects;
+ /**
+ * @objects: array of locked object descriptors.
+ */
+ struct dma_resv_txn_obj *objects;
+ /**
+ * @contended: object whose resv caused -EDEADLK; held until the next retry.
+ */
+ struct dma_resv_txn_obj contended;
+ /**
+ * @prelocked: object acquired via the slow path, pending claim by the
+ * next matching dma_resv_txn_lock() call.
+ */
+ struct dma_resv_txn_obj prelocked;
+};
+
+void dma_resv_txn_init(struct dma_resv_txn *txn, u32 flags, unsigned int nr);
+void dma_resv_txn_fini(struct dma_resv_txn *txn);
+bool dma_resv_txn_cleanup(struct dma_resv_txn *txn);
+int dma_resv_txn_lock(struct dma_resv_txn *txn, void *object,
+ const struct dma_resv_txn_obj_ops *ops);
+void dma_resv_txn_unlock(struct dma_resv_txn *txn, void *object);
+
+/* Internal helpers for the iteration macros; do not use directly. */
+#define __dma_resv_txn_for_each_obj_ops(txn, obj, __ops, __curs) \
+ for (struct dma_resv_txn_obj *__curs = (txn)->objects; \
+ __curs < &(txn)->objects[(txn)->num_objects] && \
+ ((obj) = __curs->object, true); \
+ ++__curs) \
+ if (unlikely(__curs->ops != (__ops))) {} else
+
+#define __dma_resv_txn_for_each_obj_ops_reverse(txn, obj, __ops, __idx) \
+ for (unsigned long __idx = (txn)->num_objects; \
+ __idx-- > 0 && \
+ ((obj) = (txn)->objects[__idx].object, true); \
+ ) \
+ if (unlikely((txn)->objects[__idx].ops != (__ops))) {} else
+
+/**
+ * dma_resv_txn_for_each_obj_ops - iterate over locked objects with matching ops
+ * @txn: the transaction
+ * @obj: cursor (void *), set to the @object field of each matching slot
+ * @ops: pointer to the &dma_resv_txn_obj_ops to match against
+ *
+ * Iterate over all locked objects in @txn whose @ops pointer matches @ops.
+ * Useful for subsystems that share a transaction and need to visit only
+ * their own objects.
+ */
+#define dma_resv_txn_for_each_obj_ops(txn, obj, ops) \
+ __dma_resv_txn_for_each_obj_ops(txn, obj, ops, __UNIQUE_ID(__drt_c))
+
+/**
+ * dma_resv_txn_for_each_obj_ops_reverse - iterate in reverse lock order
+ * @txn: the transaction
+ * @obj: cursor (void *), set to the @object field of each matching slot
+ * @ops: pointer to the &dma_resv_txn_obj_ops to match against
+ *
+ * Like dma_resv_txn_for_each_obj_ops() but in reverse acquisition order.
+ * Required when unlocking.
+ */
+#define dma_resv_txn_for_each_obj_ops_reverse(txn, obj, ops) \
+ __dma_resv_txn_for_each_obj_ops_reverse(txn, obj, ops, \
+ __UNIQUE_ID(__drt_c))
+
+/* Internal helper for dma_resv_txn_until_all_locked(). Do not use directly. */
+#define __dma_resv_txn_until_all_locked(txn, _label) \
+_label: \
+ for (void *const __maybe_unused __txn_retry_ptr = &&_label; \
+ dma_resv_txn_cleanup(txn);)
+
+/**
+ * dma_resv_txn_until_all_locked() - retry loop until all objects locked
+ * @txn: the transaction
+ *
+ * Drives the wound-wait retry loop. The body is re-entered as long as
+ * contention is detected; at the start of each iteration no locks are held.
+ * Use dma_resv_txn_retry_on_contention() after each lock call.
+ *
+ * Example::
+ *
+ * dma_resv_txn_until_all_locked(&txn) {
+ * ret = dma_resv_txn_lock(&txn, obj_a, &my_ops);
+ * dma_resv_txn_retry_on_contention(&txn);
+ * if (ret) goto error;
+ *
+ * ret = dma_resv_txn_lock(&txn, obj_b, &my_ops);
+ * dma_resv_txn_retry_on_contention(&txn);
+ * if (ret) goto error;
+ * }
+ */
+#define dma_resv_txn_until_all_locked(txn) \
+ __dma_resv_txn_until_all_locked(txn, __UNIQUE_ID(dma_resv_txn_l))
+
+/**
+ * dma_resv_txn_retry_on_contention() - restart loop if contention set
+ * @txn: the transaction
+ *
+ * Must be called after each dma_resv_txn_lock() inside the
+ * dma_resv_txn_until_all_locked() body.
+ */
+#define dma_resv_txn_retry_on_contention(txn) \
+ do { \
+ if (unlikely(dma_resv_txn_is_contended(txn))) \
+ goto *__txn_retry_ptr; \
+ } while (0)
+
+/**
+ * dma_resv_txn_force_retry() - unconditionally restart the retry loop
+ * @txn: the transaction
+ *
+ * Force a fresh retry iteration regardless of ww contention. Use this to
+ * unwind the current locking round after a non-ww error (e.g. memory
+ * pressure). Must be called inside dma_resv_txn_until_all_locked() and
+ * only when no contention is pending.
+ */
+#define dma_resv_txn_force_retry(txn) \
+ do { \
+ WARN_ON(dma_resv_txn_is_contended(txn)); \
+ goto *__txn_retry_ptr; \
+ } while (0)
+
+/**
+ * dma_resv_txn_is_contended() - check whether contention was detected
+ * @txn: the transaction
+ *
+ * Return: true if contention was detected and the retry loop must restart.
+ */
+static inline bool dma_resv_txn_is_contended(struct dma_resv_txn *txn)
+{
+ return !!txn->contended.ops;
+}
+
+#endif /* __LINUX_DMA_RESV_TXN_H */
--
2.54.0
next prev parent reply other threads:[~2026-06-05 11:27 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-05 11:26 [RFC PATCH 0/3] Use a dma_resv wound-wait transaction API for drm_exec Thomas Hellström
2026-06-05 11:26 ` Thomas Hellström [this message]
2026-06-05 11:26 ` [RFC PATCH] drm/exec: Rebuild drm_exec on top of dma_resv_txn Thomas Hellström
2026-06-05 11:27 ` [RFC PATCH] drm/xe/tests: Add drm_exec locking benchmark kunit test Thomas Hellström
2026-06-05 12:16 ` ✗ CI.checkpatch: warning for " Patchwork
2026-06-05 12:18 ` ✓ CI.KUnit: success " Patchwork
2026-06-05 13:13 ` ✓ Xe.CI.BAT: " Patchwork
2026-06-05 23:49 ` ✓ Xe.CI.FULL: " Patchwork
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=20260605112700.181040-2-thomas.hellstrom@linux.intel.com \
--to=thomas.hellstrom@linux.intel.com \
--cc=intel-xe@lists.freedesktop.org \
/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