Linux Documentation
 help / color / mirror / Atom feed
From: Konstantin Sinyuk <ksinyuk@kernel.org>
To: dri-devel@lists.freedesktop.org
Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
	Francois Dugast <francois.dugast@intel.com>,
	David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
	Maxime Ripard <mripard@kernel.org>,
	Thomas Zimmermann <tzimmermann@suse.de>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Donald Hunter <donald.hunter@gmail.com>,
	Jakub Kicinski <kuba@kernel.org>,
	"David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
	Ilia Levi <ilia.levi@intel.com>,
	Rodrigo Vivi <rodrigo.vivi@intel.com>,
	linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [RFC PATCH 07/12] drm/fabric: add topology-provisioning core
Date: Mon, 24 Aug 2026 11:09:35 +0300	[thread overview]
Message-ID: <0ecb1eb1209aa1ff0a58f5a23f5ba3014277690a.1787552412.git.ksinyuk@kernel.org> (raw)
In-Reply-To: <cover.1787552412.git.ksinyuk@kernel.org>

From: Ilia Levi <ilia.levi@intel.com>

Everything so far reports topology that a provider composed. Composing it
from userspace needs the core to create an empty fabric, move an endpoint
into or out of one, change administrative state and record a peer. None of
that may leave the object model half-updated.

Add the core mechanisms without exposing provisioning netlink operations.
Deleting a provider-owned or non-empty fabric is refused.

Provider callbacks may sleep and therefore run without drm_fabric_lock.
drm_fabric_mutation_lock serialises validation, the provider callback and
the core update, with targets pinned across the unlocked callback.
Registration and teardown join the same transaction domain, so a successful
callback cannot be invalidated before commit.

Administrative intent stays separate from operational state, and each port
selects one peer writer at registration. A request that already matches
committed state succeeds without invoking the provider.

Registering an orphan endpoint is now valid, so the KUnit case asserting
that a NULL fabric is rejected is repurposed to assert that it yields an
orphan reporting fabric-id 0.

Signed-off-by: Ilia Levi <ilia.levi@intel.com>
Co-developed-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Signed-off-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
 Documentation/gpu/drm-fabric.rst             |  93 ++--
 drivers/gpu/drm/fabric/drm_fabric.c          | 554 +++++++++++++++----
 drivers/gpu/drm/fabric/drm_fabric_internal.h |  13 +
 drivers/gpu/drm/fabric/drm_fabric_test.c     |  17 +-
 include/drm/drm_fabric.h                     | 108 +++-
 5 files changed, 637 insertions(+), 148 deletions(-)

diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index bc7b87c766cc..61e2c18bf0b5 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -18,6 +18,8 @@ Key Goals:
   (xGMI, UALink and similar), enabling data-center discovery and monitoring.
 * Support read-only enumeration, monitoring and state queries for
   provider-owned topology.
+* Support topology provisioning as an optional provider capability, covering
+  fabric and endpoint lifecycle, port administration and peer management.
 * Allow new attributes and fabric types to be added without reusing existing
   wire identifiers, so the uAPI extends without breaking existing consumers.
 * Allow multiple endpoints and ports per provider, so drivers can model
@@ -56,13 +58,14 @@ provider reports: for example, it may be a full mesh with no root, a linear
 chain, or a switch-based topology in which ports terminate at opaque switch
 peers rather than locally registered endpoints.
 
-A *peer* is a value descriptor, not a reference to a live kernel object: its
-``peer-id`` may name a remote accelerator managed by another OS or an opaque
-switch in another trust domain, and need not resolve in the local registry. The
-core stores one directed half-edge and does not require the reverse half-edge to
-exist, so removing an endpoint does not retract peer descriptors held by other
-endpoints. ``peer-type = switch`` only describes the kind of far end; it does
-not create a first-class switch object.
+An endpoint may be registered without a fabric. Such an endpoint is an *orphan*
+and reports ``fabric-id`` 0. A *peer* is a value descriptor, not a reference to
+a live kernel object: its ``peer-id`` may name a remote accelerator managed by
+another OS or an opaque switch in another trust domain, and need not resolve in
+the local registry. The core stores one directed half-edge and does not require
+the reverse half-edge to exist, so removing an endpoint does not retract peer
+descriptors held by other endpoints. ``peer-type = switch`` only describes the
+kind of far end; it does not create a first-class switch object.
 
 .. kernel-doc:: drivers/gpu/drm/fabric/drm_fabric.c
    :doc: DRM Fabric core
@@ -79,10 +82,10 @@ that half-edge. A peer is therefore topology as last set, not proof of live
 connectivity; liveness belongs to the fabric controller.
 
 The core never retracts a half-edge on its own. Failing to resolve a peer
-locally is not the same as the link going away -- the far end may be a switch,
-an accelerator on another node, or a local endpoint that merely unregistered
--- so only the provider knows when a port's physical adjacency actually
-changed, and only the provider retracts or replaces the descriptor.
+locally is not the same as the link going away -- the far end may be a
+switch, an accelerator on another node, or a local endpoint that merely
+unregistered -- so only the provider knows when a port's physical adjacency
+actually changed, and only the provider retracts or replaces the descriptor.
 
 ``port-peer-delete-ntf`` reports an explicitly retracted half-edge; it is
 not emitted when a peer merely becomes locally unresolvable, so its absence
@@ -105,11 +108,15 @@ Driver API
 Design scope and boundaries
 ===========================
 
-Vendor drivers retain hardware discovery, firmware interaction and the
-load/store data path; DRM Fabric represents only the topology and
-provider-reported state of DRM-managed accelerators, which is why it
-belongs in DRM. The interface does not define MMU programming, switch
-policy, key management, live migration, or any required user space daemon.
+The core records direct adjacency only, not end-to-end reachability or switch
+forwarding, which remain with the fabric controller. Vendor drivers retain
+hardware discovery, firmware interaction, memory semantics and the
+hardware-carried data path; DRM Fabric represents only the topology and
+control state of DRM-managed accelerators, which is why it belongs in DRM.
+It does not create a network device or own route computation, switch
+forwarding, transport or congestion control. The interface also does not
+define MMU programming, switch policy, key management, live migration, or
+any required user space daemon.
 
 DRM Fabric does not define in-network collective operations or how an
 endpoint or switch executes them. Such capabilities belong to the
@@ -123,27 +130,49 @@ strengthens the contract rather than breaking it.
 Object lifetime and locking
 ===========================
 
-All registry and object state is protected by ``drm_fabric_lock``. A fabric and
-its endpoints are created and torn down through the provider API; endpoint
-unregister removes the endpoint from the registry and frees its fixed set of
-ports. Fabric membership is tracked, so a provider must remove all member
-endpoints before unregistering a provider-owned fabric: drm_fabric_unregister()
-returns ``-EBUSY`` and leaves the fabric registered if any remain, so the
-provider must retry after removing them rather than treat the fabric as gone.
+Registry membership and object state are protected by ``drm_fabric_lock``;
+topology mutation is additionally serialised by ``drm_fabric_mutation_lock``,
+described below. A fabric and its endpoints are created and torn down through
+the provider API; endpoint unregister removes the endpoint from the registry
+and frees its fixed set of ports. Fabric membership is tracked, so a provider
+must remove all member endpoints before unregistering a provider-owned fabric:
+drm_fabric_unregister() returns ``-EBUSY`` and leaves the fabric registered if
+any remain, so the provider must retry after removing them rather than treat
+the fabric as gone.
 
 Objects are reference counted and a port is pinned through its owning endpoint.
 Endpoint unregister drops the registration reference and waits for outstanding
 pins before freeing the ports; fabric membership holds a fabric reference.
 
-Providers own object lifetime, so a provider must serialise endpoint
-registration against unregistration of the containing fabric. The unregister
-entry points compare the supplied pointer against the registry before
-dereferencing it, so a stale or repeated teardown is rejected: fabric
-unregistration returns ``-ENODEV``, and endpoint unregistration, having no
-error return, warns and performs no teardown. Endpoint registration rejects a
-departed parent the same way. These checks prove current address membership
-only: they cannot tell an earlier incarnation from another object registered
-later at the same address.
+Providers own object lifetime. The unregister entry points compare the supplied
+pointer against the registry before dereferencing it, so a stale or repeated
+teardown is rejected: fabric unregistration returns ``-ENODEV``, and endpoint
+unregistration, having no error return, warns and performs no teardown.
+Endpoint registration rejects a departed parent the same way. These checks
+prove current address membership only: they cannot tell an earlier incarnation
+from another object registered later at the same address. Only the provider
+knows its own object lifecycle.
+
+Netlink mutation commands additionally hold ``drm_fabric_mutation_lock`` across
+target resolution, the provider callback and the core commit. The lock order
+is::
+
+    drm_fabric_mutation_lock -> drm_fabric_lock
+
+Provider lifecycle operations that can invalidate a prepared mutation take the
+same mutation lock: endpoint registration as well as endpoint and fabric
+unregister. A provider-driven teardown therefore cannot race an in-flight
+mutation on the same object, and a registration cannot claim a fabric-scoped
+``fabric-ep-id`` after an attach has validated the identifier but before its
+provider callback completes. A provider must not invoke any of these lifecycle
+operations from one of its own mutation callbacks: the callback already holds
+the mutation lock, so the nested acquisition would self-deadlock. A successful
+callback therefore cannot be invalidated before commit, and the post-callback
+checks are invariant assertions only.
+
+A fabric records whether it was created by a provider or by userspace. Only an
+empty userspace-created fabric may be deleted through the provisioning core;
+provider-owned and non-empty fabrics are rejected.
 
 Generic Netlink family
 ======================
diff --git a/drivers/gpu/drm/fabric/drm_fabric.c b/drivers/gpu/drm/fabric/drm_fabric.c
index bbc3224c3314..1aae5ff68798 100644
--- a/drivers/gpu/drm/fabric/drm_fabric.c
+++ b/drivers/gpu/drm/fabric/drm_fabric.c
@@ -25,14 +25,23 @@
  * drm_fabric_port_unset_peer().
  *
  * The core owns object identity and lifetime: it assigns kernel-local IDs,
- * refcounts objects, serialises access under its internal lock, and advances a
- * topology generation on every change so a concurrent netlink dump can detect
- * a torn snapshot.
+ * refcounts objects, serialises object state under drm_fabric_lock and
+ * topology mutation under drm_fabric_mutation_lock, and advances a topology
+ * generation on every change so a concurrent netlink dump can detect a torn
+ * snapshot. See Documentation/gpu/drm-fabric.rst for the fuller object
+ * lifetime and locking treatment.
  */
 
 /* Global lock for all fabric, endpoint and port state. */
 DEFINE_MUTEX(drm_fabric_lock);
 
+/*
+ * Lock order is mutation_lock -> drm_fabric_lock; a provider callback already
+ * holds this and must not re-enter registration/unregistration.
+ */
+DEFINE_MUTEX(drm_fabric_mutation_lock);
+
+/* ID 0 is the orphan sentinel, so allocated IDs start at 1. */
 DEFINE_XARRAY_ALLOC1(drm_fabric_xa);
 DEFINE_XARRAY_ALLOC(drm_fabric_ep_xa);
 
@@ -101,10 +110,8 @@ drm_fabric_endpoint_find_by_dev_name(const char *devname, const char *busname)
 }
 
 /*
- * Returns true if a member of @fabric already uses @fabric_ep_id. fabric_ep_id
- * is the accelerator's identity within a fabric and is what a peer descriptor
- * names (peer_id for DRM_FABRIC_PEER_TYPE_ACCEL), so it must be unique per
- * fabric or peer resolution is ambiguous.
+ * fabric_ep_id must be unique per fabric or peer resolution is ambiguous;
+ * orphans (@fabric == NULL) do not participate.
  */
 static bool drm_fabric_ep_id_in_use(const struct drm_fabric *fabric, u64 fabric_ep_id)
 {
@@ -113,6 +120,9 @@ static bool drm_fabric_ep_id_in_use(const struct drm_fabric *fabric, u64 fabric_
 
 	lockdep_assert_held(&drm_fabric_lock);
 
+	if (!fabric)
+		return false;
+
 	xa_for_each(&drm_fabric_ep_xa, idx, ep)
 		if (ep->fabric == fabric &&
 		    ep->fabric_ep_id == fabric_ep_id)
@@ -136,40 +146,20 @@ static bool drm_fabric_has_instance(const struct drm_fabric *fabric)
 	return false;
 }
 
-static bool drm_fabric_type_valid(enum drm_fabric_type type)
-{
-	switch (type) {
-	case DRM_FABRIC_TYPE_SYNTHETIC:
-		return true;
-	}
-
-	return false;
-}
-
-/**
- * drm_fabric_register() - Register a new fabric
- * @desc: fabric description (type, instance id, name)
- *
- * Allocates the fabric and inserts it into the registry as provider-owned.
- * Rejects zero and out-of-range types before allocating.
- *
- * Context: May sleep. Acquires drm_fabric_lock.
- * Return: the registered fabric, or an ERR_PTR() on failure, -EINVAL for a
- *         type this kernel does not define.
- */
-struct drm_fabric *drm_fabric_register(const struct drm_fabric_desc *desc)
+/* @owner is set once here, before publication, so no reader needs a lock for it. */
+static struct drm_fabric *
+__drm_fabric_register(const struct drm_fabric_desc *desc,
+		      enum drm_fabric_owner owner)
 {
 	struct drm_fabric *fabric __free(kfree) = NULL;
 
-	if (WARN_ON_ONCE(!drm_fabric_type_valid(desc->type)))
-		return ERR_PTR(-EINVAL);
-
 	fabric = kzalloc_obj(*fabric);
 	if (!fabric)
 		return ERR_PTR(-ENOMEM);
 
 	fabric->type = desc->type;
 	fabric->instance_id = desc->instance_id;
+	fabric->owner = owner;
 	refcount_set(&fabric->refs, 1);
 
 	if (desc->name &&
@@ -191,6 +181,35 @@ struct drm_fabric *drm_fabric_register(const struct drm_fabric_desc *desc)
 
 	return_ptr(fabric);
 }
+
+static bool drm_fabric_type_valid(enum drm_fabric_type type)
+{
+	switch (type) {
+	case DRM_FABRIC_TYPE_SYNTHETIC:
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * drm_fabric_register() - Register a new fabric
+ * @desc: fabric description (type, instance id, name)
+ *
+ * Allocates the fabric and inserts it into the registry as provider-owned.
+ * Rejects zero and out-of-range types before allocating.
+ *
+ * Context: May sleep. Acquires drm_fabric_lock.
+ * Return: the registered fabric, or an ERR_PTR() on failure, -EINVAL for a
+ *         type this kernel does not define.
+ */
+struct drm_fabric *drm_fabric_register(const struct drm_fabric_desc *desc)
+{
+	if (WARN_ON_ONCE(!drm_fabric_type_valid(desc->type)))
+		return ERR_PTR(-EINVAL);
+
+	return __drm_fabric_register(desc, DRM_FABRIC_OWNER_PROVIDER);
+}
 EXPORT_SYMBOL(drm_fabric_register);
 
 struct drm_fabric *drm_fabric_get(struct drm_fabric *fabric)
@@ -253,27 +272,40 @@ static bool drm_fabric_is_registered(const struct drm_fabric *fabric)
  * from another fabric registered later at the same address, which remains
  * the provider's obligation.
  *
- * Context: May sleep. Acquires drm_fabric_lock.
- * Return: 0 on removal. -ENODEV if @fabric is not registered, checked first.
- *         -EBUSY if members remain; the fabric stays registered and the
- *         provider must remove them before retrying. -EBUSY also warns
- *         because this is a teardown-ordering bug.
+ * Context: May sleep. Acquires drm_fabric_mutation_lock, then drm_fabric_lock.
+ *          Must not be called from a provider mutation callback (endpoint_set /
+ *          port_set / port_peer_*), which already holds the mutation lock and
+ *          would self-deadlock.
+ * Return: 0 once the fabric is removed. -ENODEV if @fabric is not currently
+ *         registered, which takes precedence over the member check. -EBUSY
+ *         if member endpoints remain, in which case the fabric stays fully
+ *         registered and the provider must unregister the members before
+ *         retrying -- a non-empty unregister is a provider teardown-ordering
+ *         bug, so it also warns.
  */
 int drm_fabric_unregister(struct drm_fabric *fabric)
 {
-	scoped_guard(mutex, &drm_fabric_lock) {
-		if (!drm_fabric_is_registered(fabric))
-			return -ENODEV;
-		/*
-		 * Members still reference ep->fabric; freeing it here would
-		 * leave stale pointers.
-		 */
-		if (WARN_ON_ONCE(drm_fabric_has_members(fabric)))
-			return -EBUSY;
-		/* Emit before the erase, while @fabric is still live. */
-		drm_fabric_emit_fabric_delete(fabric,
-					      drm_fabric_base_seq_inc());
-		xa_erase(&drm_fabric_xa, fabric->id);
+	/*
+	 * The mutation lock below blocks a racing unregister or endpoint_set attach
+	 * on this fabric.
+	 */
+	lockdep_assert_not_held(&drm_fabric_mutation_lock);
+
+	scoped_guard(mutex, &drm_fabric_mutation_lock) {
+		scoped_guard(mutex, &drm_fabric_lock) {
+			if (!drm_fabric_is_registered(fabric))
+				return -ENODEV;
+			/*
+			 * Members still reference ep->fabric; freeing it here
+			 * would leave stale pointers.
+			 */
+			if (WARN_ON_ONCE(drm_fabric_has_members(fabric)))
+				return -EBUSY;
+			/* Emit before the erase, while @fabric is still live. */
+			drm_fabric_emit_fabric_delete(fabric,
+						      drm_fabric_base_seq_inc());
+			xa_erase(&drm_fabric_xa, fabric->id);
+		}
 	}
 
 	drm_fabric_put(fabric);
@@ -314,6 +346,8 @@ static int drm_fabric_ports_create(struct drm_fabric_endpoint *ep,
 		port->max_lane_signaling_rate_mbps =
 			descs[i].max_lane_signaling_rate_mbps;
 		port->oper_state = DRM_FABRIC_PORT_STATE_UNKNOWN;
+		port->admin_state = DRM_FABRIC_ADMIN_STATE_DOWN;
+		port->peer_mode = descs[i].peer_mode;
 		port->has_peer = false;
 		port->endpoint = ep;
 
@@ -333,23 +367,30 @@ static int drm_fabric_ports_create(struct drm_fabric_endpoint *ep,
 }
 
 /**
- * drm_fabric_endpoint_register() - Register a provider-owned endpoint
- * @fabric: non-NULL fabric the endpoint belongs to
+ * drm_fabric_endpoint_register() - Register an endpoint
+ * @fabric: fabric the endpoint belongs to
  * @desc: endpoint description, including its fixed set of ports
  *
- * Registers @desc as a member of @fabric and advances the topology generation.
- * @desc->fabric_ep_id must be unique among the fabric's registered endpoints.
+ * Registers the endpoint and emits an ENDPOINT_CREATE event. When the endpoint
+ * joins a fabric its desc->fabric_ep_id must be unique among that fabric's
+ * members; a duplicate is rejected with -EEXIST.
  *
- * The provider must serialise this call against drm_fabric_unregister() of
- * @fabric. A fabric that has already left the registry is rejected, but that
- * check matches on address and cannot distinguish incarnations; only the
- * provider knows its own object lifecycle.
+ * A NULL @fabric registers an orphan endpoint, which a later ENDPOINT_SET
+ * attach can join to a fabric.
  *
- * Context: May sleep. Acquires drm_fabric_lock.
+ * A fabric that has already left the registry is rejected, but that check
+ * matches on address and cannot distinguish incarnations; only the provider
+ * knows its own object lifecycle.
+ *
+ * Publication joins the mutation transaction domain
+ * (drm_fabric_mutation_lock -> drm_fabric_lock), so a provider must not call
+ * this from within a mutation callback (that would self-deadlock).
+ *
+ * Context: May sleep. Acquires drm_fabric_mutation_lock, then drm_fabric_lock.
  * Return: the registered endpoint, or an ERR_PTR() on failure: -EINVAL if
- * @fabric or @desc->parent is NULL, or @desc claims ports without supplying a
- * port array, -ENODEV if @fabric is no longer registered, -EEXIST if
- * @desc->fabric_ep_id is already in use within @fabric.
+ * @desc->parent is NULL, or @desc claims ports without supplying a port array,
+ * -ENODEV if @fabric is no longer registered, -EEXIST if @desc->fabric_ep_id is
+ * already in use within @fabric.
  */
 struct drm_fabric_endpoint *
 drm_fabric_endpoint_register(struct drm_fabric *fabric,
@@ -358,8 +399,7 @@ drm_fabric_endpoint_register(struct drm_fabric *fabric,
 	struct drm_fabric_endpoint *ep __free(kfree) = NULL;
 	int ret;
 
-	if (!fabric)
-		return ERR_PTR(-EINVAL);
+	lockdep_assert_not_held(&drm_fabric_mutation_lock);
 
 	/* Supplies dev_name()/bus for the query paths; pinned below. */
 	if (!desc->parent)
@@ -377,6 +417,7 @@ drm_fabric_endpoint_register(struct drm_fabric *fabric,
 	ep->parent = desc->parent;
 	ep->ops = desc->ops;
 	ep->priv = desc->priv;
+	ep->admin_state = fabric ? DRM_FABRIC_ADMIN_STATE_UP : DRM_FABRIC_ADMIN_STATE_DOWN;
 	refcount_set(&ep->refs, 1);
 	init_completion(&ep->unregistered);
 	xa_init(&ep->ports);
@@ -391,28 +432,31 @@ drm_fabric_endpoint_register(struct drm_fabric *fabric,
 
 	get_device(ep->parent);
 
-	scoped_guard(mutex, &drm_fabric_lock) {
-		/*
-		 * A concurrent drm_fabric_unregister() may have freed @fabric
-		 * since the caller passed it, so validate membership by address
-		 * without dereferencing it.
-		 */
-		if (!drm_fabric_is_registered(fabric)) {
-			ret = -ENODEV;
-			break;
-		}
-
-		if (drm_fabric_ep_id_in_use(fabric, ep->fabric_ep_id)) {
-			ret = -EEXIST;
-			break;
+	ret = 0;
+	/*
+	 * Under the mutation lock a concurrent ENDPOINT_SET attach cannot also
+	 * claim this (fabric, fabric_ep_id).
+	 */
+	scoped_guard(mutex, &drm_fabric_mutation_lock) {
+		scoped_guard(mutex, &drm_fabric_lock) {
+			if (fabric && !drm_fabric_is_registered(fabric)) {
+				ret = -ENODEV;
+				break;
+			}
+
+			if (drm_fabric_ep_id_in_use(fabric, ep->fabric_ep_id)) {
+				ret = -EEXIST;
+				break;
+			}
+
+			ret = xa_alloc(&drm_fabric_ep_xa, &ep->id, ep, xa_limit_32b, GFP_KERNEL);
+			if (ret)
+				break;
+			if (fabric)
+				drm_fabric_get(fabric);
+
+			drm_fabric_emit_endpoint_create(ep, drm_fabric_base_seq_inc());
 		}
-
-		ret = xa_alloc(&drm_fabric_ep_xa, &ep->id, ep, xa_limit_32b, GFP_KERNEL);
-		if (ret)
-			break;
-		drm_fabric_get(fabric);
-
-		drm_fabric_emit_endpoint_create(ep, drm_fabric_base_seq_inc());
 	}
 
 	if (ret) {
@@ -494,26 +538,36 @@ static bool drm_fabric_ep_is_registered(const struct drm_fabric_endpoint *ep)
  * and performs no teardown; there is no error return to report it. As for a
  * fabric, the comparison proves current address membership only.
  *
- * Context: May sleep. Acquires drm_fabric_lock.
+ * Context: May sleep. Acquires drm_fabric_mutation_lock, then drm_fabric_lock.
+ *          Must not be called from a provider mutation callback, which already
+ *          holds the mutation lock and would self-deadlock.
  */
 void drm_fabric_endpoint_unregister(struct drm_fabric_endpoint *ep)
 {
-	scoped_guard(mutex, &drm_fabric_lock) {
-		if (WARN_ON_ONCE(!drm_fabric_ep_is_registered(ep)))
-			return;
-		/* Emit before the erase, while @ep is still live. */
-		drm_fabric_emit_endpoint_delete(ep, drm_fabric_base_seq_inc());
-		xa_erase(&drm_fabric_ep_xa, ep->id);
-	}
+	lockdep_assert_not_held(&drm_fabric_mutation_lock);
 
 	/*
-	 * Drop the registration reference and wait for any in-flight netlink
-	 * operation that pinned the endpoint to complete.
+	 * Blocks a racing mutator: @ep cannot resolve once erased, so no
+	 * ENDPOINT_CHANGE follows.
 	 */
+	scoped_guard(mutex, &drm_fabric_mutation_lock) {
+		scoped_guard(mutex, &drm_fabric_lock) {
+			if (WARN_ON_ONCE(!drm_fabric_ep_is_registered(ep)))
+				return;
+			/* Emit before the erase, while @ep is still live. */
+			drm_fabric_emit_endpoint_delete(ep,
+							drm_fabric_base_seq_inc());
+			xa_erase(&drm_fabric_ep_xa, ep->id);
+		}
+	}
+
+	/* Wait for in-flight operations holding endpoint pins. */
 	drm_fabric_endpoint_put(ep);
 	wait_for_completion(&ep->unregistered);
 
-	drm_fabric_put(ep->fabric);
+	/* An orphan endpoint holds no fabric reference. */
+	if (ep->fabric)
+		drm_fabric_put(ep->fabric);
 
 	drm_fabric_ports_destroy(ep);
 	put_device(ep->parent);
@@ -572,16 +626,25 @@ static bool drm_fabric_peer_type_valid(enum drm_fabric_peer_type type)
  * @port: local port
  * @peer: descriptor of the endpoint on the other end
  *
- * Sets the peer and advances the topology generation on success.
+ * Emits a PORT_PEER_NEW event with new peer details on success. Only valid on a
+ * provider-managed port; a userspace-managed port (%DRM_FABRIC_PEER_MODE_USERSPACE)
+ * is programmed through the PORT_PEER_NEW uAPI instead.
  *
  * Context: May sleep. Acquires drm_fabric_lock.
- * Return: -EINVAL if @peer carries an unknown peer type, -EEXIST if the port
- * already has a peer. 0 on success.
+ * Return: -EOPNOTSUPP on a userspace-managed port, -EINVAL if @peer carries an
+ * unknown peer type, -EEXIST if the port already has a peer, 0 on success.
  */
 int drm_fabric_port_set_peer(struct drm_fabric_port *port,
 			     const struct drm_fabric_peer *peer)
 {
-	/* Reject a provider's invalid type before it reaches the wire. */
+	if (port->peer_mode != DRM_FABRIC_PEER_MODE_PROVIDER)
+		return -EOPNOTSUPP;
+
+	/*
+	 * An unknown type is a provider bug, and the netlink path serialises
+	 * the value verbatim into an enum-typed attribute, so refusing it here
+	 * keeps it off the wire.
+	 */
 	if (WARN_ON_ONCE(!drm_fabric_peer_type_valid(peer->peer_type)))
 		return -EINVAL;
 
@@ -602,17 +665,21 @@ EXPORT_SYMBOL(drm_fabric_port_set_peer);
  * drm_fabric_port_unset_peer() - Remove a neighbor (called by the provider)
  * @port: local port
  *
- * Inverse of drm_fabric_port_set_peer().
- * Removes the peer and advances the topology generation on success.
+ * Inverse of drm_fabric_port_set_peer(). Only valid on a provider-managed port.
+ * Emits a PORT_PEER_DEL event carrying the removed peer on success.
  *
  * Edge retraction is always explicit (provider- or controller-driven); the
  * core never removes a peer implicitly.
  *
  * Context: May sleep. Acquires drm_fabric_lock.
- * Return: -ENOENT if no peer set. 0 on success.
+ * Return: -EOPNOTSUPP on a userspace-managed port, -ENOENT if no peer set,
+ * 0 on success.
  */
 int drm_fabric_port_unset_peer(struct drm_fabric_port *port)
 {
+	if (port->peer_mode != DRM_FABRIC_PEER_MODE_PROVIDER)
+		return -EOPNOTSUPP;
+
 	scoped_guard(mutex, &drm_fabric_lock) {
 		if (!port->has_peer)
 			return -ENOENT;
@@ -669,6 +736,287 @@ void drm_fabric_port_set_oper(struct drm_fabric_port *port,
 }
 EXPORT_SYMBOL(drm_fabric_port_set_oper);
 
+/* FABRIC_NEW yields an empty fabric; endpoints join only via ENDPOINT_SET. */
+int drm_fabric_user_fabric_new(enum drm_fabric_type type, u64 instance_id,
+			       const char *name, u32 *fabric_id_out)
+{
+	struct drm_fabric_desc desc = {
+		.type = type,
+		.instance_id = instance_id,
+		.name = name,
+	};
+	struct drm_fabric *fabric;
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	/* Userspace-supplied: reject without warning. */
+	if (!drm_fabric_type_valid(type))
+		return -EINVAL;
+
+	/*
+	 * A userspace-owned fabric outlives this call -- only FABRIC_DEL
+	 * removes it -- so pin the module.
+	 */
+	if (!try_module_get(THIS_MODULE))
+		return -ENODEV;
+
+	fabric = __drm_fabric_register(&desc, DRM_FABRIC_OWNER_USERSPACE);
+	if (IS_ERR(fabric)) {
+		module_put(THIS_MODULE);
+		return PTR_ERR(fabric);
+	}
+
+	if (fabric_id_out)
+		*fabric_id_out = fabric->id;
+
+	return 0;
+}
+
+int drm_fabric_user_fabric_del(u32 fabric_id)
+{
+	struct drm_fabric *fabric;
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		fabric = xa_load(&drm_fabric_xa, fabric_id);
+		if (!fabric)
+			return -ENOENT;
+		/*
+		 * Userspace-owned fabrics only: a provider's registration pointer
+		 * would dangle.
+		 */
+		if (fabric->owner != DRM_FABRIC_OWNER_USERSPACE)
+			return -EPERM;
+		/* Symmetric with FABRIC_NEW: only an empty fabric may be removed. */
+		if (drm_fabric_has_members(fabric))
+			return -EBUSY;
+		drm_fabric_base_seq_inc();
+		xa_erase(&drm_fabric_xa, fabric->id);
+	}
+
+	/* Release the pin from FABRIC_NEW. */
+	module_put(THIS_MODULE);
+	drm_fabric_put(fabric);
+	return 0;
+}
+
+/* Pins the fabric for use after the lock is dropped, or ERR_PTR(-ENOENT). */
+static struct drm_fabric *drm_fabric_find_get_locked(u32 fabric_id)
+{
+	struct drm_fabric *fabric;
+
+	fabric = drm_fabric_find_by_id(fabric_id);
+	if (!fabric)
+		return ERR_PTR(-ENOENT);
+
+	return drm_fabric_get(fabric);
+}
+
+/*
+ * Caller holds mutation_lock throughout. The target is resolved and pinned
+ * before drm_fabric_lock is dropped for the sleeping callback.
+ */
+int drm_fabric_endpoint_set(struct drm_fabric_endpoint *ep,
+			    const struct drm_fabric_endpoint_change *req)
+{
+	struct drm_fabric_endpoint_change change = *req;
+	const struct drm_fabric_ops *ops = ep->ops;
+	struct drm_fabric *new_fabric = NULL;
+	int ret;
+
+	if (!ops || !ops->endpoint_set)
+		return -EOPNOTSUPP;
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		/* No-op administrative request. */
+		if ((change.valid & DRM_FABRIC_EP_CHANGE_ADMIN) &&
+		    change.admin == ep->admin_state)
+			change.valid &= ~DRM_FABRIC_EP_CHANGE_ADMIN;
+
+		if (!change.valid)
+			return 0;
+
+		if (!(change.valid & DRM_FABRIC_EP_CHANGE_FABRIC))
+			break;
+
+		if (change.fabric_id == drm_fabric_endpoint_fabric_id(ep)) {
+			change.valid &= ~DRM_FABRIC_EP_CHANGE_FABRIC;
+			if (!change.valid)
+				return 0;
+			break;
+		}
+
+		/* Detach */
+		if (!change.fabric_id)
+			break;
+
+		/* Attach: endpoint must be orphaned. */
+		if (ep->fabric)
+			return -EBUSY;
+
+		new_fabric = drm_fabric_find_get_locked(change.fabric_id);
+		if (IS_ERR(new_fabric))
+			return PTR_ERR(new_fabric);
+
+		/* Reject early if the target fabric already uses this id. */
+		if (drm_fabric_ep_id_in_use(new_fabric, ep->fabric_ep_id)) {
+			drm_fabric_put(new_fabric);
+			return -EEXIST;
+		}
+	}
+
+	ret = ops->endpoint_set(ep, &change, new_fabric);
+	if (ret) {
+		if (new_fabric)
+			drm_fabric_put(new_fabric);
+		return ret;
+	}
+
+	/*
+	 * mutation_lock rules out a race; the WARN_ON_ONCE rechecks below are
+	 * assertions only.
+	 */
+	scoped_guard(mutex, &drm_fabric_lock) {
+		if (change.valid & DRM_FABRIC_EP_CHANGE_FABRIC) {
+			if (new_fabric) {
+				if (WARN_ON_ONCE(xa_load(&drm_fabric_xa, new_fabric->id) !=
+						 new_fabric)) {
+					drm_fabric_put(new_fabric);
+					return -ENODEV;
+				}
+				if (WARN_ON_ONCE(drm_fabric_ep_id_in_use(new_fabric,
+									 ep->fabric_ep_id))) {
+					drm_fabric_put(new_fabric);
+					return -EEXIST;
+				}
+				ep->fabric = new_fabric;
+			} else {
+				/* Reached only for an attached endpoint. */
+				drm_fabric_put(ep->fabric);
+				ep->fabric = NULL;
+			}
+		}
+
+		if (change.valid & DRM_FABRIC_EP_CHANGE_ADMIN)
+			ep->admin_state = change.admin;
+
+		drm_fabric_base_seq_inc();
+	}
+
+	return 0;
+}
+
+/*
+ * No revalidation is needed: the port stays pinned and admin_state has no
+ * out-of-band writer.
+ */
+int drm_fabric_port_set_admin(struct drm_fabric_port *port,
+			      enum drm_fabric_admin_state admin)
+{
+	const struct drm_fabric_ops *ops = port->endpoint->ops;
+	int ret;
+
+	if (!ops || !ops->port_set)
+		return -EOPNOTSUPP;
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	/* No-op administrative request. */
+	scoped_guard(mutex, &drm_fabric_lock)
+		if (port->admin_state == admin)
+			return 0;
+
+	ret = ops->port_set(port, admin);
+	if (ret)
+		return ret;
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		port->admin_state = admin;
+		drm_fabric_emit_port_change(port, drm_fabric_base_seq_inc());
+	}
+
+	return 0;
+}
+
+/*
+ * No revalidation is needed: the port stays pinned and has_peer has no
+ * out-of-band writer.
+ */
+int drm_fabric_port_peer_new(struct drm_fabric_port *port,
+			     const struct drm_fabric_peer *peer)
+{
+	const struct drm_fabric_ops *ops = port->endpoint->ops;
+	int ret;
+
+	if (port->peer_mode != DRM_FABRIC_PEER_MODE_USERSPACE)
+		return -EOPNOTSUPP;
+
+	if (!ops || !ops->port_peer_new)
+		return -EOPNOTSUPP;
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		if (port->has_peer)
+			return -EEXIST;
+	}
+
+	ret = ops->port_peer_new(port, peer);
+	if (ret)
+		return ret;
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		port->peer = *peer;
+		port->has_peer = true;
+		drm_fabric_emit_port_peer_create(port, peer,
+						 drm_fabric_base_seq_inc());
+	}
+
+	return 0;
+}
+
+/* Same userspace-managed contract as drm_fabric_port_peer_new(). */
+int drm_fabric_port_peer_del(struct drm_fabric_port *port)
+{
+	const struct drm_fabric_ops *ops = port->endpoint->ops;
+	int ret;
+
+	if (port->peer_mode != DRM_FABRIC_PEER_MODE_USERSPACE)
+		return -EOPNOTSUPP;
+
+	if (!ops || !ops->port_peer_del)
+		return -EOPNOTSUPP;
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		if (!port->has_peer)
+			return -ENOENT;
+	}
+
+	ret = ops->port_peer_del(port);
+	if (ret)
+		return ret;
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		drm_fabric_emit_port_peer_delete(port, &port->peer,
+						 drm_fabric_base_seq_inc());
+		port->has_peer = false;
+		memset(&port->peer, 0, sizeof(port->peer));
+	}
+
+	return 0;
+}
+
 static int __init drm_fabric_init(void)
 {
 	return drm_fabric_netlink_register();
diff --git a/drivers/gpu/drm/fabric/drm_fabric_internal.h b/drivers/gpu/drm/fabric/drm_fabric_internal.h
index 7fc5bc3f33f9..dc95b9967b64 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_internal.h
+++ b/drivers/gpu/drm/fabric/drm_fabric_internal.h
@@ -13,6 +13,7 @@
 #include <drm/drm_fabric.h>
 
 extern struct mutex drm_fabric_lock;
+extern struct mutex drm_fabric_mutation_lock;
 extern struct xarray drm_fabric_xa;    /* Fabric registry */
 extern struct xarray drm_fabric_ep_xa; /* Endpoint registry */
 
@@ -34,9 +35,21 @@ void drm_fabric_endpoint_put(struct drm_fabric_endpoint *ep);
 struct drm_fabric *drm_fabric_get(struct drm_fabric *fabric);
 void drm_fabric_put(struct drm_fabric *fabric);
 
+int drm_fabric_user_fabric_new(enum drm_fabric_type type, u64 instance_id,
+			       const char *name, u32 *fabric_id_out);
+int drm_fabric_user_fabric_del(u32 fabric_id);
+int drm_fabric_endpoint_set(struct drm_fabric_endpoint *ep,
+			    const struct drm_fabric_endpoint_change *change);
+int drm_fabric_port_set_admin(struct drm_fabric_port *port,
+			      enum drm_fabric_admin_state admin);
+int drm_fabric_port_peer_new(struct drm_fabric_port *port,
+			     const struct drm_fabric_peer *peer);
+int drm_fabric_port_peer_del(struct drm_fabric_port *port);
+
 /* Emits use the post-change generation. */
 void drm_fabric_emit_endpoint_create(struct drm_fabric_endpoint *ep, u32 generation);
 void drm_fabric_emit_endpoint_delete(struct drm_fabric_endpoint *ep, u32 generation);
+void drm_fabric_emit_endpoint_change(struct drm_fabric_endpoint *ep, u32 generation);
 
 void drm_fabric_emit_port_peer_create(struct drm_fabric_port *port,
 				      const struct drm_fabric_peer *peer,
diff --git a/drivers/gpu/drm/fabric/drm_fabric_test.c b/drivers/gpu/drm/fabric/drm_fabric_test.c
index 3457675645f6..863b7cf69068 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_test.c
+++ b/drivers/gpu/drm/fabric/drm_fabric_test.c
@@ -112,13 +112,17 @@ static void drm_fabric_test_unregister_reports_removal(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, drm_fabric_unregister(fab), 0);
 }
 
-static void drm_fabric_test_endpoint_requires_fabric(struct kunit *test)
+/*
+ * A NULL fabric registers an orphan endpoint, which reports fabric-id 0
+ * and can be unregistered.
+ */
+static void drm_fabric_test_endpoint_orphan_register(struct kunit *test)
 {
 	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
 	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
 	struct drm_fabric_endpoint_desc edesc = {
 		.fabric_ep_id = 1,
-		.name = "no-fabric",
+		.name = "orphan",
 		.parent = fabrictest_dev,
 		.ports = &pdesc,
 		.num_ports = 1,
@@ -126,9 +130,10 @@ static void drm_fabric_test_endpoint_requires_fabric(struct kunit *test)
 	struct drm_fabric_endpoint *ep;
 
 	ep = drm_fabric_endpoint_register(NULL, &edesc);
-	KUNIT_EXPECT_TRUE(test, IS_ERR(ep));
-	if (IS_ERR(ep))
-		KUNIT_EXPECT_EQ(test, PTR_ERR(ep), -EINVAL);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_EXPECT_EQ(test, drm_fabric_endpoint_fabric_id(ep), 0);
+
+	drm_fabric_endpoint_unregister(ep);
 }
 
 /* Registration must not walk a NULL port array. */
@@ -1280,7 +1285,7 @@ static struct kunit_case drm_fabric_test_cases[] = {
 	KUNIT_CASE(drm_fabric_test_unregister_reports_removal),
 	KUNIT_CASE(drm_fabric_test_instance_id_unique),
 	KUNIT_CASE(drm_fabric_test_endpoint_register),
-	KUNIT_CASE(drm_fabric_test_endpoint_requires_fabric),
+	KUNIT_CASE(drm_fabric_test_endpoint_orphan_register),
 	KUNIT_CASE(drm_fabric_test_endpoint_requires_port_array),
 	KUNIT_CASE(drm_fabric_test_endpoint_register_stale_fabric),
 	KUNIT_CASE(drm_fabric_test_unregister_rejects_non_member),
diff --git a/include/drm/drm_fabric.h b/include/drm/drm_fabric.h
index 136100b3da67..69e9099a1e57 100644
--- a/include/drm/drm_fabric.h
+++ b/include/drm/drm_fabric.h
@@ -4,8 +4,8 @@
  */
 
 /*
- * Common object model for GPU interconnect topology: fabric, endpoint, port
- * and peer relationships.
+ * DRM Fabric driver API: common object model for GPU interconnect topology
+ * (fabric, endpoint, port and peer relationships).
  */
 
 #ifndef __DRM_FABRIC_H__
@@ -18,8 +18,32 @@
 
 #include <uapi/drm/drm_fabric.h>
 
+enum drm_fabric_admin_state {
+	DRM_FABRIC_ADMIN_STATE_DOWN = 1,
+	DRM_FABRIC_ADMIN_STATE_UP,
+};
+
 struct device;
 
+/**
+ * enum drm_fabric_peer_mode - authority for a port's peer descriptor
+ * @DRM_FABRIC_PEER_MODE_PROVIDER: the provider reports the peer through
+ *	drm_fabric_port_set_peer() / drm_fabric_port_unset_peer(); the userspace
+ *	PORT_PEER_NEW / PORT_PEER_DEL path on the port returns -EOPNOTSUPP.
+ * @DRM_FABRIC_PEER_MODE_USERSPACE: userspace provisions the peer through
+ *	PORT_PEER_NEW / PORT_PEER_DEL (which invoke the provider programming
+ *	hooks); the provider must not call set_peer()/unset_peer() on the port,
+ *	and doing so returns -EOPNOTSUPP.
+ *
+ * One port, one peer descriptor, one source allowed to program it, fixed at
+ * registration. PROVIDER is the zero default, so a port that does not opt in
+ * is provider-managed.
+ */
+enum drm_fabric_peer_mode {
+	DRM_FABRIC_PEER_MODE_PROVIDER = 0,
+	DRM_FABRIC_PEER_MODE_USERSPACE,
+};
+
 /**
  * struct drm_fabric_port_desc - Port descriptor for drm_fabric_endpoint_register()
  */
@@ -36,6 +60,9 @@ struct drm_fabric_port_desc {
 	 * usable bandwidth.
 	 */
 	u32 max_lane_signaling_rate_mbps;
+
+	/** @peer_mode: who may program the port's peer descriptor */
+	enum drm_fabric_peer_mode peer_mode;
 };
 
 /**
@@ -79,6 +106,19 @@ struct drm_fabric_desc {
 	u64 instance_id;
 };
 
+/**
+ * enum drm_fabric_owner - lifecycle owner of a fabric object
+ * @DRM_FABRIC_OWNER_PROVIDER: created by a provider via drm_fabric_register();
+ *	only the provider may unregister it, never userspace FABRIC_DEL.
+ * @DRM_FABRIC_OWNER_USERSPACE: created by userspace via FABRIC_NEW; may be
+ *	deleted by userspace via FABRIC_DEL.
+ */
+enum drm_fabric_owner {
+	/* Zero value, so a kzalloc'd fabric is not deletable via FABRIC_DEL. */
+	DRM_FABRIC_OWNER_PROVIDER = 0,
+	DRM_FABRIC_OWNER_USERSPACE,
+};
+
 /**
  * struct drm_fabric - Fabric object
  */
@@ -91,6 +131,8 @@ struct drm_fabric {
 	u64 instance_id;
 	/** @name: human-readable fabric name */
 	char name[32];
+	/** @owner: lifecycle owner, gates userspace FABRIC_DEL */
+	enum drm_fabric_owner owner;
 
 	/** @refs: reference count */
 	refcount_t refs;
@@ -108,8 +150,13 @@ struct drm_fabric_endpoint {
 	char name[32];
 	/** @parent: backing device, provides dev_name and bus_name */
 	struct device *parent;
+	/**
+	 * @admin_state: administrative state, initially DOWN for an orphan and
+	 *	UP for a fabric member
+	 */
+	enum drm_fabric_admin_state admin_state;
 
-	/** @fabric: parent fabric */
+	/** @fabric: parent fabric, NULL while orphaned */
 	struct drm_fabric *fabric;
 
 	/** @ops: provider driver callbacks */
@@ -132,12 +179,14 @@ struct drm_fabric_endpoint {
  * drm_fabric_endpoint_fabric_id() - Wire fabric-id for an endpoint
  * @ep: endpoint to query
  *
- * Return: the parent fabric id.
+ * An orphaned endpoint (no fabric) reports fabric-id 0 on the wire.
+ *
+ * Return: the parent fabric id, or 0 if the endpoint is orphaned.
  */
 static inline u32
 drm_fabric_endpoint_fabric_id(const struct drm_fabric_endpoint *ep)
 {
-	return ep->fabric->id;
+	return ep->fabric ? ep->fabric->id : 0;
 }
 
 /**
@@ -171,11 +220,15 @@ struct drm_fabric_port {
 	u32 index;
 	/** @oper_state: operational (link) state */
 	enum drm_fabric_port_state oper_state;
+	/** @admin_state: administrative (requested) state */
+	enum drm_fabric_admin_state admin_state;
 	/** @max_lane_count: as in &struct drm_fabric_port_desc */
 	u32 max_lane_count;
 	/** @max_lane_signaling_rate_mbps: as in &struct drm_fabric_port_desc */
 	u32 max_lane_signaling_rate_mbps;
 
+	/** @peer_mode: authority for @peer, fixed at registration */
+	enum drm_fabric_peer_mode peer_mode;
 	/** @has_peer: whether @peer holds a valid descriptor */
 	bool has_peer;
 	/** @peer: neighbor description, valid only while @has_peer is set */
@@ -203,11 +256,34 @@ struct drm_fabric_port_stats {
 	u64 retrain_count;
 };
 
+/**
+ * struct drm_fabric_endpoint_change - Requested endpoint change for the endpoint_set() callback
+ */
+struct drm_fabric_endpoint_change {
+#define DRM_FABRIC_EP_CHANGE_FABRIC	BIT(0)
+#define DRM_FABRIC_EP_CHANGE_ADMIN	BIT(1)
+	/** @valid: bitmask of %DRM_FABRIC_EP_CHANGE_* selecting which fields are meaningful */
+	u32 valid;
+
+	/** @fabric_id: target fabric, 0 to detach (%DRM_FABRIC_EP_CHANGE_FABRIC) */
+	u32 fabric_id;
+	/** @admin: requested admin state (%DRM_FABRIC_EP_CHANGE_ADMIN) */
+	enum drm_fabric_admin_state admin;
+};
+
 /**
  * struct drm_fabric_ops - Provider driver callbacks
  *
- * A callback that is not supplied makes the matching netlink operation return
- * -EOPNOTSUPP.
+ * A callback that is not supplied makes the matching netlink operation
+ * return -EOPNOTSUPP.
+ *
+ * All callbacks are invoked without drm_fabric_lock held and may sleep;
+ * see each callback's own doc below for any further constraint.
+ *
+ * The mutation callbacks (endpoint_set, port_set, port_peer_new,
+ * port_peer_del) run with drm_fabric_mutation_lock held, so a provider
+ * must not call drm_fabric_endpoint_unregister() from inside one: that
+ * call takes the same lock and would self-deadlock.
  */
 struct drm_fabric_ops {
 	/**
@@ -221,6 +297,24 @@ struct drm_fabric_ops {
 	 */
 	int (*port_stats_get)(struct drm_fabric_port *port,
 			      struct drm_fabric_port_stats *stats);
+
+	/** @endpoint_set: attach or detach an endpoint and/or set its admin state */
+	int (*endpoint_set)(struct drm_fabric_endpoint *ep,
+			    const struct drm_fabric_endpoint_change *change,
+			    struct drm_fabric *fabric);
+	/** @port_set: set a port's admin state */
+	int (*port_set)(struct drm_fabric_port *port,
+			enum drm_fabric_admin_state admin);
+	/**
+	 * @port_peer_new: program a port's neighbor. Reached only for a port
+	 * whose peer_mode is %DRM_FABRIC_PEER_MODE_USERSPACE; a
+	 * provider-managed port reports its peer through
+	 * drm_fabric_port_set_peer() instead.
+	 */
+	int (*port_peer_new)(struct drm_fabric_port *port,
+			     const struct drm_fabric_peer *peer);
+	/** @port_peer_del: unprogram a userspace-managed port's neighbor */
+	int (*port_peer_del)(struct drm_fabric_port *port);
 };
 
 struct drm_fabric *drm_fabric_register(const struct drm_fabric_desc *desc);
-- 
2.43.0


  parent reply	other threads:[~2026-08-24  8:10 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-24  8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 01/12] drm/fabric: add core object model and provider API Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 02/12] drm/fabric: add query uAPI and generated headers Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 03/12] drm/fabric: implement query netlink operations Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 04/12] drm/fabric: add read-only synthetic provider Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 05/12] drm/fabric: add object-model KUnit tests Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 06/12] drm/fabric: add YNL query and policy selftests Konstantin Sinyuk
2026-08-24  8:09 ` Konstantin Sinyuk [this message]
2026-08-24  8:09 ` [RFC PATCH 08/12] drm/fabric: add provisioning netlink uAPI Konstantin Sinyuk

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=0ecb1eb1209aa1ff0a58f5a23f5ba3014277690a.1787552412.git.ksinyuk@kernel.org \
    --to=ksinyuk@kernel.org \
    --cc=airlied@gmail.com \
    --cc=corbet@lwn.net \
    --cc=davem@davemloft.net \
    --cc=donald.hunter@gmail.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=edumazet@google.com \
    --cc=francois.dugast@intel.com \
    --cc=horms@kernel.org \
    --cc=ilia.levi@intel.com \
    --cc=kuba@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=rodrigo.vivi@intel.com \
    --cc=simona@ffwll.ch \
    --cc=skhan@linuxfoundation.org \
    --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