* [RFC PATCH 01/12] drm/fabric: add core object model and provider API
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 ` Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 02/12] drm/fabric: add query uAPI and generated headers Konstantin Sinyuk
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
From: Ilia Levi <ilia.levi@intel.com>
Accelerator drivers currently expose interconnect topology through
driver-specific interfaces, leaving topology semantics fragmented across
drivers. Add a common DRM object model for fabrics, endpoints, ports and
directly adjacent peers, together with the provider API drivers use to
populate it:
fabric -> endpoint -> port -> peer
A fabric groups the endpoints of one provider-defined interconnect
instance. An endpoint owns a fixed set of ports. A port may carry a
type-qualified peer value describing the accelerator or switch directly
adjacent at the far end of its link; the peer need not resolve to another
live kernel object. The core assigns live-object IDs, manages object
lifetime and enforces the model's invariants. The complete object, peer,
locking and lifecycle contracts are documented in
Documentation/gpu/drm-fabric.rst, added here.
CONFIG_DRM_FABRIC builds the core as drm-fabric.ko and depends on NET for
the Generic Netlink uAPI added next. No provider uses the API yet, so
loading the module registers no fabric objects.
Add a MAINTAINERS entry for the subsystem.
Split the core and generated uAPI header across this patch and the next to
keep the object model and wire contract separately reviewable while both
commits remain buildable. Temporarily define the uAPI value enums locally;
the generated header replaces them in the next patch.
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 | 142 ++++
Documentation/gpu/index.rst | 1 +
MAINTAINERS | 14 +
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/fabric/Kconfig | 14 +
drivers/gpu/drm/fabric/Makefile | 4 +
drivers/gpu/drm/fabric/drm_fabric.c | 681 +++++++++++++++++++
drivers/gpu/drm/fabric/drm_fabric_internal.h | 34 +
include/drm/drm_fabric.h | 259 +++++++
10 files changed, 1151 insertions(+)
create mode 100644 Documentation/gpu/drm-fabric.rst
create mode 100644 drivers/gpu/drm/fabric/Kconfig
create mode 100644 drivers/gpu/drm/fabric/Makefile
create mode 100644 drivers/gpu/drm/fabric/drm_fabric.c
create mode 100644 drivers/gpu/drm/fabric/drm_fabric_internal.h
create mode 100644 include/drm/drm_fabric.h
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
new file mode 100644
index 000000000000..99b189dc614e
--- /dev/null
+++ b/Documentation/gpu/drm-fabric.rst
@@ -0,0 +1,142 @@
+.. SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+
+===============================
+DRM Fabric over Generic Netlink
+===============================
+
+Modern GPUs and dedicated AI accelerators are increasingly connected through
+scale-up interconnect fabrics such as AMD xGMI and
+`UALink <https://ualinkconsortium.org/specification/>`__.
+
+DRM Fabric is a registry and dispatcher. It does not discover routes, compute
+reachability, program switch forwarding, manage device memory, or provide a
+data path; those remain with the vendor driver and the fabric controller.
+
+Key Goals:
+
+* Provide a standardized topology model for GPU and accelerator interconnects
+ (xGMI, UALink and similar), enabling data-center discovery and monitoring.
+* Support read-only enumeration, monitoring and state queries for
+ provider-owned topology.
+* Offer a flexible, future-proof interface that can be extended with new fabric
+ types and attributes without breaking the uAPI.
+* Allow multiple endpoints and ports per provider, so drivers can model
+ accelerator attachments, links and their peers.
+
+.. contents::
+
+Object model
+============
+
+DRM Fabric models interconnect topology with four object types::
+
+ fabric
+ `-- endpoint
+ `-- port
+ `-- peer (optional value descriptor)
+
+A *fabric* is a membership group of *endpoints*; each endpoint represents one
+accelerator attachment and owns a fixed set of *ports*; and a *port* may carry a
+*peer* describing the device directly adjacent at the far end of its link, which
+is either another accelerator or a fabric switch.
+
+Identity is layered. The core assigns kernel-local ``fabric-id`` and
+``endpoint-id`` values that address live registry objects for the duration of
+their registration. Providers supply an endpoint ``fabric-ep-id`` -- an
+accelerator's identity within its fabric's identity domain. A peer instead
+carries a type-qualified ``peer-id``: for ``peer-type = accel`` it is the
+far-end accelerator's ``fabric-ep-id``, and for ``peer-type = switch`` it is an
+opaque provider-defined switch identity that names no local object. Accelerator
+and switch identities occupy separate namespaces selected by ``peer-type``, so
+the same numeric value may name different objects under each type.
+
+Fabric membership does not imply end-to-end reachability, and the topology is
+not necessarily a tree. The registered shape reflects the direct adjacency the
+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.
+
+.. kernel-doc:: drivers/gpu/drm/fabric/drm_fabric.c
+ :doc: DRM Fabric core
+
+Peer semantics
+--------------
+
+A peer is an identity rather than a reference to a live object, and the
+difference decides what the core reports. A ``peer-id`` that resolves in the
+local registry today may stop resolving later because the endpoint it named
+unregistered, and no event is emitted on the port still carrying it: a peer
+is recorded on one local half-edge, and peer disappearance does not retract
+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.
+
+Endpoint teardown removes the endpoint's owned half-edges without generating
+a separate event for each port: the delete already describes the transition,
+so removing an endpoint advances the topology generation once rather than
+once per child port.
+
+Driver API
+----------
+
+.. kernel-doc:: include/drm/drm_fabric.h
+ :internal:
+
+.. kernel-doc:: drivers/gpu/drm/fabric/drm_fabric.c
+ :export:
+
+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.
+
+DRM Fabric does not define in-network collective operations or how an
+endpoint or switch executes them. Such capabilities belong to the
+interconnect implementation and its provider. Adding capability later is not
+foreclosed: for an endpoint it is a new attribute, while
+``peer-type = switch`` is a value descriptor rather than a registered object,
+so there is nowhere to attach one today. ``peer-id`` is already an opaque
+switch identity that need not resolve locally, so making one resolvable later
+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.
+
+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.
diff --git a/Documentation/gpu/index.rst b/Documentation/gpu/index.rst
index 65bf3b26e4f4..7d99c47ddbe7 100644
--- a/Documentation/gpu/index.rst
+++ b/Documentation/gpu/index.rst
@@ -16,6 +16,7 @@ GPU Driver Developer's Guide
driver-uapi
drm-client
drm-compute
+ drm-fabric
drivers
backlight
vga-switcheroo
diff --git a/MAINTAINERS b/MAINTAINERS
index 3c508bda61d5..2ab63bd763b3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8966,6 +8966,20 @@ F: Documentation/devicetree/bindings/display/xlnx/
F: Documentation/gpu/zynqmp.rst
F: drivers/gpu/drm/xlnx/
+DRM FABRIC
+M: Konstantin Sinyuk <ksinyuk@kernel.org>
+M: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>
+R: Francois Dugast <francois.dugast@intel.com>
+L: dri-devel@lists.freedesktop.org
+S: Maintained
+T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F: Documentation/gpu/drm-fabric.rst
+F: Documentation/netlink/specs/drm_fabric.yaml
+F: drivers/gpu/drm/fabric/
+F: include/drm/drm_fabric.h
+F: include/uapi/drm/drm_fabric.h
+F: tools/testing/selftests/drivers/gpu/drm_fabric/
+
DRM GPU SCHEDULER
M: Matthew Brost <matthew.brost@intel.com>
M: Danilo Krummrich <dakr@kernel.org>
diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 323422861e8f..fc66146a603b 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -309,6 +309,7 @@ source "drivers/gpu/drm/atmel-hlcdc/Kconfig"
source "drivers/gpu/drm/bridge/Kconfig"
source "drivers/gpu/drm/etnaviv/Kconfig"
source "drivers/gpu/drm/exynos/Kconfig"
+source "drivers/gpu/drm/fabric/Kconfig"
source "drivers/gpu/drm/fsl-dcu/Kconfig"
source "drivers/gpu/drm/gma500/Kconfig"
source "drivers/gpu/drm/gud/Kconfig"
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index e635fcffd379..bdf67dd3aab9 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -213,6 +213,7 @@ obj-y += panel/
obj-y += bridge/
obj-$(CONFIG_DRM_FSL_DCU) += fsl-dcu/
obj-$(CONFIG_DRM_ETNAVIV) += etnaviv/
+obj-$(CONFIG_DRM_FABRIC) += fabric/
obj-y += hisilicon/
obj-y += mxsfb/
obj-y += sysfb/
diff --git a/drivers/gpu/drm/fabric/Kconfig b/drivers/gpu/drm/fabric/Kconfig
new file mode 100644
index 000000000000..21fbfae9863d
--- /dev/null
+++ b/drivers/gpu/drm/fabric/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config DRM_FABRIC
+ tristate "DRM fabric support"
+ depends on DRM && NET
+ help
+ Enable DRM fabric support. This infrastructure provides the
+ core object model and provider API for registered accelerator
+ interconnect topologies.
+
+ To compile this as a module, choose M here: the module will be
+ called drm-fabric.
+
+ If in doubt, say N.
diff --git a/drivers/gpu/drm/fabric/Makefile b/drivers/gpu/drm/fabric/Makefile
new file mode 100644
index 000000000000..3a76f31f1e83
--- /dev/null
+++ b/drivers/gpu/drm/fabric/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_DRM_FABRIC) += drm-fabric.o
+drm-fabric-y := drm_fabric.o
diff --git a/drivers/gpu/drm/fabric/drm_fabric.c b/drivers/gpu/drm/fabric/drm_fabric.c
new file mode 100644
index 000000000000..8769d7bdcde1
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric.c
@@ -0,0 +1,681 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/refcount.h>
+#include <linux/slab.h>
+#include <linux/xarray.h>
+
+#include <drm/drm_fabric.h>
+
+#include "drm_fabric_internal.h"
+
+/**
+ * DOC: DRM Fabric core
+ *
+ * The core keeps a registry of fabric, endpoint, port and peer objects behind
+ * a small provider-facing API. A provider registers a fabric, attaches
+ * endpoints with their fixed set of ports, then reports link changes and peer
+ * adjacency through drm_fabric_port_set_oper(), drm_fabric_port_set_peer() and
+ * 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.
+ */
+
+/* Global lock for all fabric, endpoint and port state. */
+DEFINE_MUTEX(drm_fabric_lock);
+
+DEFINE_XARRAY_ALLOC1(drm_fabric_xa);
+DEFINE_XARRAY_ALLOC(drm_fabric_ep_xa);
+
+/*
+ * Family-global topology change token, exposed to user space as
+ * topology-generation and used as the netlink dump-consistency sequence. It is
+ * nonzero and never emits zero across the u32 wrap. Increment it on a
+ * committed topology mutation so a subsequent GET/DUMP reports the
+ * post-change generation.
+ */
+u32 drm_fabric_base_seq = 1;
+
+static u32 drm_fabric_base_seq_inc(void)
+{
+ lockdep_assert_held(&drm_fabric_lock);
+ /*
+ * Generation feeds cb->seq; netlink treats 0 as "no dump in progress",
+ * so skip it on wrap.
+ */
+ if (++drm_fabric_base_seq == 0)
+ drm_fabric_base_seq = 1;
+ return drm_fabric_base_seq;
+}
+
+struct drm_fabric *drm_fabric_find_by_id(u32 id)
+{
+ lockdep_assert_held(&drm_fabric_lock);
+ return xa_load(&drm_fabric_xa, id);
+}
+
+struct drm_fabric_endpoint *drm_fabric_endpoint_find_by_id(u32 id)
+{
+ lockdep_assert_held(&drm_fabric_lock);
+ return xa_load(&drm_fabric_ep_xa, id);
+}
+
+/*
+ * Returns NULL if no endpoint matches, or ERR_PTR(-EINVAL) if @devname is
+ * ambiguous across buses and @busname does not disambiguate it.
+ */
+struct drm_fabric_endpoint *
+drm_fabric_endpoint_find_by_dev_name(const char *devname, const char *busname)
+{
+ struct drm_fabric_endpoint *match = NULL;
+ struct drm_fabric_endpoint *ep;
+ unsigned long idx;
+
+ lockdep_assert_held(&drm_fabric_lock);
+ xa_for_each(&drm_fabric_ep_xa, idx, ep) {
+ if (strcmp(dev_name(ep->parent), devname))
+ continue;
+
+ if (busname) {
+ if (strcmp(dev_bus_name(ep->parent), busname))
+ continue;
+ return ep;
+ }
+
+ if (match)
+ return ERR_PTR(-EINVAL);
+
+ match = ep;
+ }
+
+ return match;
+}
+
+/*
+ * 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.
+ */
+static bool drm_fabric_ep_id_in_use(const struct drm_fabric *fabric, u64 fabric_ep_id)
+{
+ struct drm_fabric_endpoint *ep;
+ unsigned long idx;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each(&drm_fabric_ep_xa, idx, ep)
+ if (ep->fabric == fabric &&
+ ep->fabric_ep_id == fabric_ep_id)
+ return true;
+
+ return false;
+}
+
+static bool drm_fabric_has_instance(const struct drm_fabric *fabric)
+{
+ struct drm_fabric *other;
+ unsigned long idx;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each(&drm_fabric_xa, idx, other)
+ if (other->type == fabric->type &&
+ other->instance_id == fabric->instance_id)
+ return true;
+
+ 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)
+{
+ 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;
+ refcount_set(&fabric->refs, 1);
+
+ if (desc->name &&
+ strscpy(fabric->name, desc->name, sizeof(fabric->name)) < 0)
+ return ERR_PTR(-ENAMETOOLONG);
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ int ret;
+
+ if (drm_fabric_has_instance(fabric))
+ return ERR_PTR(-EEXIST);
+
+ ret = xa_alloc(&drm_fabric_xa, &fabric->id, fabric,
+ xa_limit_32b, GFP_KERNEL);
+ if (ret)
+ return ERR_PTR(ret);
+ drm_fabric_base_seq_inc();
+ }
+
+ return_ptr(fabric);
+}
+EXPORT_SYMBOL(drm_fabric_register);
+
+struct drm_fabric *drm_fabric_get(struct drm_fabric *fabric)
+{
+ lockdep_assert_held(&drm_fabric_lock);
+ refcount_inc(&fabric->refs);
+ return fabric;
+}
+
+void drm_fabric_put(struct drm_fabric *fabric)
+{
+ if (refcount_dec_and_test(&fabric->refs))
+ kfree(fabric);
+}
+
+static bool drm_fabric_has_members(const struct drm_fabric *fabric)
+{
+ struct drm_fabric_endpoint *ep;
+ unsigned long idx;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each(&drm_fabric_ep_xa, idx, ep)
+ if (ep->fabric == fabric)
+ return true;
+
+ return false;
+}
+
+/*
+ * Compare the possibly stale pointer by address without dereferencing it.
+ * A reused address passes as the later object; providers own incarnation
+ * tracking.
+ */
+static bool drm_fabric_is_registered(const struct drm_fabric *fabric)
+{
+ struct drm_fabric *entry;
+ unsigned long idx;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each(&drm_fabric_xa, idx, entry)
+ if (entry == fabric)
+ return true;
+
+ return false;
+}
+
+/**
+ * drm_fabric_unregister() - Unregister a fabric
+ * @fabric: fabric to remove
+ *
+ * Removes an empty @fabric from the registry and frees it. A provider must
+ * unregister all member endpoints first.
+ *
+ * @fabric must still be registered. The pointer is compared against the
+ * registry before anything dereferences it, so a stale or repeated
+ * unregister is rejected rather than acted on. That comparison proves
+ * current address membership only: it cannot tell an earlier incarnation
+ * 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.
+ */
+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;
+ drm_fabric_base_seq_inc();
+ xa_erase(&drm_fabric_xa, fabric->id);
+ }
+
+ drm_fabric_put(fabric);
+ return 0;
+}
+EXPORT_SYMBOL(drm_fabric_unregister);
+
+static void drm_fabric_ports_destroy(struct drm_fabric_endpoint *ep)
+{
+ struct drm_fabric_port *port;
+ unsigned long index;
+
+ xa_for_each(&ep->ports, index, port) {
+ kfree(port);
+ ep->num_ports--;
+ }
+ xa_destroy(&ep->ports);
+}
+
+static int drm_fabric_ports_create(struct drm_fabric_endpoint *ep,
+ const struct drm_fabric_port_desc *descs,
+ unsigned int num_ports)
+{
+ unsigned int i;
+
+ for (i = 0; i < num_ports; i++) {
+ struct drm_fabric_port *port;
+ int ret;
+
+ port = kzalloc_obj(*port);
+ if (!port) {
+ drm_fabric_ports_destroy(ep);
+ return -ENOMEM;
+ }
+
+ port->index = descs[i].index;
+ port->max_lane_count = descs[i].max_lane_count;
+ port->max_lane_signaling_rate_mbps =
+ descs[i].max_lane_signaling_rate_mbps;
+ port->oper_state = DRM_FABRIC_PORT_STATE_UNKNOWN;
+ port->has_peer = false;
+ port->endpoint = ep;
+
+ ret = xa_insert(&ep->ports, port->index, port, GFP_KERNEL);
+ if (ret) {
+ /* A duplicate port index is a provider bug. */
+ WARN_ON_ONCE(ret == -EBUSY);
+ kfree(port);
+ drm_fabric_ports_destroy(ep);
+ return ret;
+ }
+
+ ep->num_ports++;
+ }
+
+ return 0;
+}
+
+/**
+ * drm_fabric_endpoint_register() - Register a provider-owned endpoint
+ * @fabric: non-NULL 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.
+ *
+ * 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.
+ *
+ * Context: May sleep. Acquires 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.
+ */
+struct drm_fabric_endpoint *
+drm_fabric_endpoint_register(struct drm_fabric *fabric,
+ const struct drm_fabric_endpoint_desc *desc)
+{
+ struct drm_fabric_endpoint *ep __free(kfree) = NULL;
+ int ret;
+
+ if (!fabric)
+ return ERR_PTR(-EINVAL);
+
+ /* Supplies dev_name()/bus for the query paths; pinned below. */
+ if (!desc->parent)
+ return ERR_PTR(-EINVAL);
+
+ if (desc->num_ports && !desc->ports)
+ return ERR_PTR(-EINVAL);
+
+ ep = kzalloc_obj(*ep);
+ if (!ep)
+ return ERR_PTR(-ENOMEM);
+
+ ep->fabric_ep_id = desc->fabric_ep_id;
+ ep->fabric = fabric;
+ ep->parent = desc->parent;
+ ep->ops = desc->ops;
+ ep->priv = desc->priv;
+ refcount_set(&ep->refs, 1);
+ init_completion(&ep->unregistered);
+ xa_init(&ep->ports);
+
+ if (desc->name &&
+ strscpy(ep->name, desc->name, sizeof(ep->name)) < 0)
+ return ERR_PTR(-ENAMETOOLONG);
+
+ ret = drm_fabric_ports_create(ep, desc->ports, desc->num_ports);
+ if (ret)
+ return ERR_PTR(ret);
+
+ 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 = xa_alloc(&drm_fabric_ep_xa, &ep->id, ep, xa_limit_32b, GFP_KERNEL);
+ if (ret)
+ break;
+ drm_fabric_get(fabric);
+
+ drm_fabric_base_seq_inc();
+ }
+
+ if (ret) {
+ put_device(ep->parent);
+ drm_fabric_ports_destroy(ep);
+ return ERR_PTR(ret);
+ }
+
+ return_ptr(ep);
+}
+EXPORT_SYMBOL(drm_fabric_endpoint_register);
+
+/**
+ * drm_fabric_endpoint_port() - Look up a port of a registered endpoint
+ * @ep: provider-owned endpoint
+ * @port_index: per-endpoint port index
+ *
+ * An endpoint's set of ports is fixed for its registration lifetime, so a
+ * provider that serializes the endpoint lifecycle may call this without
+ * holding drm_fabric_lock. The returned pointer is borrowed and remains valid
+ * only while @ep is registered; it must not be retained across
+ * drm_fabric_endpoint_unregister().
+ *
+ * Return: the port at @port_index, or NULL if no such port exists.
+ */
+struct drm_fabric_port *
+drm_fabric_endpoint_port(struct drm_fabric_endpoint *ep, u32 port_index)
+{
+ return xa_load(&ep->ports, port_index);
+}
+EXPORT_SYMBOL(drm_fabric_endpoint_port);
+
+struct drm_fabric_endpoint *drm_fabric_endpoint_get(struct drm_fabric_endpoint *ep)
+{
+ lockdep_assert_held(&drm_fabric_lock);
+ refcount_inc(&ep->refs);
+ return ep;
+}
+
+void drm_fabric_endpoint_put(struct drm_fabric_endpoint *ep)
+{
+ if (refcount_dec_and_test(&ep->refs))
+ complete(&ep->unregistered);
+}
+
+/*
+ * Same contract as drm_fabric_is_registered(): a stale, possibly freed @ep is
+ * compared but never dereferenced, and an address reused by a later
+ * registration passes as that later endpoint.
+ */
+static bool drm_fabric_ep_is_registered(const struct drm_fabric_endpoint *ep)
+{
+ struct drm_fabric_endpoint *entry;
+ unsigned long idx;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each(&drm_fabric_ep_xa, idx, entry)
+ if (entry == ep)
+ return true;
+
+ return false;
+}
+
+/**
+ * drm_fabric_endpoint_unregister() - Unregister and free an endpoint
+ * @ep: endpoint to remove
+ *
+ * Removes @ep from the registry and frees it along with its ports, advancing
+ * the topology generation. Peers on other endpoints that name @ep are left
+ * untouched: the core never scans or retracts a half-edge on unregister.
+ *
+ * The provider must first quiesce its own use of @ep and any borrowed
+ * drm_fabric_endpoint_port() pointer. The core waits only for references it
+ * issued itself; concurrent netlink readers need no provider action.
+ *
+ * @ep must still be registered. The pointer is compared against the registry
+ * before anything dereferences it, so a stale or repeated unregister warns
+ * 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.
+ */
+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;
+ drm_fabric_base_seq_inc();
+ xa_erase(&drm_fabric_ep_xa, ep->id);
+ }
+
+ /*
+ * Drop the registration reference and wait for any in-flight netlink
+ * operation that pinned the endpoint to complete.
+ */
+ drm_fabric_endpoint_put(ep);
+ wait_for_completion(&ep->unregistered);
+
+ drm_fabric_put(ep->fabric);
+
+ drm_fabric_ports_destroy(ep);
+ put_device(ep->parent);
+ kfree(ep);
+}
+EXPORT_SYMBOL(drm_fabric_endpoint_unregister);
+
+/* Borrowed: valid only while drm_fabric_lock is held. */
+struct drm_fabric_port *drm_fabric_port_find(u32 ep_id, u32 port_idx)
+{
+ struct drm_fabric_endpoint *ep;
+
+ lockdep_assert_held(&drm_fabric_lock);
+ ep = drm_fabric_endpoint_find_by_id(ep_id);
+ if (!ep)
+ return NULL;
+ return xa_load(&ep->ports, port_idx);
+}
+
+/*
+ * No refcount of its own: pinned through its owning endpoint and released
+ * with drm_fabric_port_put(). ERR_PTR(-ENOENT) if no such port exists.
+ */
+struct drm_fabric_port *drm_fabric_port_find_get(u32 ep_id, u32 port_idx)
+{
+ struct drm_fabric_port *port;
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ port = drm_fabric_port_find(ep_id, port_idx);
+ if (!port)
+ return ERR_PTR(-ENOENT);
+ drm_fabric_endpoint_get(port->endpoint);
+ }
+
+ return port;
+}
+
+void drm_fabric_port_put(struct drm_fabric_port *port)
+{
+ drm_fabric_endpoint_put(port->endpoint);
+}
+
+static bool drm_fabric_peer_type_valid(enum drm_fabric_peer_type type)
+{
+ switch (type) {
+ case DRM_FABRIC_PEER_TYPE_ACCEL:
+ case DRM_FABRIC_PEER_TYPE_SWITCH:
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * drm_fabric_port_set_peer() - Set a neighbor (called by the provider)
+ * @port: local port
+ * @peer: descriptor of the endpoint on the other end
+ *
+ * Sets the peer and advances the topology generation on success.
+ *
+ * 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.
+ */
+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 (WARN_ON_ONCE(!drm_fabric_peer_type_valid(peer->peer_type)))
+ return -EINVAL;
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ if (port->has_peer)
+ return -EEXIST;
+ port->peer = *peer;
+ port->has_peer = true;
+ drm_fabric_base_seq_inc();
+ }
+
+ return 0;
+}
+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.
+ *
+ * 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.
+ */
+int drm_fabric_port_unset_peer(struct drm_fabric_port *port)
+{
+ scoped_guard(mutex, &drm_fabric_lock) {
+ if (!port->has_peer)
+ return -ENOENT;
+ drm_fabric_base_seq_inc();
+ port->has_peer = false;
+ memset(&port->peer, 0, sizeof(port->peer));
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_fabric_port_unset_peer);
+
+static bool drm_fabric_port_oper_state_valid(enum drm_fabric_port_state state)
+{
+ switch (state) {
+ case DRM_FABRIC_PORT_STATE_UNKNOWN:
+ case DRM_FABRIC_PORT_STATE_INACTIVE:
+ case DRM_FABRIC_PORT_STATE_ACTIVE:
+ case DRM_FABRIC_PORT_STATE_DEGRADED:
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * drm_fabric_port_set_oper() - Update a port's operational state
+ * @port: port to update
+ * @state: new operational state
+ *
+ * Advances the topology generation if the state actually changed. Invalid
+ * states are provider bugs; they are warned about and ignored.
+ *
+ * Context: May sleep. Acquires drm_fabric_lock.
+ */
+void drm_fabric_port_set_oper(struct drm_fabric_port *port,
+ enum drm_fabric_port_state state)
+{
+ if (WARN_ON_ONCE(!drm_fabric_port_oper_state_valid(state)))
+ return;
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ enum drm_fabric_port_state old = port->oper_state;
+
+ port->oper_state = state;
+ if (old != state)
+ drm_fabric_base_seq_inc();
+ }
+}
+EXPORT_SYMBOL(drm_fabric_port_set_oper);
+
+static int __init drm_fabric_init(void)
+{
+ return 0;
+}
+
+static void __exit drm_fabric_exit(void)
+{
+ WARN_ON(!xa_empty(&drm_fabric_xa));
+ WARN_ON(!xa_empty(&drm_fabric_ep_xa));
+ xa_destroy(&drm_fabric_xa);
+ xa_destroy(&drm_fabric_ep_xa);
+}
+
+module_init(drm_fabric_init);
+module_exit(drm_fabric_exit);
+
+MODULE_AUTHOR("Intel Corporation");
+MODULE_DESCRIPTION("DRM fabric infrastructure");
+MODULE_LICENSE("Dual MIT/GPL");
diff --git a/drivers/gpu/drm/fabric/drm_fabric_internal.h b/drivers/gpu/drm/fabric/drm_fabric_internal.h
new file mode 100644
index 000000000000..d454225a0b81
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_internal.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#ifndef __DRM_FABRIC_INTERNAL_H__
+#define __DRM_FABRIC_INTERNAL_H__
+
+#include <linux/mutex.h>
+#include <linux/types.h>
+#include <linux/xarray.h>
+
+#include <drm/drm_fabric.h>
+
+extern struct mutex drm_fabric_lock;
+extern struct xarray drm_fabric_xa; /* Fabric registry */
+extern struct xarray drm_fabric_ep_xa; /* Endpoint registry */
+
+extern u32 drm_fabric_base_seq; /* Dump consistency sequence */
+
+struct drm_fabric *drm_fabric_find_by_id(u32 id);
+struct drm_fabric_endpoint *drm_fabric_endpoint_find_by_id(u32 id);
+struct drm_fabric_endpoint *
+drm_fabric_endpoint_find_by_dev_name(const char *devname, const char *busname);
+struct drm_fabric_port *drm_fabric_port_find(u32 ep_id, u32 port_idx);
+
+struct drm_fabric_port *drm_fabric_port_find_get(u32 ep_id, u32 port_idx);
+void drm_fabric_port_put(struct drm_fabric_port *port);
+struct drm_fabric_endpoint *drm_fabric_endpoint_get(struct drm_fabric_endpoint *ep);
+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);
+
+#endif /* __DRM_FABRIC_INTERNAL_H__ */
diff --git a/include/drm/drm_fabric.h b/include/drm/drm_fabric.h
new file mode 100644
index 000000000000..68d8acec2d38
--- /dev/null
+++ b/include/drm/drm_fabric.h
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/*
+ * DRM Fabric driver API: common object model for GPU interconnect topology
+ * (fabric, endpoint, port and peer relationships).
+ */
+
+#ifndef __DRM_FABRIC_H__
+#define __DRM_FABRIC_H__
+
+#include <linux/completion.h>
+#include <linux/refcount.h>
+#include <linux/types.h>
+#include <linux/xarray.h>
+
+enum drm_fabric_type {
+ /* Zero is invalid; concrete fabric types start at 1. */
+ DRM_FABRIC_TYPE_SYNTHETIC = 1,
+};
+
+enum drm_fabric_port_state {
+ DRM_FABRIC_PORT_STATE_UNKNOWN,
+ DRM_FABRIC_PORT_STATE_INACTIVE,
+ DRM_FABRIC_PORT_STATE_ACTIVE,
+ DRM_FABRIC_PORT_STATE_DEGRADED,
+};
+
+enum drm_fabric_peer_type {
+ DRM_FABRIC_PEER_TYPE_ACCEL = 1,
+ DRM_FABRIC_PEER_TYPE_SWITCH,
+};
+
+struct device;
+
+/**
+ * struct drm_fabric_port_desc - Port descriptor for drm_fabric_endpoint_register()
+ */
+struct drm_fabric_port_desc {
+ /** @index: per-endpoint port index */
+ u32 index;
+ /** @max_lane_count: maximum provider-reported link width capability, 0 if not reported */
+ u32 max_lane_count;
+ /**
+ * @max_lane_signaling_rate_mbps: maximum provider-reported per-lane
+ * signaling rate in decimal megabits per second before encoding, FEC
+ * and protocol overhead, 0 if not reported. A capability, not the
+ * negotiated rate: multiplying it by @max_lane_count does not give
+ * usable bandwidth.
+ */
+ u32 max_lane_signaling_rate_mbps;
+};
+
+/**
+ * struct drm_fabric_endpoint_desc - Endpoint descriptor for drm_fabric_endpoint_register()
+ */
+struct drm_fabric_endpoint_desc {
+ /**
+ * @fabric_ep_id: accelerator identity within the fabric's identity
+ * domain, unique among its members and stable for the duration of
+ * membership; a duplicate is rejected with -EEXIST. Distinct from the
+ * core-assigned &drm_fabric_endpoint.id.
+ */
+ u64 fabric_ep_id;
+ /** @name: human-readable endpoint name */
+ const char *name;
+ /** @parent: required backing device, provides dev_name and bus */
+ struct device *parent;
+ /** @ops: provider driver callbacks */
+ const struct drm_fabric_ops *ops;
+ /**
+ * @priv: provider cookie reachable from every @ops callback via @ep
+ * or @port. The core stores it, never dereferences it, never frees it.
+ */
+ void *priv;
+
+ /** @ports: fixed set of ports, copied by the core */
+ const struct drm_fabric_port_desc *ports;
+ /** @num_ports: number of entries in @ports */
+ unsigned int num_ports;
+};
+
+/**
+ * struct drm_fabric_desc - Fabric descriptor for drm_fabric_register()
+ */
+struct drm_fabric_desc {
+ /** @type: fabric interconnect technology */
+ enum drm_fabric_type type;
+ /** @name: human-readable fabric name */
+ const char *name;
+ /** @instance_id: vendor-unique instance identifier */
+ u64 instance_id;
+};
+
+/**
+ * struct drm_fabric - Fabric object
+ */
+struct drm_fabric {
+ /** @id: kernel-local identifier, assigned by the core */
+ u32 id;
+ /** @type: fabric interconnect technology */
+ enum drm_fabric_type type;
+ /** @instance_id: vendor-unique identifier within @type */
+ u64 instance_id;
+ /** @name: human-readable fabric name */
+ char name[32];
+
+ /** @refs: reference count */
+ refcount_t refs;
+};
+
+/**
+ * struct drm_fabric_endpoint - Endpoint object
+ */
+struct drm_fabric_endpoint {
+ /** @id: kernel-local identifier, assigned by the core */
+ u32 id;
+ /** @fabric_ep_id: provider's stable fabric-local identity */
+ u64 fabric_ep_id;
+ /** @name: human-readable endpoint name */
+ char name[32];
+ /** @parent: backing device, provides dev_name and bus_name */
+ struct device *parent;
+
+ /** @fabric: parent fabric */
+ struct drm_fabric *fabric;
+
+ /** @ops: provider driver callbacks */
+ const struct drm_fabric_ops *ops;
+ /** @priv: provider cookie, as supplied at registration */
+ void *priv;
+
+ /** @ports: xarray of &struct drm_fabric_port owned by this endpoint */
+ struct xarray ports;
+ /** @num_ports: number of ports in @ports */
+ unsigned int num_ports;
+
+ /** @refs: reference count */
+ refcount_t refs;
+ /** @unregistered: completed once the endpoint is fully unregistered */
+ struct completion unregistered;
+};
+
+/**
+ * drm_fabric_endpoint_fabric_id() - Wire fabric-id for an endpoint
+ * @ep: endpoint to query
+ *
+ * Return: the parent fabric id.
+ */
+static inline u32
+drm_fabric_endpoint_fabric_id(const struct drm_fabric_endpoint *ep)
+{
+ return ep->fabric->id;
+}
+
+/**
+ * struct drm_fabric_peer - Directly adjacent far-end identity
+ *
+ * A value descriptor, not a reference to a live object. While its owning port
+ * remains registered, it is retained until explicitly retracted even if the
+ * object it names stops resolving locally. See "Peer semantics" in
+ * Documentation/gpu/drm-fabric.rst.
+ */
+struct drm_fabric_peer {
+ /**
+ * @peer_id: identity of the far-end object, read per @peer_type: an
+ * accelerator's fabric_ep_id, or an opaque switch identity. The two
+ * are separate namespaces, so one value names different objects
+ * under each type, and neither has to resolve locally.
+ */
+ u64 peer_id;
+
+ /** @peer_type: kind of far-end device; selects the @peer_id namespace */
+ enum drm_fabric_peer_type peer_type;
+ /** @port_index: far-end port index within the object @peer_id names */
+ u32 port_index;
+};
+
+/**
+ * struct drm_fabric_port - Port object
+ */
+struct drm_fabric_port {
+ /** @index: per-endpoint port index */
+ u32 index;
+ /** @oper_state: operational (link) state */
+ enum drm_fabric_port_state oper_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;
+
+ /** @has_peer: whether @peer holds a valid descriptor */
+ bool has_peer;
+ /** @peer: neighbor description, valid only while @has_peer is set */
+ struct drm_fabric_peer peer;
+
+ /** @endpoint: parent endpoint */
+ struct drm_fabric_endpoint *endpoint;
+};
+
+/**
+ * struct drm_fabric_port_stats - Per-port statistics for the port_stats_get() callback
+ *
+ * Counters are monotonic for the lifetime of the provider's registration and
+ * are not clearable through this uAPI. Link error accounting belongs to DRM
+ * RAS and is deliberately absent here.
+ */
+struct drm_fabric_port_stats {
+ /** @read_bytes: bytes received on the port */
+ u64 read_bytes;
+ /** @write_bytes: bytes transmitted on the port */
+ u64 write_bytes;
+ /** @link_down_count: link-down transitions */
+ u64 link_down_count;
+ /** @retrain_count: link retrain events */
+ u64 retrain_count;
+};
+
+/**
+ * struct drm_fabric_ops - Provider driver callbacks
+ *
+ * A callback that is not supplied makes the matching netlink operation return
+ * -EOPNOTSUPP.
+ */
+struct drm_fabric_ops {
+ /**
+ * @port_stats_get: read per-port statistics. The core pins the port
+ * through its endpoint and calls without drm_fabric_lock held, so this
+ * may sleep. It must not re-enter a core API that takes the lock or take
+ * a provider lock from which the core may be called.
+ *
+ * Populate every field or return -EOPNOTSUPP. Zero is a valid count,
+ * not an unsupported marker.
+ */
+ int (*port_stats_get)(struct drm_fabric_port *port,
+ struct drm_fabric_port_stats *stats);
+};
+
+struct drm_fabric *drm_fabric_register(const struct drm_fabric_desc *desc);
+int drm_fabric_unregister(struct drm_fabric *fabric);
+
+struct drm_fabric_endpoint *
+drm_fabric_endpoint_register(struct drm_fabric *fabric,
+ const struct drm_fabric_endpoint_desc *desc);
+void drm_fabric_endpoint_unregister(struct drm_fabric_endpoint *ep);
+
+struct drm_fabric_port *
+drm_fabric_endpoint_port(struct drm_fabric_endpoint *ep, u32 port_index);
+
+int drm_fabric_port_set_peer(struct drm_fabric_port *port,
+ const struct drm_fabric_peer *peer);
+int drm_fabric_port_unset_peer(struct drm_fabric_port *port);
+
+void drm_fabric_port_set_oper(struct drm_fabric_port *port,
+ enum drm_fabric_port_state state);
+
+#endif /* __DRM_FABRIC_H__ */
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 02/12] drm/fabric: add query uAPI and generated headers
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 ` Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 03/12] drm/fabric: implement query netlink operations Konstantin Sinyuk
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
From: Ilia Levi <ilia.levi@intel.com>
Add a YNL specification for the drm-fabric Generic Netlink family and
generate its userspace and kernel headers, giving the object model from
the previous patch a userspace-visible wire format.
The read-only ABI provides FABRIC_GET, ENDPOINT_GET, PORT_GET and
PORT_STATS_GET as single lookups and multipart dumps. Notifications report
fabric and endpoint lifecycle, peer adjacency and port state through the
monitor multicast group.
The wire format separates core-assigned object IDs from provider-defined
endpoint and peer identities and carries a topology-generation token.
Document the family, the YAML-as-contract rule and the append-only
stability guarantees in Documentation/gpu/drm-fabric.rst, together with
the full identity and consistency semantics.
Generate the headers from the tree root with:
tools/net/ynl/pyynl/ynl_gen_c.py --mode uapi --header \
--spec Documentation/netlink/specs/drm_fabric.yaml \
-o include/uapi/drm/drm_fabric.h
tools/net/ynl/pyynl/ynl_gen_c.py --mode kernel --header \
--spec Documentation/netlink/specs/drm_fabric.yaml \
-o drivers/gpu/drm/fabric/drm_fabric_nl.h
The generated operation and policy source references the doit and dumpit
handlers, so it lands with their implementation in the next patch. The
generated uAPI header replaces the enums defined locally in the previous
patch.
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 | 35 ++
Documentation/netlink/specs/drm_fabric.yaml | 427 ++++++++++++++++++++
drivers/gpu/drm/fabric/drm_fabric_nl.h | 36 ++
include/drm/drm_fabric.h | 21 +-
include/uapi/drm/drm_fabric.h | 134 ++++++
5 files changed, 635 insertions(+), 18 deletions(-)
create mode 100644 Documentation/netlink/specs/drm_fabric.yaml
create mode 100644 drivers/gpu/drm/fabric/drm_fabric_nl.h
create mode 100644 include/uapi/drm/drm_fabric.h
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index 99b189dc614e..d2c9e694b67f 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -140,3 +140,38 @@ 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.
+
+Generic Netlink family
+======================
+
+DRM Fabric is exposed through the ``drm-fabric`` Generic Netlink family.
+Dump enumeration and asynchronous notifications fit this multi-object model
+better than one-value-per-file sysfs. The family follows the YAML/ynl
+discipline used by DRM RAS; devlink's device hierarchy does not represent a
+fabric spanning multiple DRM devices.
+
+YAML specification
+------------------
+
+The interface is described in a YAML specification
+``Documentation/netlink/specs/drm_fabric.yaml``, which is the source of truth for
+the wire format. It auto-generates the uAPI header
+(``include/uapi/drm/drm_fabric.h``) and the kernel glue via
+``tools/net/ynl/pyynl/ynl_gen_c.py``. Generated files must never be edited by
+hand; regenerate them with ``tools/net/ynl/ynl-regen.sh`` after any spec change.
+
+uAPI stability
+--------------
+
+The YAML specification is the contract. New attributes, commands and enum values
+are added append-only; existing attribute numbers, command numbers and meanings
+are never reused. Requests are strictly validated, so an unknown attribute in a
+request is rejected; user space should ignore attributes it does not recognize
+in replies and notifications. The family is versioned through
+``DRM_FABRIC_FAMILY_VERSION``.
+
+Kernel-local ``fabric-id`` and ``endpoint-id`` values identify live registry
+objects and are not persistent hardware identities: they remain valid for the
+lifetime of the registered object, but may disappear or be reused after the
+object is unregistered. ``instance-id`` and ``fabric-ep-id`` carry
+provider-defined identity, whose scope is described by the containing object.
diff --git a/Documentation/netlink/specs/drm_fabric.yaml b/Documentation/netlink/specs/drm_fabric.yaml
new file mode 100644
index 000000000000..778443d92a0d
--- /dev/null
+++ b/Documentation/netlink/specs/drm_fabric.yaml
@@ -0,0 +1,427 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+---
+name: drm-fabric
+protocol: genetlink
+uapi-header: drm/drm_fabric.h
+
+doc: |
+ Common object model for GPU and accelerator interconnect topology
+ (xGMI, UALink and similar), exposed over Generic Netlink.
+
+ A @fabric groups @endpoint objects; each @endpoint owns a fixed set of
+ @port objects; and a @port may carry a @peer naming the directly adjacent
+ far end. The core records direct adjacency only, and a @peer need not
+ resolve locally.
+
+ This read-only interface is confined to the initial network namespace.
+ Providers own topology; userspace queries it and receives notifications.
+ Replies and notifications carry a topology-generation token so an
+ interrupted or changed dump can be discarded and retried.
+
+definitions:
+ -
+ name: type
+ type: enum
+ value-start: 1
+ doc: >-
+ Fabric interconnect technology. New types are added with the provider
+ that first reports them. Zero is not a valid type.
+ entries:
+ - synthetic
+
+ -
+ name: port-state
+ type: enum
+ doc: Operational (link) state of a port, as observed by the driver.
+ entries:
+ - unknown
+ - inactive
+ - active
+ - degraded
+
+ -
+ name: peer-type
+ type: enum
+ value-start: 1
+ doc: Kind of device directly adjacent at a port's far end.
+ entries: [accel, switch]
+
+attribute-sets:
+ -
+ name: drm-fabric
+ enum-name: drm-fabric-a
+ doc: Top-level attributes, shared by all drm-fabric operations.
+ attributes:
+ -
+ name: fabric
+ type: nest
+ nested-attributes: fabric
+ doc: A fabric object (full nest).
+ -
+ name: endpoint
+ type: nest
+ nested-attributes: endpoint
+ doc: An endpoint object (full nest).
+ -
+ name: port
+ type: nest
+ nested-attributes: port
+ doc: A port object (full nest).
+ -
+ name: port-stats
+ type: nest
+ nested-attributes: port-stats
+ doc: Per-port statistics (full nest).
+ -
+ name: fabric-id
+ type: u32
+ doc: Kernel-local fabric identifier, assigned by the core.
+ -
+ name: endpoint-id
+ type: u32
+ doc: Kernel-local endpoint identifier, assigned by the core.
+ -
+ name: port-index
+ type: u32
+ doc: Per-endpoint port index.
+ -
+ name: dev-name
+ type: string
+ doc: Bus-specific identity (e.g. PCI BDF 0000:c1:00.0).
+ -
+ name: bus-name
+ type: string
+ doc: Disambiguate dev-name by bus (e.g. pci).
+ -
+ name: peer
+ type: nest
+ nested-attributes: peer
+ doc: A port's neighbor description (full nest).
+ -
+ name: topology-generation
+ type: u32
+ doc: >-
+ Family-global topology change token, not an event counter. Nonzero,
+ advances on every committed topology-visible transition and may wrap,
+ but zero is never emitted. Compare for equality only; treat any
+ change, or NLM_F_DUMP_INTR on a dump, as a reason to re-dump. A GET
+ or DUMP reply carries its snapshot's generation; a notification
+ carries the generation of its change. Statistics reads do not
+ advance it.
+
+ -
+ name: fabric
+ name-prefix: drm-fabric-a-fabric-attrs-
+ enum-name: drm-fabric-a-fabric-attrs
+ doc: Attributes describing a fabric object.
+ attributes:
+ -
+ name: pad
+ type: pad
+ -
+ name: fabric-id
+ type: u32
+ doc: Kernel-local fabric identifier, assigned by the core.
+ -
+ name: type
+ type: u32
+ enum: type
+ doc: Fabric interconnect technology, see enum type.
+ -
+ name: name
+ type: string
+ doc: Human-readable fabric name.
+ -
+ name: instance-id
+ type: u64
+ doc: Vendor-unique fabric instance identifier (e.g. a hive ID).
+
+ -
+ name: endpoint
+ name-prefix: drm-fabric-a-endpoint-attrs-
+ enum-name: drm-fabric-a-endpoint-attrs
+ doc: Attributes describing an endpoint object.
+ attributes:
+ -
+ name: pad
+ type: pad
+ -
+ name: endpoint-id
+ type: u32
+ doc: Kernel-local endpoint identifier, assigned by the core.
+ -
+ name: fabric-id
+ type: u32
+ doc: Identifier of the parent fabric.
+ -
+ name: fabric-ep-id
+ type: u64
+ doc: Vendor's stable fabric-local endpoint identifier.
+ -
+ name: name
+ type: string
+ doc: Human-readable endpoint name.
+ -
+ name: dev-name
+ type: string
+ doc: Bus-specific identity (e.g. PCI BDF 0000:c1:00.0).
+ -
+ name: bus-name
+ type: string
+ doc: The bus of the backing device (e.g. pci).
+
+ -
+ name: port
+ name-prefix: drm-fabric-a-port-attrs-
+ enum-name: drm-fabric-a-port-attrs
+ doc: Attributes describing a port object.
+ attributes:
+ -
+ name: port-index
+ type: u32
+ doc: Per-endpoint port index.
+ -
+ name: endpoint-id
+ type: u32
+ doc: Identifier of the endpoint that owns this port.
+ -
+ name: oper-state
+ type: u32
+ enum: port-state
+ doc: Port's operational state.
+
+ -
+ name: max-lane-count
+ type: u32
+ doc: >-
+ Maximum provider-reported lane count (link width capability).
+ 0 means unknown or not reported.
+ -
+ name: max-lane-signaling-rate-mbps
+ type: u32
+ doc: >-
+ Maximum provider-reported per-lane signaling rate in decimal
+ megabits per second, before encoding, FEC and protocol overhead.
+ Zero means unknown. This is a capability, not the negotiated rate
+ or usable payload bandwidth.
+ -
+ name: peer
+ type: nest
+ nested-attributes: peer
+ doc: Peer info, absent if the port has no neighbor set.
+
+ -
+ name: peer
+ name-prefix: drm-fabric-a-peer-attrs-
+ enum-name: drm-fabric-a-peer-attrs
+ doc: Attributes describing a port's neighbor (peer).
+ attributes:
+ -
+ name: pad
+ type: pad
+ -
+ name: peer-id
+ type: u64
+ doc: >-
+ Type-qualified identity of the directly adjacent far-end object.
+ For type=accel, this is the far-end accelerator's fabric-ep-id; for
+ type=switch, it is an opaque provider-defined identity naming no
+ local endpoint. Accelerator and switch identities occupy separate
+ namespaces, and neither must resolve locally.
+ -
+ name: type
+ type: u32
+ enum: peer-type
+ doc: Kind of peer device; selects the namespace and meaning of peer-id.
+ -
+ name: port-index
+ type: u32
+ doc: Far-end port index, in the peer's own port numbering.
+
+ -
+ name: port-stats
+ name-prefix: drm-fabric-a-port-stats-attrs-
+ enum-name: drm-fabric-a-port-stats-attrs
+ doc: |-
+ Per-port statistics. All-or-none: on success every base counter is
+ present and zero is a valid count. A provider unable to supply the
+ complete base set returns -EOPNOTSUPP.
+ attributes:
+ -
+ name: pad
+ type: pad
+ -
+ name: endpoint-id
+ type: u32
+ doc: Identifier of the endpoint that owns the port.
+ -
+ name: port-index
+ type: u32
+ doc: Per-endpoint port index.
+ -
+ name: read-bytes
+ type: u64
+ doc: Bytes received on the port.
+ -
+ name: write-bytes
+ type: u64
+ doc: Bytes transmitted on the port.
+ -
+ name: link-down-count
+ type: u64
+ doc: Number of link-down transitions.
+ -
+ name: retrain-count
+ type: u64
+ doc: Number of link retrain events.
+
+operations:
+ enum-name: drm-fabric-cmd
+ list:
+ -
+ name: fabric-get
+ doc: Enumerate registered fabrics
+ attribute-set: drm-fabric
+ do:
+ request:
+ attributes:
+ - fabric-id
+ reply:
+ attributes:
+ - fabric
+ - topology-generation
+ dump:
+ reply:
+ attributes:
+ - fabric
+ - topology-generation
+
+ -
+ name: endpoint-get
+ doc: Enumerate registered endpoints, optionally filtered by fabric-id
+ attribute-set: drm-fabric
+ do:
+ request:
+ attributes:
+ - endpoint-id
+ - dev-name
+ - bus-name
+ reply:
+ attributes:
+ - endpoint
+ - topology-generation
+ dump:
+ request:
+ attributes:
+ - fabric-id
+ reply:
+ attributes:
+ - endpoint
+ - topology-generation
+
+ -
+ name: port-get
+ doc: Enumerate ports, optionally filtered by endpoint-id
+ attribute-set: drm-fabric
+ do:
+ request:
+ attributes:
+ - endpoint-id
+ - port-index
+ reply:
+ attributes:
+ - port
+ - topology-generation
+ dump:
+ request:
+ attributes:
+ - endpoint-id
+ reply:
+ attributes:
+ - port
+ - topology-generation
+
+ -
+ name: port-stats-get
+ doc: Query port-scoped statistics
+ attribute-set: drm-fabric
+ do:
+ request:
+ attributes:
+ - endpoint-id
+ - port-index
+ reply:
+ attributes:
+ - port-stats
+ dump:
+ request:
+ attributes:
+ - endpoint-id
+ reply:
+ attributes:
+ - port-stats
+
+ -
+ name: port-change-ntf
+ doc: |
+ Port state change notification. Reuses the port-get
+ reply shape (full port nest).
+ notify: port-get
+
+ -
+ name: port-peer-create-ntf
+ doc: A port's neighbor has been set.
+ attribute-set: drm-fabric
+ event:
+ attributes:
+ - endpoint-id
+ - port-index
+ - peer
+ - topology-generation
+
+ -
+ name: port-peer-delete-ntf
+ doc: A port's neighbor has been unset.
+ attribute-set: drm-fabric
+ event:
+ attributes:
+ - endpoint-id
+ - port-index
+ - peer
+ - topology-generation
+
+ -
+ name: endpoint-create-ntf
+ doc: |
+ Endpoint registration notification, fired after the endpoint is
+ committed to the registry. Reuses the endpoint-get reply shape
+ (full endpoint nest).
+ notify: endpoint-get
+
+ -
+ name: endpoint-delete-ntf
+ doc: |
+ Endpoint unregistration notification, fired before the endpoint is
+ removed from the registry so its identity is still resolvable.
+ Reuses the endpoint-get reply shape (full endpoint nest).
+ notify: endpoint-get
+
+ -
+ name: fabric-create-ntf
+ doc: |
+ Fabric registration notification, fired after the fabric is committed
+ to the registry. Reuses the fabric-get reply shape (full fabric nest).
+ notify: fabric-get
+
+ -
+ name: fabric-delete-ntf
+ doc: |
+ Fabric removal notification, fired before the fabric is removed from
+ the registry so its identity is still resolvable. Reuses the fabric-get
+ reply shape (full fabric nest).
+ notify: fabric-get
+
+mcast-groups:
+ list:
+ -
+ name: monitor
diff --git a/drivers/gpu/drm/fabric/drm_fabric_nl.h b/drivers/gpu/drm/fabric/drm_fabric_nl.h
new file mode 100644
index 000000000000..8cedc004260f
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_nl.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit directly, auto-generated from: */
+/* Documentation/netlink/specs/drm_fabric.yaml */
+/* YNL-GEN kernel header */
+/* To regenerate run: tools/net/ynl/ynl-regen.sh */
+
+#ifndef _LINUX_DRM_FABRIC_GEN_H
+#define _LINUX_DRM_FABRIC_GEN_H
+
+#include <net/netlink.h>
+#include <net/genetlink.h>
+
+#include <uapi/drm/drm_fabric.h>
+
+int drm_fabric_nl_fabric_get_doit(struct sk_buff *skb, struct genl_info *info);
+int drm_fabric_nl_fabric_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb);
+int drm_fabric_nl_endpoint_get_doit(struct sk_buff *skb,
+ struct genl_info *info);
+int drm_fabric_nl_endpoint_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb);
+int drm_fabric_nl_port_get_doit(struct sk_buff *skb, struct genl_info *info);
+int drm_fabric_nl_port_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb);
+int drm_fabric_nl_port_stats_get_doit(struct sk_buff *skb,
+ struct genl_info *info);
+int drm_fabric_nl_port_stats_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb);
+
+enum {
+ DRM_FABRIC_NLGRP_MONITOR,
+};
+
+extern struct genl_family drm_fabric_nl_family;
+
+#endif /* _LINUX_DRM_FABRIC_GEN_H */
diff --git a/include/drm/drm_fabric.h b/include/drm/drm_fabric.h
index 68d8acec2d38..136100b3da67 100644
--- a/include/drm/drm_fabric.h
+++ b/include/drm/drm_fabric.h
@@ -4,8 +4,8 @@
*/
/*
- * DRM Fabric driver API: common object model for GPU interconnect topology
- * (fabric, endpoint, port and peer relationships).
+ * Common object model for GPU interconnect topology: fabric, endpoint, port
+ * and peer relationships.
*/
#ifndef __DRM_FABRIC_H__
@@ -16,22 +16,7 @@
#include <linux/types.h>
#include <linux/xarray.h>
-enum drm_fabric_type {
- /* Zero is invalid; concrete fabric types start at 1. */
- DRM_FABRIC_TYPE_SYNTHETIC = 1,
-};
-
-enum drm_fabric_port_state {
- DRM_FABRIC_PORT_STATE_UNKNOWN,
- DRM_FABRIC_PORT_STATE_INACTIVE,
- DRM_FABRIC_PORT_STATE_ACTIVE,
- DRM_FABRIC_PORT_STATE_DEGRADED,
-};
-
-enum drm_fabric_peer_type {
- DRM_FABRIC_PEER_TYPE_ACCEL = 1,
- DRM_FABRIC_PEER_TYPE_SWITCH,
-};
+#include <uapi/drm/drm_fabric.h>
struct device;
diff --git a/include/uapi/drm/drm_fabric.h b/include/uapi/drm/drm_fabric.h
new file mode 100644
index 000000000000..b6c7bd6e35f0
--- /dev/null
+++ b/include/uapi/drm/drm_fabric.h
@@ -0,0 +1,134 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit directly, auto-generated from: */
+/* Documentation/netlink/specs/drm_fabric.yaml */
+/* YNL-GEN uapi header */
+/* To regenerate run: tools/net/ynl/ynl-regen.sh */
+
+#ifndef _UAPI_LINUX_DRM_FABRIC_H
+#define _UAPI_LINUX_DRM_FABRIC_H
+
+#define DRM_FABRIC_FAMILY_NAME "drm-fabric"
+#define DRM_FABRIC_FAMILY_VERSION 1
+
+/*
+ * Fabric interconnect technology. New types are added with the provider that
+ * first reports them. Zero is not a valid type.
+ */
+enum drm_fabric_type {
+ DRM_FABRIC_TYPE_SYNTHETIC = 1,
+};
+
+/*
+ * Operational (link) state of a port, as observed by the driver.
+ */
+enum drm_fabric_port_state {
+ DRM_FABRIC_PORT_STATE_UNKNOWN,
+ DRM_FABRIC_PORT_STATE_INACTIVE,
+ DRM_FABRIC_PORT_STATE_ACTIVE,
+ DRM_FABRIC_PORT_STATE_DEGRADED,
+};
+
+/*
+ * Kind of device directly adjacent at a port's far end.
+ */
+enum drm_fabric_peer_type {
+ DRM_FABRIC_PEER_TYPE_ACCEL = 1,
+ DRM_FABRIC_PEER_TYPE_SWITCH,
+};
+
+enum drm_fabric_a {
+ DRM_FABRIC_A_FABRIC = 1,
+ DRM_FABRIC_A_ENDPOINT,
+ DRM_FABRIC_A_PORT,
+ DRM_FABRIC_A_PORT_STATS,
+ DRM_FABRIC_A_FABRIC_ID,
+ DRM_FABRIC_A_ENDPOINT_ID,
+ DRM_FABRIC_A_PORT_INDEX,
+ DRM_FABRIC_A_DEV_NAME,
+ DRM_FABRIC_A_BUS_NAME,
+ DRM_FABRIC_A_PEER,
+ DRM_FABRIC_A_TOPOLOGY_GENERATION,
+
+ __DRM_FABRIC_A_MAX,
+ DRM_FABRIC_A_MAX = (__DRM_FABRIC_A_MAX - 1)
+};
+
+enum drm_fabric_a_fabric_attrs {
+ DRM_FABRIC_A_FABRIC_ATTRS_PAD = 1,
+ DRM_FABRIC_A_FABRIC_ATTRS_FABRIC_ID,
+ DRM_FABRIC_A_FABRIC_ATTRS_TYPE,
+ DRM_FABRIC_A_FABRIC_ATTRS_NAME,
+ DRM_FABRIC_A_FABRIC_ATTRS_INSTANCE_ID,
+
+ __DRM_FABRIC_A_FABRIC_ATTRS_MAX,
+ DRM_FABRIC_A_FABRIC_ATTRS_MAX = (__DRM_FABRIC_A_FABRIC_ATTRS_MAX - 1)
+};
+
+enum drm_fabric_a_endpoint_attrs {
+ DRM_FABRIC_A_ENDPOINT_ATTRS_PAD = 1,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_ENDPOINT_ID,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_FABRIC_ID,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_FABRIC_EP_ID,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_NAME,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_DEV_NAME,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_BUS_NAME,
+
+ __DRM_FABRIC_A_ENDPOINT_ATTRS_MAX,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_MAX = (__DRM_FABRIC_A_ENDPOINT_ATTRS_MAX - 1)
+};
+
+enum drm_fabric_a_port_attrs {
+ DRM_FABRIC_A_PORT_ATTRS_PORT_INDEX = 1,
+ DRM_FABRIC_A_PORT_ATTRS_ENDPOINT_ID,
+ DRM_FABRIC_A_PORT_ATTRS_OPER_STATE,
+ DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_COUNT,
+ DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_SIGNALING_RATE_MBPS,
+ DRM_FABRIC_A_PORT_ATTRS_PEER,
+
+ __DRM_FABRIC_A_PORT_ATTRS_MAX,
+ DRM_FABRIC_A_PORT_ATTRS_MAX = (__DRM_FABRIC_A_PORT_ATTRS_MAX - 1)
+};
+
+enum drm_fabric_a_peer_attrs {
+ DRM_FABRIC_A_PEER_ATTRS_PAD = 1,
+ DRM_FABRIC_A_PEER_ATTRS_PEER_ID,
+ DRM_FABRIC_A_PEER_ATTRS_TYPE,
+ DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX,
+
+ __DRM_FABRIC_A_PEER_ATTRS_MAX,
+ DRM_FABRIC_A_PEER_ATTRS_MAX = (__DRM_FABRIC_A_PEER_ATTRS_MAX - 1)
+};
+
+enum drm_fabric_a_port_stats_attrs {
+ DRM_FABRIC_A_PORT_STATS_ATTRS_PAD = 1,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_ENDPOINT_ID,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_PORT_INDEX,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_READ_BYTES,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_WRITE_BYTES,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_LINK_DOWN_COUNT,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_RETRAIN_COUNT,
+
+ __DRM_FABRIC_A_PORT_STATS_ATTRS_MAX,
+ DRM_FABRIC_A_PORT_STATS_ATTRS_MAX = (__DRM_FABRIC_A_PORT_STATS_ATTRS_MAX - 1)
+};
+
+enum drm_fabric_cmd {
+ DRM_FABRIC_CMD_FABRIC_GET = 1,
+ DRM_FABRIC_CMD_ENDPOINT_GET,
+ DRM_FABRIC_CMD_PORT_GET,
+ DRM_FABRIC_CMD_PORT_STATS_GET,
+ DRM_FABRIC_CMD_PORT_CHANGE_NTF,
+ DRM_FABRIC_CMD_PORT_PEER_CREATE_NTF,
+ DRM_FABRIC_CMD_PORT_PEER_DELETE_NTF,
+ DRM_FABRIC_CMD_ENDPOINT_CREATE_NTF,
+ DRM_FABRIC_CMD_ENDPOINT_DELETE_NTF,
+ DRM_FABRIC_CMD_FABRIC_CREATE_NTF,
+ DRM_FABRIC_CMD_FABRIC_DELETE_NTF,
+
+ __DRM_FABRIC_CMD_MAX,
+ DRM_FABRIC_CMD_MAX = (__DRM_FABRIC_CMD_MAX - 1)
+};
+
+#define DRM_FABRIC_MCGRP_MONITOR "monitor"
+
+#endif /* _UAPI_LINUX_DRM_FABRIC_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 03/12] drm/fabric: implement query netlink operations
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 ` Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 04/12] drm/fabric: add read-only synthetic provider Konstantin Sinyuk
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
Register the drm-fabric Generic Netlink family and connect the four
read-only query commands to the core object model, adding the generated
operation and policy source that dispatches to them. Their reply shapes
are:
fabric-get -> fabric, topology-generation
endpoint-get -> endpoint, topology-generation
port-get -> port, topology-generation
port-stats-get -> port-stats
Support single-object lookup and multipart dumps for fabrics, endpoints and
ports, with optional fabric and endpoint filters. Preserve the nested
endpoint/port dump cursor across batches, including endpoint removal and
terminal port indices.
Port statistics are optional: a targeted unsupported request returns
-EOPNOTSUPP, while a dump skips unsupported ports and continues; other
provider errors terminate the dump. Use topology-generation for dump
consistency and report concurrent changes with NLM_F_DUMP_INTR.
Emit fabric, endpoint, port-state and peer notifications through the
monitor multicast group. Advance topology-generation before emitting each
notification, so the event carries the same post-change value as a later
query.
Queries and notifications are confined to init_net. The query commands,
notifications and dump-consistency behavior are documented in
Documentation/gpu/drm-fabric.rst.
Co-developed-by: Ilia Levi <ilia.levi@intel.com>
Signed-off-by: Ilia Levi <ilia.levi@intel.com>
Signed-off-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
Documentation/gpu/drm-fabric.rst | 148 ++-
drivers/gpu/drm/fabric/Kconfig | 6 +-
drivers/gpu/drm/fabric/Makefile | 2 +-
drivers/gpu/drm/fabric/drm_fabric.c | 29 +-
drivers/gpu/drm/fabric/drm_fabric_internal.h | 19 +
drivers/gpu/drm/fabric/drm_fabric_netlink.c | 1035 ++++++++++++++++++
drivers/gpu/drm/fabric/drm_fabric_nl.c | 125 +++
7 files changed, 1345 insertions(+), 19 deletions(-)
create mode 100644 drivers/gpu/drm/fabric/drm_fabric_netlink.c
create mode 100644 drivers/gpu/drm/fabric/drm_fabric_nl.c
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index d2c9e694b67f..a8b33d6cd618 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -18,8 +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.
-* Offer a flexible, future-proof interface that can be extended with new fabric
- types and attributes without breaking the uAPI.
+* 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
accelerator attachments, links and their peers.
@@ -84,10 +84,14 @@ 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.
-Endpoint teardown removes the endpoint's owned half-edges without generating
-a separate event for each port: the delete already describes the transition,
-so removing an endpoint advances the topology generation once rather than
-once per child port.
+``port-peer-delete-ntf`` reports an explicitly retracted half-edge; it is
+not emitted when a peer merely becomes locally unresolvable, so its absence
+is not evidence the far end is still reachable.
+
+Endpoint teardown removes the endpoint's owned half-edges without
+generating a separate event for each port: the delete already describes
+the transition, so removing an endpoint advances the topology generation
+once rather than once per child port.
Driver API
----------
@@ -175,3 +179,135 @@ objects and are not persistent hardware identities: they remain valid for the
lifetime of the registered object, but may disappear or be reused after the
object is unregistered. ``instance-id`` and ``fabric-ep-id`` carry
provider-defined identity, whose scope is described by the containing object.
+
+Query operations
+================
+
+User space enumerates topology with four read-only commands, each supporting a
+single lookup (``do``) and a bulk dump (``dump``):
+
+* ``fabric-get`` -- enumerate fabrics (``do`` by ``fabric-id``, ``dump`` for all).
+* ``endpoint-get`` -- enumerate endpoints, optionally filtered by ``fabric-id``,
+ or resolve one by ``endpoint-id`` or backing ``dev-name``/``bus-name``.
+* ``port-get`` -- enumerate ports, filtered by ``endpoint-id``. Ports may report
+ the provider's maximum capability as ``max-lane-count`` and
+ ``max-lane-signaling-rate-mbps`` (zero means unknown); these are maxima, not
+ the currently negotiated width or rate.
+* ``port-stats-get`` -- per-port statistics, filtered by ``endpoint-id``. A
+ targeted request for a port whose provider does not implement
+ ``port_stats_get`` returns ``-EOPNOTSUPP``. During a dump, ports without
+ statistics support are omitted and enumeration continues with later ports;
+ any other provider error ends the dump.
+
+Notifications
+-------------
+
+Subscribe to the ``monitor`` multicast group to receive asynchronous change
+notifications. Full-object notifications reuse the shape of their matching
+``get`` reply and are declared with ``notify:``; peer-link notifications carry
+a partial payload and are declared with ``event:``.
+
+.. list-table::
+ :header-rows: 1
+
+ * - Notification
+ - Trigger
+ - Payload
+ * - ``fabric-create-ntf``
+ - a fabric is registered by a provider
+ - reuses ``fabric-get``
+ * - ``fabric-delete-ntf``
+ - a provider-owned fabric is unregistered
+ - reuses ``fabric-get``
+ * - ``endpoint-create-ntf``
+ - an endpoint is registered
+ - reuses ``endpoint-get``
+ * - ``endpoint-delete-ntf``
+ - an endpoint is unregistered
+ - reuses ``endpoint-get``
+ * - ``port-change-ntf``
+ - a port's operational state changes
+ - reuses ``port-get``
+ * - ``port-peer-create-ntf``
+ - a port's peer descriptor is set by its provider
+ - partial (``event:``)
+ * - ``port-peer-delete-ntf``
+ - a port's peer descriptor is explicitly unset by its provider; never
+ emitted for an implicit half-edge loss (see `Peer semantics`_)
+ - partial (``event:``)
+
+Notifications are best-effort. A listener that detects loss, restarts, or
+receives an interrupted dump must rebuild state with the query commands.
+
+Topology generation and dump consistency
+----------------------------------------
+
+The core keeps a nonzero generation counter and advances it whenever topology or
+exposed state changes. It is surfaced as the ``topology-generation`` attribute on
+``fabric-get``, ``endpoint-get`` and ``port-get`` replies and on the topology
+notifications. A provider-reported change advances the generation before its
+notification is serialized, so an event carries the post-change value that a
+later ``get``/``dump`` will also report.
+
+``topology-generation`` is a change token, not a timestamp, liveness counter or
+event count: a changed value means topology changed, but the delta between two
+values has no defined meaning and the counter may wrap (it skips zero). Statistics
+reads do not advance it.
+
+The same value backs dump consistency. Every multipart dump samples it and calls
+``genl_dump_check_consistent()``; if it changes between dump batches, Generic
+Netlink marks the dump with ``NLM_F_DUMP_INTR``, meaning the snapshot may be torn.
+User space must then discard the partial result and retry the complete dump. The
+generation does not apply to statistics reads.
+
+Only a dump that serialized at least one entry can carry ``NLM_F_DUMP_INTR``,
+because Generic Netlink arms the consistency check on the first entry it emits.
+A dump that yields no entries at all cannot report interruption, so user space
+should treat an empty result as advisory and re-read ``topology-generation``
+before concluding that the topology is empty.
+
+Namespaces
+----------
+
+DRM Fabric objects describe host-global hardware and are not scoped per network
+namespace. Query and dump operations (``*-get``) are therefore accepted only from
+the initial network namespace; a request from any other network namespace fails
+with ``-EPERM``. The Generic Netlink family is registered ``netnsok`` (so it
+resolves in any network namespace and can return that policy error rather than
+being invisible), but the operations themselves remain confined to ``init_net``.
+Monitor notifications are likewise emitted only into ``init_net``, so a listener
+that joins the multicast group from another network namespace never receives
+them.
+
+Examples
+========
+
+Query the topology with the in-tree YNL tool, pointing it at the spec:
+
+.. code-block:: bash
+
+ # List all fabrics
+ ./tools/net/ynl/pyynl/cli.py \
+ --spec Documentation/netlink/specs/drm_fabric.yaml \
+ --dump fabric-get
+
+Replies follow the shapes described above; a provider must be registered for
+the topology to be non-empty.
+
+List the endpoints of a fabric:
+
+.. code-block:: bash
+
+ ./tools/net/ynl/pyynl/cli.py \
+ --spec Documentation/netlink/specs/drm_fabric.yaml \
+ --dump endpoint-get --json '{"fabric-id": 1}'
+
+Query a single port:
+
+.. code-block:: bash
+
+ ./tools/net/ynl/pyynl/cli.py \
+ --spec Documentation/netlink/specs/drm_fabric.yaml \
+ --do port-get --json '{"endpoint-id": 1, "port-index": 0}'
+
+The family name on the wire is ``drm-fabric``.
diff --git a/drivers/gpu/drm/fabric/Kconfig b/drivers/gpu/drm/fabric/Kconfig
index 21fbfae9863d..7e9c569fd258 100644
--- a/drivers/gpu/drm/fabric/Kconfig
+++ b/drivers/gpu/drm/fabric/Kconfig
@@ -4,9 +4,9 @@ config DRM_FABRIC
tristate "DRM fabric support"
depends on DRM && NET
help
- Enable DRM fabric support. This infrastructure provides the
- core object model and provider API for registered accelerator
- interconnect topologies.
+ Enable DRM fabric support. This infrastructure exposes
+ registered accelerator interconnect topologies to userspace
+ through the drm-fabric generic netlink family.
To compile this as a module, choose M here: the module will be
called drm-fabric.
diff --git a/drivers/gpu/drm/fabric/Makefile b/drivers/gpu/drm/fabric/Makefile
index 3a76f31f1e83..cf9d9d6be3d3 100644
--- a/drivers/gpu/drm/fabric/Makefile
+++ b/drivers/gpu/drm/fabric/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_DRM_FABRIC) += drm-fabric.o
-drm-fabric-y := drm_fabric.o
+drm-fabric-y := drm_fabric.o drm_fabric_netlink.o drm_fabric_nl.o
diff --git a/drivers/gpu/drm/fabric/drm_fabric.c b/drivers/gpu/drm/fabric/drm_fabric.c
index 8769d7bdcde1..ce331656af70 100644
--- a/drivers/gpu/drm/fabric/drm_fabric.c
+++ b/drivers/gpu/drm/fabric/drm_fabric.c
@@ -186,7 +186,7 @@ struct drm_fabric *drm_fabric_register(const struct drm_fabric_desc *desc)
xa_limit_32b, GFP_KERNEL);
if (ret)
return ERR_PTR(ret);
- drm_fabric_base_seq_inc();
+ drm_fabric_emit_fabric_create(fabric, drm_fabric_base_seq_inc());
}
return_ptr(fabric);
@@ -270,7 +270,9 @@ int drm_fabric_unregister(struct drm_fabric *fabric)
*/
if (WARN_ON_ONCE(drm_fabric_has_members(fabric)))
return -EBUSY;
- drm_fabric_base_seq_inc();
+ /* 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);
}
@@ -410,7 +412,7 @@ drm_fabric_endpoint_register(struct drm_fabric *fabric,
break;
drm_fabric_get(fabric);
- drm_fabric_base_seq_inc();
+ drm_fabric_emit_endpoint_create(ep, drm_fabric_base_seq_inc());
}
if (ret) {
@@ -499,7 +501,8 @@ 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;
- drm_fabric_base_seq_inc();
+ /* 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);
}
@@ -587,7 +590,8 @@ int drm_fabric_port_set_peer(struct drm_fabric_port *port,
return -EEXIST;
port->peer = *peer;
port->has_peer = true;
- drm_fabric_base_seq_inc();
+ drm_fabric_emit_port_peer_create(port, peer,
+ drm_fabric_base_seq_inc());
}
return 0;
@@ -612,7 +616,9 @@ int drm_fabric_port_unset_peer(struct drm_fabric_port *port)
scoped_guard(mutex, &drm_fabric_lock) {
if (!port->has_peer)
return -ENOENT;
- drm_fabric_base_seq_inc();
+ /* Emit before clearing to show the peer being removed. */
+ 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));
}
@@ -654,19 +660,24 @@ void drm_fabric_port_set_oper(struct drm_fabric_port *port,
enum drm_fabric_port_state old = port->oper_state;
port->oper_state = state;
- if (old != state)
- drm_fabric_base_seq_inc();
+ if (old != state) {
+ u32 gen = drm_fabric_base_seq_inc();
+
+ drm_fabric_emit_port_change(port, gen);
+ }
}
}
EXPORT_SYMBOL(drm_fabric_port_set_oper);
static int __init drm_fabric_init(void)
{
- return 0;
+ return drm_fabric_netlink_register();
}
static void __exit drm_fabric_exit(void)
{
+ drm_fabric_netlink_unregister();
+
WARN_ON(!xa_empty(&drm_fabric_xa));
WARN_ON(!xa_empty(&drm_fabric_ep_xa));
xa_destroy(&drm_fabric_xa);
diff --git a/drivers/gpu/drm/fabric/drm_fabric_internal.h b/drivers/gpu/drm/fabric/drm_fabric_internal.h
index d454225a0b81..7fc5bc3f33f9 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_internal.h
+++ b/drivers/gpu/drm/fabric/drm_fabric_internal.h
@@ -18,6 +18,9 @@ extern struct xarray drm_fabric_ep_xa; /* Endpoint registry */
extern u32 drm_fabric_base_seq; /* Dump consistency sequence */
+int drm_fabric_netlink_register(void);
+void drm_fabric_netlink_unregister(void);
+
struct drm_fabric *drm_fabric_find_by_id(u32 id);
struct drm_fabric_endpoint *drm_fabric_endpoint_find_by_id(u32 id);
struct drm_fabric_endpoint *
@@ -31,4 +34,20 @@ 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);
+/* 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_port_peer_create(struct drm_fabric_port *port,
+ const struct drm_fabric_peer *peer,
+ u32 generation);
+void drm_fabric_emit_port_peer_delete(struct drm_fabric_port *port,
+ const struct drm_fabric_peer *peer,
+ u32 generation);
+
+void drm_fabric_emit_port_change(struct drm_fabric_port *port, u32 generation);
+
+void drm_fabric_emit_fabric_create(struct drm_fabric *fabric, u32 generation);
+void drm_fabric_emit_fabric_delete(struct drm_fabric *fabric, u32 generation);
+
#endif /* __DRM_FABRIC_INTERNAL_H__ */
diff --git a/drivers/gpu/drm/fabric/drm_fabric_netlink.c b/drivers/gpu/drm/fabric/drm_fabric_netlink.c
new file mode 100644
index 000000000000..fef2d4c8f5bb
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_netlink.c
@@ -0,0 +1,1035 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/module.h>
+#include <net/genetlink.h>
+#include <net/net_namespace.h>
+
+#include <drm/drm_fabric.h>
+#include <uapi/drm/drm_fabric.h>
+
+#include "drm_fabric_internal.h"
+#include "drm_fabric_nl.h"
+
+struct drm_fabric_dump_ctx {
+ unsigned long idx;
+};
+
+static struct drm_fabric_dump_ctx *
+drm_fabric_dump_context(struct netlink_callback *cb)
+{
+ return (struct drm_fabric_dump_ctx *)cb->ctx;
+}
+
+/* Resume cursor for the nested per-port dumps (PORT_GET, PORT_STATS_GET). */
+struct drm_fabric_port_dump_ctx {
+ unsigned long ep_idx;
+ unsigned long port_idx;
+};
+
+static struct drm_fabric_port_dump_ctx *
+drm_fabric_port_dump_context(struct netlink_callback *cb)
+{
+ return (struct drm_fabric_port_dump_ctx *)cb->ctx;
+}
+
+/* netnsok so a non-init_net caller gets -EPERM, not a missing family. */
+static int drm_fabric_nl_host_only(const struct net *net)
+{
+ return net_eq(net, &init_net) ? 0 : -EPERM;
+}
+
+static int drm_fabric_fill_fabric(struct sk_buff *skb,
+ struct drm_fabric *fabric)
+{
+ struct nlattr *nest;
+
+ nest = nla_nest_start(skb, DRM_FABRIC_A_FABRIC);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u32(skb, DRM_FABRIC_A_FABRIC_ATTRS_FABRIC_ID, fabric->id) ||
+ nla_put_u32(skb, DRM_FABRIC_A_FABRIC_ATTRS_TYPE, fabric->type) ||
+ nla_put_string(skb, DRM_FABRIC_A_FABRIC_ATTRS_NAME, fabric->name) ||
+ nla_put_u64_64bit(skb, DRM_FABRIC_A_FABRIC_ATTRS_INSTANCE_ID,
+ fabric->instance_id, DRM_FABRIC_A_FABRIC_ATTRS_PAD)) {
+ nla_nest_cancel(skb, nest);
+ return -EMSGSIZE;
+ }
+
+ nla_nest_end(skb, nest);
+ return 0;
+}
+
+static int drm_fabric_fill_endpoint(struct sk_buff *skb,
+ struct drm_fabric_endpoint *ep)
+{
+ struct nlattr *nest;
+
+ nest = nla_nest_start(skb, DRM_FABRIC_A_ENDPOINT);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u32(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_ENDPOINT_ID, ep->id) ||
+ nla_put_u32(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_FABRIC_ID,
+ drm_fabric_endpoint_fabric_id(ep)) ||
+ nla_put_u64_64bit(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_FABRIC_EP_ID,
+ ep->fabric_ep_id, DRM_FABRIC_A_ENDPOINT_ATTRS_PAD) ||
+ nla_put_string(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_NAME, ep->name) ||
+ nla_put_string(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_DEV_NAME,
+ dev_name(ep->parent)) ||
+ nla_put_string(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_BUS_NAME,
+ dev_bus_name(ep->parent))) {
+ nla_nest_cancel(skb, nest);
+ return -EMSGSIZE;
+ }
+
+ nla_nest_end(skb, nest);
+ return 0;
+}
+
+static int drm_fabric_fill_peer(struct sk_buff *skb,
+ struct drm_fabric_port *port)
+{
+ struct nlattr *nest;
+
+ if (!port->has_peer)
+ return 0;
+
+ nest = nla_nest_start(skb, DRM_FABRIC_A_PORT_ATTRS_PEER);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u64_64bit(skb, DRM_FABRIC_A_PEER_ATTRS_PEER_ID,
+ port->peer.peer_id, DRM_FABRIC_A_PEER_ATTRS_PAD) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PEER_ATTRS_TYPE,
+ port->peer.peer_type) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX,
+ port->peer.port_index)) {
+ nla_nest_cancel(skb, nest);
+ return -EMSGSIZE;
+ }
+
+ nla_nest_end(skb, nest);
+ return 0;
+}
+
+static int drm_fabric_fill_port(struct sk_buff *skb,
+ struct drm_fabric_port *port)
+{
+ struct nlattr *nest;
+ int ret;
+
+ nest = nla_nest_start(skb, DRM_FABRIC_A_PORT);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_PORT_INDEX, port->index) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_ENDPOINT_ID, port->endpoint->id) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_OPER_STATE,
+ port->oper_state) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_COUNT,
+ port->max_lane_count) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_SIGNALING_RATE_MBPS,
+ port->max_lane_signaling_rate_mbps)) {
+ nla_nest_cancel(skb, nest);
+ return -EMSGSIZE;
+ }
+
+ ret = drm_fabric_fill_peer(skb, port);
+ if (ret) {
+ nla_nest_cancel(skb, nest);
+ return ret;
+ }
+
+ nla_nest_end(skb, nest);
+ return 0;
+}
+
+static int drm_fabric_fill_port_stats(struct sk_buff *skb,
+ struct drm_fabric_port *port,
+ struct drm_fabric_port_stats *stats)
+{
+ struct nlattr *nest;
+
+ nest = nla_nest_start(skb, DRM_FABRIC_A_PORT_STATS);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u32(skb, DRM_FABRIC_A_PORT_STATS_ATTRS_ENDPOINT_ID,
+ port->endpoint->id) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PORT_STATS_ATTRS_PORT_INDEX, port->index) ||
+ nla_put_u64_64bit(skb, DRM_FABRIC_A_PORT_STATS_ATTRS_READ_BYTES,
+ stats->read_bytes, DRM_FABRIC_A_PORT_STATS_ATTRS_PAD) ||
+ nla_put_u64_64bit(skb, DRM_FABRIC_A_PORT_STATS_ATTRS_WRITE_BYTES,
+ stats->write_bytes, DRM_FABRIC_A_PORT_STATS_ATTRS_PAD) ||
+ nla_put_u64_64bit(skb, DRM_FABRIC_A_PORT_STATS_ATTRS_LINK_DOWN_COUNT,
+ stats->link_down_count, DRM_FABRIC_A_PORT_STATS_ATTRS_PAD) ||
+ nla_put_u64_64bit(skb, DRM_FABRIC_A_PORT_STATS_ATTRS_RETRAIN_COUNT,
+ stats->retrain_count, DRM_FABRIC_A_PORT_STATS_ATTRS_PAD)) {
+ nla_nest_cancel(skb, nest);
+ return -EMSGSIZE;
+ }
+
+ nla_nest_end(skb, nest);
+ return 0;
+}
+
+int drm_fabric_nl_fabric_get_doit(struct sk_buff *skb, struct genl_info *info)
+{
+ struct drm_fabric *fabric;
+ struct sk_buff *msg;
+ u32 fabric_id;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ if (GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_FABRIC_ID))
+ return -EINVAL;
+
+ fabric_id = nla_get_u32(info->attrs[DRM_FABRIC_A_FABRIC_ID]);
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
+ &drm_fabric_nl_family, 0,
+ DRM_FABRIC_CMD_FABRIC_GET);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ fabric = drm_fabric_find_by_id(fabric_id);
+ if (!fabric) {
+ nlmsg_free(msg);
+ return -ENOENT;
+ }
+
+ ret = drm_fabric_fill_fabric(msg, fabric);
+ if (ret) {
+ nlmsg_free(msg);
+ return ret;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ drm_fabric_base_seq)) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+ }
+
+ genlmsg_end(msg, hdr);
+ return genlmsg_reply(msg, info);
+}
+
+int drm_fabric_nl_fabric_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb)
+{
+ struct drm_fabric_dump_ctx *ctx = drm_fabric_dump_context(cb);
+ struct drm_fabric *fabric;
+ unsigned long fidx;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(genl_info_dump(cb)));
+ if (ret)
+ return ret;
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ cb->seq = drm_fabric_base_seq;
+ xa_for_each_start(&drm_fabric_xa, fidx, fabric, ctx->idx) {
+ hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid,
+ cb->nlh->nlmsg_seq,
+ &drm_fabric_nl_family, NLM_F_MULTI,
+ DRM_FABRIC_CMD_FABRIC_GET);
+ if (!hdr) {
+ ret = -EMSGSIZE;
+ break;
+ }
+ genl_dump_check_consistent(cb, hdr);
+
+ if (nla_put_u32(skb, DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ drm_fabric_base_seq)) {
+ genlmsg_cancel(skb, hdr);
+ ret = -EMSGSIZE;
+ break;
+ }
+
+ ret = drm_fabric_fill_fabric(skb, fabric);
+ if (ret) {
+ genlmsg_cancel(skb, hdr);
+ break;
+ }
+
+ genlmsg_end(skb, hdr);
+ }
+ }
+
+ if (ret == -EMSGSIZE) {
+ ctx->idx = fidx;
+ return skb->len;
+ }
+ return ret;
+}
+
+/* Identified by core-assigned id, or by bus + dev name. */
+static struct drm_fabric_endpoint *
+drm_fabric_resolve_endpoint(struct genl_info *info)
+{
+ struct nlattr **attrs = info->attrs;
+ const char *devname = NULL;
+ const char *busname = NULL;
+ struct drm_fabric_endpoint *ep;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ if (attrs[DRM_FABRIC_A_DEV_NAME])
+ devname = nla_data(attrs[DRM_FABRIC_A_DEV_NAME]);
+ if (attrs[DRM_FABRIC_A_BUS_NAME])
+ busname = nla_data(attrs[DRM_FABRIC_A_BUS_NAME]);
+
+ if (attrs[DRM_FABRIC_A_ENDPOINT_ID]) {
+ u32 ep_id = nla_get_u32(attrs[DRM_FABRIC_A_ENDPOINT_ID]);
+
+ ep = drm_fabric_endpoint_find_by_id(ep_id);
+ if (!ep)
+ return ERR_PTR(-ENOENT);
+
+ if (devname && strcmp(dev_name(ep->parent), devname))
+ return ERR_PTR(-EINVAL);
+ if (busname && strcmp(dev_bus_name(ep->parent), busname))
+ return ERR_PTR(-EINVAL);
+
+ return ep;
+ }
+
+ if (!devname)
+ return ERR_PTR(-EINVAL);
+
+ ep = drm_fabric_endpoint_find_by_dev_name(devname, busname);
+ if (IS_ERR(ep))
+ return ep;
+ if (!ep)
+ return ERR_PTR(-ENOENT);
+
+ return ep;
+}
+
+int drm_fabric_nl_endpoint_get_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct drm_fabric_endpoint *ep;
+ struct sk_buff *msg;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
+ &drm_fabric_nl_family, 0,
+ DRM_FABRIC_CMD_ENDPOINT_GET);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ ep = drm_fabric_resolve_endpoint(info);
+ if (IS_ERR(ep)) {
+ nlmsg_free(msg);
+ return PTR_ERR(ep);
+ }
+
+ ret = drm_fabric_fill_endpoint(msg, ep);
+ if (ret) {
+ nlmsg_free(msg);
+ return ret;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ drm_fabric_base_seq)) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+ }
+
+ genlmsg_end(msg, hdr);
+ return genlmsg_reply(msg, info);
+}
+
+int drm_fabric_nl_endpoint_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb)
+{
+ const struct genl_info *info = genl_info_dump(cb);
+ struct drm_fabric_dump_ctx *ctx = drm_fabric_dump_context(cb);
+ u32 filter_fabric_id = 0;
+ bool has_filter = false;
+ struct drm_fabric_endpoint *ep;
+ unsigned long eidx;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ if (info->attrs[DRM_FABRIC_A_FABRIC_ID]) {
+ filter_fabric_id =
+ nla_get_u32(info->attrs[DRM_FABRIC_A_FABRIC_ID]);
+ has_filter = true;
+ }
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ cb->seq = drm_fabric_base_seq;
+ xa_for_each_start(&drm_fabric_ep_xa, eidx, ep, ctx->idx) {
+ if (has_filter &&
+ drm_fabric_endpoint_fabric_id(ep) != filter_fabric_id)
+ continue;
+
+ hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid,
+ cb->nlh->nlmsg_seq,
+ &drm_fabric_nl_family, NLM_F_MULTI,
+ DRM_FABRIC_CMD_ENDPOINT_GET);
+ if (!hdr) {
+ ret = -EMSGSIZE;
+ break;
+ }
+ genl_dump_check_consistent(cb, hdr);
+
+ if (nla_put_u32(skb, DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ drm_fabric_base_seq)) {
+ genlmsg_cancel(skb, hdr);
+ ret = -EMSGSIZE;
+ break;
+ }
+
+ ret = drm_fabric_fill_endpoint(skb, ep);
+ if (ret) {
+ genlmsg_cancel(skb, hdr);
+ break;
+ }
+
+ genlmsg_end(skb, hdr);
+ }
+ }
+
+ if (ret == -EMSGSIZE) {
+ ctx->idx = eidx;
+ return skb->len;
+ }
+ return ret;
+}
+
+static int drm_fabric_port_key(struct genl_info *info, u32 *ep_id, u32 *port_idx)
+{
+ if (GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_ENDPOINT_ID) ||
+ GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_PORT_INDEX))
+ return -EINVAL;
+
+ *ep_id = nla_get_u32(info->attrs[DRM_FABRIC_A_ENDPOINT_ID]);
+ *port_idx = nla_get_u32(info->attrs[DRM_FABRIC_A_PORT_INDEX]);
+ return 0;
+}
+
+int drm_fabric_nl_port_get_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct drm_fabric_port *port;
+ struct sk_buff *msg;
+ u32 ep_id, port_idx;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ ret = drm_fabric_port_key(info, &ep_id, &port_idx);
+ if (ret)
+ return ret;
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
+ &drm_fabric_nl_family, 0,
+ DRM_FABRIC_CMD_PORT_GET);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ port = drm_fabric_port_find(ep_id, port_idx);
+ if (!port) {
+ nlmsg_free(msg);
+ return -ENOENT;
+ }
+
+ ret = drm_fabric_fill_port(msg, port);
+ if (ret) {
+ nlmsg_free(msg);
+ return ret;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ drm_fabric_base_seq)) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+ }
+
+ genlmsg_end(msg, hdr);
+ return genlmsg_reply(msg, info);
+}
+
+static int
+drm_fabric_dump_endpoint_ports(struct sk_buff *skb,
+ struct netlink_callback *cb,
+ struct drm_fabric_endpoint *ep, u8 cmd,
+ int (*fill)(struct sk_buff *,
+ struct drm_fabric_port *),
+ unsigned long *s_port_idx)
+{
+ struct drm_fabric_port *port;
+ unsigned long pidx;
+ void *hdr;
+ int ret = 0;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each_start(&ep->ports, pidx, port, *s_port_idx) {
+ hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid,
+ cb->nlh->nlmsg_seq, &drm_fabric_nl_family,
+ NLM_F_MULTI, cmd);
+ if (!hdr) {
+ ret = -EMSGSIZE;
+ break;
+ }
+ genl_dump_check_consistent(cb, hdr);
+
+ /* PORT_STATS_GET replies carry no topology-generation. */
+ if (cmd == DRM_FABRIC_CMD_PORT_GET &&
+ nla_put_u32(skb, DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ drm_fabric_base_seq)) {
+ genlmsg_cancel(skb, hdr);
+ ret = -EMSGSIZE;
+ break;
+ }
+
+ ret = fill(skb, port);
+ if (ret) {
+ genlmsg_cancel(skb, hdr);
+ break;
+ }
+
+ genlmsg_end(skb, hdr);
+ }
+
+ /* Only a partially emitted endpoint keeps its port cursor. */
+ *s_port_idx = (ret == -EMSGSIZE) ? pidx : 0;
+ return ret;
+}
+
+static int drm_fabric_port_dump(struct sk_buff *skb,
+ struct netlink_callback *cb,
+ int cmd,
+ int (*fill)(struct sk_buff *, struct drm_fabric_port *))
+{
+ const struct genl_info *info = genl_info_dump(cb);
+ struct drm_fabric_port_dump_ctx *ctx = drm_fabric_port_dump_context(cb);
+ u32 filter_ep_id = 0;
+ bool has_ep_filter = false;
+ struct drm_fabric_endpoint *ep;
+ unsigned long eidx;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ if (info->attrs[DRM_FABRIC_A_ENDPOINT_ID]) {
+ filter_ep_id = nla_get_u32(info->attrs[DRM_FABRIC_A_ENDPOINT_ID]);
+ has_ep_filter = true;
+ }
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ bool first = true;
+
+ cb->seq = drm_fabric_base_seq;
+ xa_for_each_start(&drm_fabric_ep_xa, eidx, ep, ctx->ep_idx) {
+ /*
+ * port_idx belongs to ep_idx; honor it only if
+ * that endpoint still resolves.
+ */
+ if (first) {
+ if (eidx != ctx->ep_idx)
+ ctx->port_idx = 0;
+ first = false;
+ }
+
+ if (has_ep_filter && ep->id != filter_ep_id) {
+ ctx->port_idx = 0;
+ continue;
+ }
+
+ ret = drm_fabric_dump_endpoint_ports(skb, cb, ep,
+ cmd,
+ fill,
+ &ctx->port_idx);
+ if (ret)
+ break;
+ }
+ }
+
+ if (ret == -EMSGSIZE) {
+ ctx->ep_idx = eidx;
+ return skb->len;
+ }
+ return ret;
+}
+
+int drm_fabric_nl_port_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb)
+{
+ return drm_fabric_port_dump(skb, cb, DRM_FABRIC_CMD_PORT_GET,
+ drm_fabric_fill_port);
+}
+
+/* Advance the cursor to the next port at or after it, or return NULL. */
+static struct drm_fabric_port *
+drm_fabric_stats_dump_next(unsigned long *ep_idx, unsigned long *port_idx,
+ bool has_ep_filter, u32 filter_ep_id)
+{
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+ unsigned long eidx;
+ bool first = true;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ xa_for_each_start(&drm_fabric_ep_xa, eidx, ep, *ep_idx) {
+ if (first) {
+ if (eidx != *ep_idx)
+ *port_idx = 0;
+ first = false;
+ }
+
+ if (has_ep_filter && ep->id != filter_ep_id) {
+ *port_idx = 0;
+ continue;
+ }
+
+ port = xa_find(&ep->ports, port_idx, ULONG_MAX, XA_PRESENT);
+ if (port) {
+ *ep_idx = eidx;
+ return port;
+ }
+
+ *port_idx = 0;
+ }
+
+ /* Mark the endpoint cursor exhausted. */
+ *ep_idx = ULONG_MAX;
+ return NULL;
+}
+
+/*
+ * Port indices are provider-chosen u32s, so port_index may be U32_MAX;
+ * adding one there would wrap to 0 and re-dump the endpoint forever.
+ */
+static void
+drm_fabric_stats_cursor_advance(unsigned long *ep_idx, unsigned long *port_idx,
+ u32 port_index)
+{
+ if (port_index == U32_MAX) {
+ (*ep_idx)++;
+ *port_idx = 0;
+ } else {
+ *port_idx = port_index + 1;
+ }
+}
+
+/* Advance past an unsupported port so a resumed dump cannot retry it. */
+static void
+drm_fabric_stats_dump_skip(struct sk_buff *skb, void *hdr,
+ struct drm_fabric_port_dump_ctx *ctx,
+ struct drm_fabric_port *port,
+ u32 port_index)
+{
+ genlmsg_cancel(skb, hdr);
+ drm_fabric_port_put(port);
+ drm_fabric_stats_cursor_advance(&ctx->ep_idx, &ctx->port_idx,
+ port_index);
+}
+
+int drm_fabric_nl_port_stats_get_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ const struct drm_fabric_ops *ops;
+ struct drm_fabric_port_stats stats = {};
+ struct drm_fabric_port *port;
+ struct sk_buff *msg;
+ u32 ep_id, port_idx;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ ret = drm_fabric_port_key(info, &ep_id, &port_idx);
+ if (ret)
+ return ret;
+
+ /*
+ * Pins the port through its endpoint across the unlocked provider
+ * callback; endpoint_unregister() waits on this pin, so it cannot be
+ * freed underneath us.
+ */
+ port = drm_fabric_port_find_get(ep_id, port_idx);
+ if (IS_ERR(port))
+ return PTR_ERR(port);
+
+ ops = port->endpoint->ops;
+ if (!ops || !ops->port_stats_get) {
+ ret = -EOPNOTSUPP;
+ goto out_put;
+ }
+
+ lockdep_assert_not_held(&drm_fabric_lock);
+ ret = ops->port_stats_get(port, &stats);
+ if (ret)
+ goto out_put;
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg) {
+ ret = -ENOMEM;
+ goto out_put;
+ }
+
+ hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
+ &drm_fabric_nl_family, 0,
+ DRM_FABRIC_CMD_PORT_STATS_GET);
+ if (!hdr) {
+ ret = -EMSGSIZE;
+ goto out_free;
+ }
+
+ ret = drm_fabric_fill_port_stats(msg, port, &stats);
+ if (ret)
+ goto out_free;
+
+ genlmsg_end(msg, hdr);
+ drm_fabric_port_put(port);
+ return genlmsg_reply(msg, info);
+
+out_free:
+ nlmsg_free(msg);
+out_put:
+ drm_fabric_port_put(port);
+ return ret;
+}
+
+/*
+ * Statistics callbacks may sleep. Pin the endpoint, drop the lock, then
+ * resample cb->seq so a mutation in that window sets NLM_F_DUMP_INTR.
+ */
+int drm_fabric_nl_port_stats_get_dumpit(struct sk_buff *skb,
+ struct netlink_callback *cb)
+{
+ const struct genl_info *info = genl_info_dump(cb);
+ struct drm_fabric_port_dump_ctx *ctx = drm_fabric_port_dump_context(cb);
+ u32 filter_ep_id = 0;
+ bool has_ep_filter = false;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ if (info->attrs[DRM_FABRIC_A_ENDPOINT_ID]) {
+ filter_ep_id = nla_get_u32(info->attrs[DRM_FABRIC_A_ENDPOINT_ID]);
+ has_ep_filter = true;
+ }
+
+ for (;;) {
+ const struct drm_fabric_ops *ops;
+ struct drm_fabric_port_stats stats = {};
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+ u32 port_index;
+ void *hdr;
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ /*
+ * Sampled before the cursor check so even a batch
+ * that emits nothing can report a late mutation.
+ */
+ cb->seq = drm_fabric_base_seq;
+
+ port = drm_fabric_stats_dump_next(&ctx->ep_idx,
+ &ctx->port_idx,
+ has_ep_filter,
+ filter_ep_id);
+ /*
+ * Exhausted: 0 terminates the dump with NLMSG_DONE
+ * instead of re-running the parked cursor.
+ */
+ if (!port)
+ return 0;
+
+ ep = port->endpoint;
+ ops = ep->ops;
+ port_index = port->index;
+ drm_fabric_endpoint_get(ep);
+ }
+
+ /* Reserve the reply before invoking the sleeping provider callback. */
+ hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid,
+ cb->nlh->nlmsg_seq, &drm_fabric_nl_family,
+ NLM_F_MULTI, DRM_FABRIC_CMD_PORT_STATS_GET);
+ if (!hdr) {
+ drm_fabric_port_put(port);
+ return skb->len;
+ }
+
+ if (!ops || !ops->port_stats_get) {
+ drm_fabric_stats_dump_skip(skb, hdr, ctx, port, port_index);
+ continue;
+ }
+
+ lockdep_assert_not_held(&drm_fabric_lock);
+ ret = ops->port_stats_get(port, &stats);
+ if (ret == -EOPNOTSUPP) {
+ drm_fabric_stats_dump_skip(skb, hdr, ctx, port, port_index);
+ continue;
+ }
+ if (ret) {
+ genlmsg_cancel(skb, hdr);
+ drm_fabric_port_put(port);
+ break;
+ }
+
+ genl_dump_check_consistent(cb, hdr);
+
+ if (drm_fabric_fill_port_stats(skb, port, &stats)) {
+ genlmsg_cancel(skb, hdr);
+ drm_fabric_port_put(port);
+ return skb->len;
+ }
+
+ genlmsg_end(skb, hdr);
+ drm_fabric_port_put(port);
+
+ drm_fabric_stats_cursor_advance(&ctx->ep_idx, &ctx->port_idx,
+ port_index);
+ }
+
+ return ret;
+}
+
+void drm_fabric_emit_port_change(struct drm_fabric_port *port, u32 generation)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return;
+
+ hdr = genlmsg_put(msg, 0, 0, &drm_fabric_nl_family, 0, DRM_FABRIC_CMD_PORT_CHANGE_NTF);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION, generation) ||
+ drm_fabric_fill_port(msg, port)) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ genlmsg_end(msg, hdr);
+ genlmsg_multicast(&drm_fabric_nl_family, msg, 0,
+ DRM_FABRIC_NLGRP_MONITOR, GFP_KERNEL);
+}
+
+static void drm_fabric_port_peer_event_send(enum drm_fabric_cmd cmd,
+ struct drm_fabric_port *port,
+ const struct drm_fabric_peer *peer,
+ u32 generation)
+{
+ struct sk_buff *msg;
+ void *hdr;
+ struct nlattr *nest;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return;
+
+ hdr = genlmsg_put(msg, 0, 0, &drm_fabric_nl_family, 0, cmd);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION, generation) ||
+ nla_put_u32(msg, DRM_FABRIC_A_ENDPOINT_ID, port->endpoint->id) ||
+ nla_put_u32(msg, DRM_FABRIC_A_PORT_INDEX, port->index)) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ nest = nla_nest_start(msg, DRM_FABRIC_A_PEER);
+ if (!nest) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ if (nla_put_u64_64bit(msg, DRM_FABRIC_A_PEER_ATTRS_PEER_ID,
+ peer->peer_id, DRM_FABRIC_A_PEER_ATTRS_PAD) ||
+ nla_put_u32(msg, DRM_FABRIC_A_PEER_ATTRS_TYPE, peer->peer_type) ||
+ nla_put_u32(msg, DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX, peer->port_index)) {
+ nla_nest_cancel(msg, nest);
+ nlmsg_free(msg);
+ return;
+ }
+
+ nla_nest_end(msg, nest);
+
+ genlmsg_end(msg, hdr);
+ genlmsg_multicast(&drm_fabric_nl_family, msg, 0,
+ DRM_FABRIC_NLGRP_MONITOR, GFP_KERNEL);
+}
+
+void drm_fabric_emit_port_peer_create(struct drm_fabric_port *port,
+ const struct drm_fabric_peer *peer,
+ u32 generation)
+{
+ drm_fabric_port_peer_event_send(DRM_FABRIC_CMD_PORT_PEER_CREATE_NTF,
+ port, peer, generation);
+}
+
+void drm_fabric_emit_port_peer_delete(struct drm_fabric_port *port,
+ const struct drm_fabric_peer *peer,
+ u32 generation)
+{
+ drm_fabric_port_peer_event_send(DRM_FABRIC_CMD_PORT_PEER_DELETE_NTF,
+ port, peer, generation);
+}
+
+static void drm_fabric_endpoint_event_send(enum drm_fabric_cmd cmd,
+ struct drm_fabric_endpoint *ep,
+ u32 generation)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return;
+
+ hdr = genlmsg_put(msg, 0, 0, &drm_fabric_nl_family, 0, cmd);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION, generation) ||
+ drm_fabric_fill_endpoint(msg, ep)) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ genlmsg_end(msg, hdr);
+ genlmsg_multicast(&drm_fabric_nl_family, msg, 0,
+ DRM_FABRIC_NLGRP_MONITOR, GFP_KERNEL);
+}
+
+void drm_fabric_emit_endpoint_create(struct drm_fabric_endpoint *ep, u32 generation)
+{
+ drm_fabric_endpoint_event_send(DRM_FABRIC_CMD_ENDPOINT_CREATE_NTF, ep,
+ generation);
+}
+
+void drm_fabric_emit_endpoint_delete(struct drm_fabric_endpoint *ep, u32 generation)
+{
+ drm_fabric_endpoint_event_send(DRM_FABRIC_CMD_ENDPOINT_DELETE_NTF, ep,
+ generation);
+}
+
+static void drm_fabric_fabric_event_send(enum drm_fabric_cmd cmd,
+ struct drm_fabric *fabric,
+ u32 generation)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ lockdep_assert_held(&drm_fabric_lock);
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return;
+
+ hdr = genlmsg_put(msg, 0, 0, &drm_fabric_nl_family, 0, cmd);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ if (nla_put_u32(msg, DRM_FABRIC_A_TOPOLOGY_GENERATION, generation) ||
+ drm_fabric_fill_fabric(msg, fabric)) {
+ nlmsg_free(msg);
+ return;
+ }
+
+ genlmsg_end(msg, hdr);
+ genlmsg_multicast(&drm_fabric_nl_family, msg, 0,
+ DRM_FABRIC_NLGRP_MONITOR, GFP_KERNEL);
+}
+
+void drm_fabric_emit_fabric_create(struct drm_fabric *fabric, u32 generation)
+{
+ drm_fabric_fabric_event_send(DRM_FABRIC_CMD_FABRIC_CREATE_NTF, fabric,
+ generation);
+}
+
+void drm_fabric_emit_fabric_delete(struct drm_fabric *fabric, u32 generation)
+{
+ drm_fabric_fabric_event_send(DRM_FABRIC_CMD_FABRIC_DELETE_NTF, fabric,
+ generation);
+}
+
+int drm_fabric_netlink_register(void)
+{
+ return genl_register_family(&drm_fabric_nl_family);
+}
+
+void drm_fabric_netlink_unregister(void)
+{
+ genl_unregister_family(&drm_fabric_nl_family);
+}
diff --git a/drivers/gpu/drm/fabric/drm_fabric_nl.c b/drivers/gpu/drm/fabric/drm_fabric_nl.c
new file mode 100644
index 000000000000..032548405146
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_nl.c
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+/* Do not edit directly, auto-generated from: */
+/* Documentation/netlink/specs/drm_fabric.yaml */
+/* YNL-GEN kernel source */
+/* To regenerate run: tools/net/ynl/ynl-regen.sh */
+
+#include <net/netlink.h>
+#include <net/genetlink.h>
+
+#include "drm_fabric_nl.h"
+
+#include <uapi/drm/drm_fabric.h>
+
+/* DRM_FABRIC_CMD_FABRIC_GET - do */
+static const struct nla_policy drm_fabric_fabric_get_nl_policy[DRM_FABRIC_A_FABRIC_ID + 1] = {
+ [DRM_FABRIC_A_FABRIC_ID] = { .type = NLA_U32, },
+};
+
+/* DRM_FABRIC_CMD_ENDPOINT_GET - do */
+static const struct nla_policy drm_fabric_endpoint_get_do_nl_policy[DRM_FABRIC_A_BUS_NAME + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_DEV_NAME] = { .type = NLA_NUL_STRING, },
+ [DRM_FABRIC_A_BUS_NAME] = { .type = NLA_NUL_STRING, },
+};
+
+/* DRM_FABRIC_CMD_ENDPOINT_GET - dump */
+static const struct nla_policy drm_fabric_endpoint_get_dump_nl_policy[DRM_FABRIC_A_FABRIC_ID + 1] = {
+ [DRM_FABRIC_A_FABRIC_ID] = { .type = NLA_U32, },
+};
+
+/* DRM_FABRIC_CMD_PORT_GET - do */
+static const struct nla_policy drm_fabric_port_get_do_nl_policy[DRM_FABRIC_A_PORT_INDEX + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_PORT_INDEX] = { .type = NLA_U32, },
+};
+
+/* DRM_FABRIC_CMD_PORT_GET - dump */
+static const struct nla_policy drm_fabric_port_get_dump_nl_policy[DRM_FABRIC_A_ENDPOINT_ID + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+};
+
+/* DRM_FABRIC_CMD_PORT_STATS_GET - do */
+static const struct nla_policy drm_fabric_port_stats_get_do_nl_policy[DRM_FABRIC_A_PORT_INDEX + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_PORT_INDEX] = { .type = NLA_U32, },
+};
+
+/* DRM_FABRIC_CMD_PORT_STATS_GET - dump */
+static const struct nla_policy drm_fabric_port_stats_get_dump_nl_policy[DRM_FABRIC_A_ENDPOINT_ID + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+};
+
+/* Ops table for drm_fabric */
+static const struct genl_split_ops drm_fabric_nl_ops[] = {
+ {
+ .cmd = DRM_FABRIC_CMD_FABRIC_GET,
+ .doit = drm_fabric_nl_fabric_get_doit,
+ .policy = drm_fabric_fabric_get_nl_policy,
+ .maxattr = DRM_FABRIC_A_FABRIC_ID,
+ .flags = GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_FABRIC_GET,
+ .dumpit = drm_fabric_nl_fabric_get_dumpit,
+ .flags = GENL_CMD_CAP_DUMP,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_ENDPOINT_GET,
+ .doit = drm_fabric_nl_endpoint_get_doit,
+ .policy = drm_fabric_endpoint_get_do_nl_policy,
+ .maxattr = DRM_FABRIC_A_BUS_NAME,
+ .flags = GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_ENDPOINT_GET,
+ .dumpit = drm_fabric_nl_endpoint_get_dumpit,
+ .policy = drm_fabric_endpoint_get_dump_nl_policy,
+ .maxattr = DRM_FABRIC_A_FABRIC_ID,
+ .flags = GENL_CMD_CAP_DUMP,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_GET,
+ .doit = drm_fabric_nl_port_get_doit,
+ .policy = drm_fabric_port_get_do_nl_policy,
+ .maxattr = DRM_FABRIC_A_PORT_INDEX,
+ .flags = GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_GET,
+ .dumpit = drm_fabric_nl_port_get_dumpit,
+ .policy = drm_fabric_port_get_dump_nl_policy,
+ .maxattr = DRM_FABRIC_A_ENDPOINT_ID,
+ .flags = GENL_CMD_CAP_DUMP,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_STATS_GET,
+ .doit = drm_fabric_nl_port_stats_get_doit,
+ .policy = drm_fabric_port_stats_get_do_nl_policy,
+ .maxattr = DRM_FABRIC_A_PORT_INDEX,
+ .flags = GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_STATS_GET,
+ .dumpit = drm_fabric_nl_port_stats_get_dumpit,
+ .policy = drm_fabric_port_stats_get_dump_nl_policy,
+ .maxattr = DRM_FABRIC_A_ENDPOINT_ID,
+ .flags = GENL_CMD_CAP_DUMP,
+ },
+};
+
+static const struct genl_multicast_group drm_fabric_nl_mcgrps[] = {
+ [DRM_FABRIC_NLGRP_MONITOR] = { "monitor", },
+};
+
+struct genl_family drm_fabric_nl_family __ro_after_init = {
+ .name = DRM_FABRIC_FAMILY_NAME,
+ .version = DRM_FABRIC_FAMILY_VERSION,
+ .netnsok = true,
+ .parallel_ops = true,
+ .module = THIS_MODULE,
+ .split_ops = drm_fabric_nl_ops,
+ .n_split_ops = ARRAY_SIZE(drm_fabric_nl_ops),
+ .mcgrps = drm_fabric_nl_mcgrps,
+ .n_mcgrps = ARRAY_SIZE(drm_fabric_nl_mcgrps),
+};
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 04/12] drm/fabric: add read-only synthetic provider
2026-08-24 8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
` (2 preceding siblings ...)
2026-08-24 8:09 ` [RFC PATCH 03/12] drm/fabric: implement query netlink operations Konstantin Sinyuk
@ 2026-08-24 8:09 ` Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 05/12] drm/fabric: add object-model KUnit tests Konstantin Sinyuk
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
Add drm_fabric_sim (fabricsim), a software-only provider modeled on
netdevsim. It registers deterministic linear, mesh and switch-facing
topologies through the same provider API a hardware driver uses, so the
object model and query uAPI can be exercised without accelerator hardware.
The switch shape uses opaque peers that do not resolve to registered
endpoints.
CONFIG_DRM_FABRIC_SIM builds the provider as drm-fabric-sim.ko. Module
parameters select the topology shape and initial endpoint and port counts.
With topology=linear num_endpoints=2, an endpoint is queried using the
in-tree YNL tool:
$ ./tools/net/ynl/pyynl/cli.py \
--spec Documentation/netlink/specs/drm_fabric.yaml \
--do endpoint-get --json '{"endpoint-id": 1}'
{'endpoint': {'bus-name': 'platform',
'dev-name': 'fabricsim.1',
'endpoint-id': 1,
'fabric-ep-id': 257,
'fabric-id': 1,
'name': 'sim-ep1'},
'topology-generation': 18}
The reply separates the core-assigned endpoint-id from the provider-defined
fabric-ep-id and carries the topology-generation token.
Report maximum link capabilities at registration, operational state through
drm_fabric_port_set_oper() and optional port statistics through the
port_stats_get callback.
Test-only debugfs controls stimulate provider behavior for the selftests.
They are not uAPI; observable results are reported through the drm-fabric
Generic Netlink family.
Signed-off-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
Documentation/gpu/drm-fabric.rst | 46 +-
drivers/gpu/drm/fabric/Kconfig | 10 +
drivers/gpu/drm/fabric/Makefile | 3 +
drivers/gpu/drm/fabric/drm_fabric_sim.c | 978 ++++++++++++++++++++++++
4 files changed, 1034 insertions(+), 3 deletions(-)
create mode 100644 drivers/gpu/drm/fabric/drm_fabric_sim.c
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index a8b33d6cd618..8bd3633d41be 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -291,10 +291,16 @@ Query the topology with the in-tree YNL tool, pointing it at the spec:
--spec Documentation/netlink/specs/drm_fabric.yaml \
--dump fabric-get
-Replies follow the shapes described above; a provider must be registered for
-the topology to be non-empty.
+Against drm_fabric_sim loaded with ``topology=linear num_endpoints=2``
+(both non-default, for a minimal example), this returns:
-List the endpoints of a fabric:
+.. code-block:: text
+
+ [{'fabric': {'fabric-id': 1, 'instance-id': 2156317438,
+ 'name': 'fabricsim', 'type': 'synthetic'},
+ 'topology-generation': 18}]
+
+List the endpoints of that fabric:
.. code-block:: bash
@@ -302,6 +308,15 @@ List the endpoints of a fabric:
--spec Documentation/netlink/specs/drm_fabric.yaml \
--dump endpoint-get --json '{"fabric-id": 1}'
+.. code-block:: text
+
+ [{'endpoint': {'bus-name': 'platform', 'dev-name': 'fabricsim.0',
+ 'endpoint-id': 0, 'fabric-ep-id': 256, 'fabric-id': 1,
+ 'name': 'sim-ep0'}, 'topology-generation': 18},
+ {'endpoint': {'bus-name': 'platform', 'dev-name': 'fabricsim.1',
+ 'endpoint-id': 1, 'fabric-ep-id': 257, 'fabric-id': 1,
+ 'name': 'sim-ep1'}, 'topology-generation': 18}]
+
Query a single port:
.. code-block:: bash
@@ -310,4 +325,29 @@ Query a single port:
--spec Documentation/netlink/specs/drm_fabric.yaml \
--do port-get --json '{"endpoint-id": 1, "port-index": 0}'
+.. code-block:: text
+
+ {'port': {'endpoint-id': 1, 'max-lane-count': 4,
+ 'max-lane-signaling-rate-mbps': 200000, 'oper-state': 'active',
+ 'peer': {'peer-id': 256, 'port-index': 0, 'type': 'accel'},
+ 'port-index': 0},
+ 'topology-generation': 18}
+
The family name on the wire is ``drm-fabric``.
+
+Synthetic provider
+==================
+
+``CONFIG_DRM_FABRIC_SIM`` builds ``drm-fabric-sim.ko``, a software-only provider
+modeled on netdevsim (Documentation/networking/devlink/netdevsim.rst) that drives
+the object model and uAPI without real hardware. Module parameters select a
+linear, mesh or switch-shaped topology and bound the number of endpoints and
+ports. The switch shape links every endpoint to an opaque switch peer
+(``peer-type = switch``) whose id does not resolve to an endpoint, exercising the
+directed half-edge model without a first-class switch object.
+
+Its debugfs knobs stimulate synthetic counter activity, operational-state changes
+and runtime endpoint add/remove. These files are unstable test controls and are
+not part of the uAPI; the stable, reviewed interface is the YAML-described
+Generic Netlink family. Tests mutate simulator state through debugfs and observe
+the result over Generic Netlink.
diff --git a/drivers/gpu/drm/fabric/Kconfig b/drivers/gpu/drm/fabric/Kconfig
index 7e9c569fd258..87115356baca 100644
--- a/drivers/gpu/drm/fabric/Kconfig
+++ b/drivers/gpu/drm/fabric/Kconfig
@@ -12,3 +12,13 @@ config DRM_FABRIC
called drm-fabric.
If in doubt, say N.
+
+config DRM_FABRIC_SIM
+ tristate "DRM fabric synthetic provider (test-only)"
+ depends on DRM_FABRIC
+ help
+ Synthetic provider for testing drm_fabric topology and ABI.
+ Provides debugfs-only hooks for synthetic activity generation,
+ error injection, and port-state transitions. These hooks are
+ NOT part of the drm_fabric uAPI and are used only by selftests.
+ Does not model UALink protocol traffic or memory semantics.
diff --git a/drivers/gpu/drm/fabric/Makefile b/drivers/gpu/drm/fabric/Makefile
index cf9d9d6be3d3..bc0a6c742164 100644
--- a/drivers/gpu/drm/fabric/Makefile
+++ b/drivers/gpu/drm/fabric/Makefile
@@ -2,3 +2,6 @@
obj-$(CONFIG_DRM_FABRIC) += drm-fabric.o
drm-fabric-y := drm_fabric.o drm_fabric_netlink.o drm_fabric_nl.o
+
+obj-$(CONFIG_DRM_FABRIC_SIM) += drm-fabric-sim.o
+drm-fabric-sim-y := drm_fabric_sim.o
diff --git a/drivers/gpu/drm/fabric/drm_fabric_sim.c b/drivers/gpu/drm/fabric/drm_fabric_sim.c
new file mode 100644
index 000000000000..7d489c6894bf
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_sim.c
@@ -0,0 +1,978 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#include <linux/cleanup.h>
+#include <linux/debugfs.h>
+#include <linux/err.h>
+#include <linux/kstrtox.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timer.h>
+
+#include <drm/drm_fabric.h>
+#include <uapi/drm/drm_fabric.h>
+
+/**
+ * DOC: fabricsim
+ *
+ * In-kernel synthetic drm_fabric provider for review and CI. It registers a
+ * linear, mesh or switch topology and drives the selftests through debugfs:
+ * synthetic counters, port-state transitions, endpoint hotplug, a bulk
+ * population for dump-resume testing, and fault injection. The debugfs hooks
+ * are not uAPI; userspace observes the resulting state through the Generic
+ * Netlink ABI.
+ */
+
+static char *topology = "mesh";
+module_param(topology, charp, 0444);
+MODULE_PARM_DESC(topology, "Topology shape: linear, mesh, switch (default: mesh)");
+
+static int num_endpoints = 4;
+module_param(num_endpoints, int, 0444);
+MODULE_PARM_DESC(num_endpoints, "Number of endpoints (2-8, default: 4)");
+
+static int ports_per_ep = 4;
+module_param(ports_per_ep, int, 0444);
+MODULE_PARM_DESC(ports_per_ep, "Ports per endpoint (1-16, default: 4)");
+
+struct fabricsim_port_priv {
+ struct drm_fabric_port *port;
+
+ /* Bumped lockless from the timer and debugfs; untorn on 32-bit. */
+ atomic64_t read_bytes;
+ atomic64_t write_bytes;
+ atomic64_t link_down_count;
+ atomic64_t retrain_count;
+
+ /* Blocks enable while disable drains a pending timer re-arm. */
+ struct mutex activity_lock;
+ bool activity_enabled;
+ struct timer_list activity_timer;
+ u32 read_rate; /* bytes per timer tick (FABRICSIM_TICK_MS) */
+ u32 write_rate; /* bytes per timer tick (FABRICSIM_TICK_MS) */
+
+ /*
+ * Test-only: returns -stats_errno, with values above MAX_ERRNO mapped
+ * to -EIO.
+ */
+ u32 stats_errno;
+};
+
+struct fabricsim_ep_priv {
+ struct drm_fabric_endpoint *ep;
+ struct platform_device *pdev; /* backing device for dev-name/bus-name */
+ struct fabricsim_port_priv *ports;
+ int num_ports;
+ int slot;
+ bool runtime;
+ struct dentry *dbg_dir;
+};
+
+#define FABRICSIM_MAX_EPS 512
+/* Initial generated topology only; runtime hotplug uses FABRICSIM_MAX_EPS. */
+#define FABRICSIM_MAX_INIT_EPS 8
+
+static struct drm_fabric *fabricsim_fabric;
+static struct fabricsim_ep_priv *fabricsim_slots[FABRICSIM_MAX_EPS];
+static int fabricsim_init_eps;
+static bool fabricsim_exiting; /* gate runtime controls during teardown */
+/*
+ * Nests outside drm_fabric_lock and is never taken from the provider ops or the
+ * debugfs port handlers, so the two cannot invert.
+ */
+static DEFINE_MUTEX(fabricsim_lock);
+static struct dentry *fabricsim_debugfs_root;
+
+/* Test-only fault injection (debugfs). Sticky until cleared. */
+static bool fabricsim_fail_register;
+static u32 fabricsim_fail_errno = ENOMEM;
+
+/*
+ * Negative errno an armed fault returns; zero or an out-of-range value
+ * gives -ENOMEM.
+ */
+static int fabricsim_injected_errno(void)
+{
+ u32 e = fabricsim_fail_errno;
+
+ if (e == 0 || e > MAX_ERRNO)
+ return -ENOMEM;
+ return -(int)e;
+}
+
+static int fabricsim_port_stats_get(struct drm_fabric_port *port,
+ struct drm_fabric_port_stats *stats)
+{
+ struct fabricsim_ep_priv *ep_priv = port->endpoint->priv;
+ struct fabricsim_port_priv *pp;
+
+ /*
+ * The endpoint is pinned by the core; missing private state is a
+ * provider bug.
+ */
+ if (!ep_priv)
+ return -ENOENT;
+
+ /*
+ * Port i has index i. Index rather than search for a matching ->port,
+ * which a dump racing registration would not yet see.
+ */
+ if (port->index >= (u32)ep_priv->num_ports)
+ return -ENOENT;
+
+ pp = &ep_priv->ports[port->index];
+
+ /* Dumps skip -EOPNOTSUPP; any other injected error aborts the dump. */
+ if (pp->stats_errno) {
+ u32 e = pp->stats_errno;
+
+ return e <= MAX_ERRNO ? -(int)e : -EIO;
+ }
+
+ stats->read_bytes = atomic64_read(&pp->read_bytes);
+ stats->write_bytes = atomic64_read(&pp->write_bytes);
+ stats->link_down_count = atomic64_read(&pp->link_down_count);
+ stats->retrain_count = atomic64_read(&pp->retrain_count);
+
+ return 0;
+}
+
+static const struct drm_fabric_ops fabricsim_ops = {
+ .port_stats_get = fabricsim_port_stats_get,
+};
+
+#define FABRICSIM_TICK_MS 100
+
+static void fabricsim_activity_tick(struct timer_list *t)
+{
+ struct fabricsim_port_priv *pp =
+ container_of(t, struct fabricsim_port_priv, activity_timer);
+
+ /*
+ * Paired with WRITE_ONCE() in the enable/disable path, so a stopped
+ * timer stays stopped.
+ */
+ if (!READ_ONCE(pp->activity_enabled))
+ return;
+
+ atomic64_add(READ_ONCE(pp->read_rate), &pp->read_bytes);
+ atomic64_add(READ_ONCE(pp->write_rate), &pp->write_bytes);
+
+ mod_timer(&pp->activity_timer,
+ jiffies + msecs_to_jiffies(FABRICSIM_TICK_MS));
+}
+
+static ssize_t fabricsim_activity_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct fabricsim_port_priv *pp = file->private_data;
+ char kbuf[8];
+ int val;
+
+ if (count >= sizeof(kbuf))
+ return -EINVAL;
+ if (copy_from_user(kbuf, buf, count))
+ return -EFAULT;
+ kbuf[count] = '\0';
+
+ if (kstrtoint(kbuf, 10, &val))
+ return -EINVAL;
+
+ /*
+ * Held across the whole transition, or a concurrent enable could re-arm
+ * mid-drain.
+ */
+ scoped_guard(mutex, &pp->activity_lock) {
+ if (val && !pp->activity_enabled) {
+ WRITE_ONCE(pp->activity_enabled, true);
+ mod_timer(&pp->activity_timer,
+ jiffies + msecs_to_jiffies(FABRICSIM_TICK_MS));
+ } else if (!val && pp->activity_enabled) {
+ WRITE_ONCE(pp->activity_enabled, false);
+ /*
+ * Not timer_shutdown_sync(): a later enable re-arms.
+ * A tick past the enabled check can re-arm after one
+ * timer_delete_sync(), so loop until a drain is clean.
+ */
+ while (timer_delete_sync(&pp->activity_timer))
+ ;
+ }
+ }
+
+ return count;
+}
+
+static ssize_t fabricsim_activity_read(struct file *file,
+ char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct fabricsim_port_priv *pp = file->private_data;
+ char kbuf[4];
+ int len;
+
+ len = scnprintf(kbuf, sizeof(kbuf), "%d\n",
+ READ_ONCE(pp->activity_enabled) ? 1 : 0);
+ return simple_read_from_buffer(buf, count, ppos, kbuf, len);
+}
+
+/* Per-port debugfs files use fabricsim_port_priv as file->private_data. */
+static const struct file_operations fabricsim_activity_fops = {
+ .owner = THIS_MODULE,
+ .open = simple_open,
+ .read = fabricsim_activity_read,
+ .write = fabricsim_activity_write,
+};
+
+static ssize_t fabricsim_inject_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct fabricsim_port_priv *pp = file->private_data;
+ char kbuf[32];
+
+ if (count >= sizeof(kbuf))
+ return -EINVAL;
+ if (copy_from_user(kbuf, buf, count))
+ return -EFAULT;
+ kbuf[count] = '\0';
+ if (count > 0 && kbuf[count - 1] == '\n')
+ kbuf[count - 1] = '\0';
+
+ if (strcmp(kbuf, "link_down") == 0) {
+ atomic64_inc(&pp->link_down_count);
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_INACTIVE);
+ } else if (strcmp(kbuf, "degrade") == 0) {
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_DEGRADED);
+ } else if (strcmp(kbuf, "recover_to_active") == 0) {
+ atomic64_inc(&pp->retrain_count);
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ } else {
+ return -EINVAL;
+ }
+
+ return count;
+}
+
+static const struct file_operations fabricsim_inject_fops = {
+ .owner = THIS_MODULE,
+ .open = simple_open,
+ .write = fabricsim_inject_write,
+};
+
+static ssize_t fabricsim_oper_state_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct fabricsim_port_priv *pp = file->private_data;
+ char kbuf[16];
+
+ if (count >= sizeof(kbuf))
+ return -EINVAL;
+ if (copy_from_user(kbuf, buf, count))
+ return -EFAULT;
+ kbuf[count] = '\0';
+ if (count > 0 && kbuf[count - 1] == '\n')
+ kbuf[count - 1] = '\0';
+
+ if (strcmp(kbuf, "unknown") == 0)
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_UNKNOWN);
+ else if (strcmp(kbuf, "inactive") == 0)
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_INACTIVE);
+ else if (strcmp(kbuf, "active") == 0)
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ else if (strcmp(kbuf, "degraded") == 0)
+ drm_fabric_port_set_oper(pp->port, DRM_FABRIC_PORT_STATE_DEGRADED);
+ else
+ return -EINVAL;
+
+ return count;
+}
+
+static ssize_t fabricsim_oper_state_read(struct file *file,
+ char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct fabricsim_port_priv *pp = file->private_data;
+ const char *state_str;
+ char kbuf[16];
+ int len;
+
+ switch (pp->port->oper_state) {
+ case DRM_FABRIC_PORT_STATE_INACTIVE:
+ state_str = "inactive";
+ break;
+ case DRM_FABRIC_PORT_STATE_ACTIVE:
+ state_str = "active";
+ break;
+ case DRM_FABRIC_PORT_STATE_DEGRADED:
+ state_str = "degraded";
+ break;
+ default:
+ state_str = "unknown";
+ break;
+ }
+
+ len = scnprintf(kbuf, sizeof(kbuf), "%s\n", state_str);
+ return simple_read_from_buffer(buf, count, ppos, kbuf, len);
+}
+
+static const struct file_operations fabricsim_oper_state_fops = {
+ .owner = THIS_MODULE,
+ .open = simple_open,
+ .read = fabricsim_oper_state_read,
+ .write = fabricsim_oper_state_write,
+};
+
+static void fabricsim_link_linear(void)
+{
+ int i;
+ int port_cursor[FABRICSIM_MAX_INIT_EPS] = {0};
+ struct drm_fabric_peer peer;
+
+ /* Linear chain: ep[0]<->ep[1]<->ep[2]<->...<->ep[N-1] */
+ for (i = 0; i < fabricsim_init_eps - 1; i++) {
+ int pa_idx = port_cursor[i]++;
+ int pb_idx = port_cursor[i + 1]++;
+ struct drm_fabric_endpoint *ep_a = fabricsim_slots[i]->ep;
+ struct drm_fabric_endpoint *ep_b = fabricsim_slots[i + 1]->ep;
+ struct drm_fabric_port *pa, *pb;
+
+ /*
+ * Interior nodes consume two ports; stop rather than walk off
+ * an endpoint's port array if it was sized too small.
+ */
+ if (pa_idx >= fabricsim_slots[i]->num_ports ||
+ pb_idx >= fabricsim_slots[i + 1]->num_ports)
+ break;
+
+ pa = fabricsim_slots[i]->ports[pa_idx].port;
+ pb = fabricsim_slots[i + 1]->ports[pb_idx].port;
+
+ /* Peers are directed half-edges, so install both directions. */
+ peer.peer_id = ep_b->fabric_ep_id;
+ peer.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL;
+ peer.port_index = pb->index;
+ drm_fabric_port_set_peer(pa, &peer);
+
+ peer.peer_id = ep_a->fabric_ep_id;
+ peer.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL;
+ peer.port_index = pa->index;
+ drm_fabric_port_set_peer(pb, &peer);
+ }
+}
+
+static void fabricsim_link_mesh(void)
+{
+ int i, j, k, port_idx, peer_port_idx;
+ struct drm_fabric_peer peer;
+
+ /* Fully-connected K_N: port j on ep[i] reaches ep[j], shifted past i. */
+ for (i = 0; i < fabricsim_init_eps; i++) {
+ port_idx = 0;
+ for (j = 0; j < fabricsim_init_eps; j++) {
+ if (i == j)
+ continue;
+
+ if (port_idx >= fabricsim_slots[i]->num_ports)
+ break;
+
+ /*
+ * On endpoint j, i uses the slot obtained by skipping j
+ * in endpoint order.
+ */
+ peer_port_idx = 0;
+ for (k = 0; k < fabricsim_init_eps; k++) {
+ if (k == j)
+ continue;
+ if (k == i)
+ break;
+ peer_port_idx++;
+ }
+
+ peer.peer_id = fabricsim_slots[j]->ep->fabric_ep_id;
+ peer.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL;
+ peer.port_index = peer_port_idx;
+ drm_fabric_port_set_peer(fabricsim_slots[i]->ports[port_idx].port,
+ &peer);
+
+ port_idx++;
+ }
+ }
+}
+
+/*
+ * Opaque switch peer-id, deliberately outside the leaf range (0x100 + slot)
+ * so it never resolves in the endpoint registry.
+ */
+#define FABRICSIM_SWITCH_FABRIC_EP_ID 0x5000ULL
+
+static void fabricsim_link_switch(void)
+{
+ struct drm_fabric_peer peer;
+ int i;
+
+ for (i = 0; i < fabricsim_init_eps; i++) {
+ struct drm_fabric_port *leaf_port =
+ fabricsim_slots[i]->ports[0].port;
+
+ if (!leaf_port)
+ continue;
+
+ /* One directed half-edge from the leaf to an opaque switch. */
+ peer.peer_id = FABRICSIM_SWITCH_FABRIC_EP_ID;
+ peer.peer_type = DRM_FABRIC_PEER_TYPE_SWITCH;
+ peer.port_index = i; /* distinct switch-side port per leaf */
+ drm_fabric_port_set_peer(leaf_port, &peer);
+ }
+}
+
+static void fabricsim_ep_debugfs_create(struct fabricsim_ep_priv *ep_priv)
+{
+ struct dentry *port_dir;
+ char name[32];
+ int j;
+
+ if (IS_ERR_OR_NULL(fabricsim_debugfs_root))
+ return;
+
+ snprintf(name, sizeof(name), "ep%d", ep_priv->slot);
+ ep_priv->dbg_dir = debugfs_create_dir(name, fabricsim_debugfs_root);
+ if (IS_ERR_OR_NULL(ep_priv->dbg_dir)) {
+ ep_priv->dbg_dir = NULL;
+ return;
+ }
+
+ for (j = 0; j < ep_priv->num_ports; j++) {
+ struct fabricsim_port_priv *pp = &ep_priv->ports[j];
+
+ snprintf(name, sizeof(name), "port%d", j);
+ port_dir = debugfs_create_dir(name, ep_priv->dbg_dir);
+
+ debugfs_create_file("activity_enable", 0644,
+ port_dir, pp, &fabricsim_activity_fops);
+ debugfs_create_file("inject", 0200,
+ port_dir, pp, &fabricsim_inject_fops);
+ debugfs_create_file("oper_state", 0644,
+ port_dir, pp, &fabricsim_oper_state_fops);
+ debugfs_create_u32("read_rate", 0644, port_dir, &pp->read_rate);
+ debugfs_create_u32("write_rate", 0644, port_dir, &pp->write_rate);
+ debugfs_create_u32("stats_errno", 0644, port_dir, &pp->stats_errno);
+ }
+}
+
+/*
+ * Create one endpoint at @slot with @nports ports, registered as a member of
+ * the synthetic fabric. Returns the new ep_priv or an ERR_PTR. Caller holds
+ * fabricsim_lock.
+ */
+static struct fabricsim_ep_priv *fabricsim_make_ep(int slot, int nports)
+{
+ struct drm_fabric_endpoint_desc edesc = {};
+ struct drm_fabric_port_desc pdescs[16];
+ struct fabricsim_ep_priv *ep_priv;
+ struct platform_device *pdev;
+ char ep_name[32];
+ int j, ret;
+
+ lockdep_assert_held(&fabricsim_lock);
+
+ if (nports < 1)
+ nports = 1;
+ if (nports > 16)
+ nports = 16;
+
+ /* Refuse before any allocation, so there is nothing to roll back. */
+ if (fabricsim_fail_register)
+ return ERR_PTR(fabricsim_injected_errno());
+
+ ep_priv = kzalloc_obj(*ep_priv, GFP_KERNEL);
+ if (!ep_priv)
+ return ERR_PTR(-ENOMEM);
+
+ ep_priv->slot = slot;
+ ep_priv->num_ports = nports;
+
+ pdev = platform_device_register_simple("fabricsim", slot, NULL, 0);
+ if (IS_ERR(pdev)) {
+ ret = PTR_ERR(pdev);
+ goto err_free;
+ }
+ ep_priv->pdev = pdev;
+
+ for (j = 0; j < nports; j++) {
+ pdescs[j].index = j;
+ pdescs[j].max_lane_count = 4;
+ pdescs[j].max_lane_signaling_rate_mbps = 200000; /* 200 Gbps/lane */
+ }
+
+ snprintf(ep_name, sizeof(ep_name), "sim-ep%d", slot);
+ edesc.fabric_ep_id = 0x100 + slot;
+ edesc.name = ep_name;
+ edesc.parent = &pdev->dev;
+ edesc.ops = &fabricsim_ops;
+ edesc.priv = ep_priv;
+ edesc.ports = pdescs;
+ edesc.num_ports = nports;
+
+ /*
+ * Fill the port array before registering: a racing PORT_STATS_GET can
+ * hit any port once published.
+ */
+ ep_priv->ports = kcalloc(nports, sizeof(struct fabricsim_port_priv),
+ GFP_KERNEL);
+ if (!ep_priv->ports) {
+ ret = -ENOMEM;
+ goto err_pdev;
+ }
+
+ for (j = 0; j < nports; j++) {
+ struct fabricsim_port_priv *pp = &ep_priv->ports[j];
+
+ pp->read_rate = 1024;
+ pp->write_rate = 512;
+ mutex_init(&pp->activity_lock);
+ timer_setup(&pp->activity_timer, fabricsim_activity_tick, 0);
+ }
+
+ ep_priv->ep = drm_fabric_endpoint_register(fabricsim_fabric, &edesc);
+ if (IS_ERR(ep_priv->ep)) {
+ ret = PTR_ERR(ep_priv->ep);
+ goto err_ports;
+ }
+
+ /*
+ * Fill the simulator port pointers before topology wiring and before
+ * the debugfs nodes make them externally reachable.
+ */
+ for (j = 0; j < nports; j++)
+ ep_priv->ports[j].port = drm_fabric_endpoint_port(ep_priv->ep, j);
+
+ fabricsim_ep_debugfs_create(ep_priv);
+
+ return ep_priv;
+
+err_ports:
+ for (j = 0; j < nports; j++)
+ mutex_destroy(&ep_priv->ports[j].activity_lock);
+ kfree(ep_priv->ports);
+err_pdev:
+ platform_device_unregister(ep_priv->pdev);
+err_free:
+ kfree(ep_priv);
+ return ERR_PTR(ret);
+}
+
+/* Peers on other endpoints are left untouched. Caller holds fabricsim_lock. */
+static void fabricsim_destroy_ep(struct fabricsim_ep_priv *ep_priv)
+{
+ int j;
+
+ lockdep_assert_held(&fabricsim_lock);
+
+ debugfs_remove_recursive(ep_priv->dbg_dir);
+
+ for (j = 0; j < ep_priv->num_ports; j++) {
+ struct fabricsim_port_priv *pp = &ep_priv->ports[j];
+
+ /*
+ * debugfs_remove_recursive() drained writers before the
+ * activity timer is shut down.
+ */
+ scoped_guard(mutex, &pp->activity_lock) {
+ WRITE_ONCE(pp->activity_enabled, false);
+ timer_shutdown_sync(&pp->activity_timer);
+ }
+ mutex_destroy(&pp->activity_lock);
+ }
+
+ drm_fabric_endpoint_unregister(ep_priv->ep);
+ kfree(ep_priv->ports);
+ platform_device_unregister(ep_priv->pdev);
+ kfree(ep_priv);
+}
+
+static int fabricsim_add_endpoint(int nports)
+{
+ struct fabricsim_ep_priv *ep_priv;
+ int slot, ret;
+
+ mutex_lock(&fabricsim_lock);
+ if (fabricsim_exiting) {
+ mutex_unlock(&fabricsim_lock);
+ return -ENODEV;
+ }
+
+ for (slot = 0; slot < FABRICSIM_MAX_EPS; slot++)
+ if (!fabricsim_slots[slot])
+ break;
+ if (slot == FABRICSIM_MAX_EPS) {
+ mutex_unlock(&fabricsim_lock);
+ return -ENOSPC;
+ }
+
+ ep_priv = fabricsim_make_ep(slot, nports);
+ if (IS_ERR(ep_priv)) {
+ ret = PTR_ERR(ep_priv);
+ mutex_unlock(&fabricsim_lock);
+ return ret;
+ }
+ ep_priv->runtime = true;
+ fabricsim_slots[slot] = ep_priv;
+ mutex_unlock(&fabricsim_lock);
+
+ return slot;
+}
+
+/* fabricsim_lock stays held across teardown, so the slot remains reserved. */
+static int fabricsim_del_endpoint(int slot)
+{
+ struct fabricsim_ep_priv *ep_priv;
+
+ if (slot < 0 || slot >= FABRICSIM_MAX_EPS)
+ return -EINVAL;
+
+ mutex_lock(&fabricsim_lock);
+ if (fabricsim_exiting) {
+ mutex_unlock(&fabricsim_lock);
+ return -ENODEV;
+ }
+ ep_priv = fabricsim_slots[slot];
+ if (!ep_priv) {
+ mutex_unlock(&fabricsim_lock);
+ return -ENOENT;
+ }
+ fabricsim_destroy_ep(ep_priv);
+ fabricsim_slots[slot] = NULL;
+ mutex_unlock(&fabricsim_lock);
+
+ return 0;
+}
+
+/*
+ * @n single-port member endpoints for the dump-scale selftest: population, not
+ * topology. Returns the count added, or a negative errno only if none were.
+ */
+static int fabricsim_bulk_add(int n)
+{
+ int added = 0;
+ int ret;
+
+ if (n <= 0)
+ return -EINVAL;
+
+ while (added < n) {
+ ret = fabricsim_add_endpoint(1);
+ if (ret < 0)
+ return added ? added : ret;
+ added++;
+ }
+ return added;
+}
+
+/*
+ * Ownership comes from ->runtime, not the slot index: slots are reused, so a
+ * runtime endpoint can sit below the init population.
+ */
+static int fabricsim_bulk_del(void)
+{
+ int slot;
+
+ for (slot = 0; slot < FABRICSIM_MAX_EPS; slot++) {
+ mutex_lock(&fabricsim_lock);
+ if (fabricsim_exiting) {
+ mutex_unlock(&fabricsim_lock);
+ return -ENODEV;
+ }
+ if (fabricsim_slots[slot] && fabricsim_slots[slot]->runtime) {
+ fabricsim_destroy_ep(fabricsim_slots[slot]);
+ fabricsim_slots[slot] = NULL;
+ }
+ mutex_unlock(&fabricsim_lock);
+ }
+ return 0;
+}
+
+static int fabricsim_parse_int(const char __user *buf, size_t count, int dflt)
+{
+ char kbuf[16];
+ int val;
+
+ if (count == 0 || count >= sizeof(kbuf))
+ return dflt;
+ if (copy_from_user(kbuf, buf, count))
+ return dflt;
+ kbuf[count] = '\0';
+ if (kstrtoint(strim(kbuf), 10, &val))
+ return dflt;
+ return val;
+}
+
+static ssize_t fabricsim_add_ep_write(struct file *file, const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ int nports = fabricsim_parse_int(buf, count, ports_per_ep);
+ int ret = fabricsim_add_endpoint(nports);
+
+ return ret < 0 ? ret : count;
+}
+
+static ssize_t fabricsim_del_ep_write(struct file *file, const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ int slot = fabricsim_parse_int(buf, count, -1);
+ int ret = fabricsim_del_endpoint(slot);
+
+ return ret < 0 ? ret : count;
+}
+
+static const struct file_operations fabricsim_add_ep_fops = {
+ .owner = THIS_MODULE,
+ .write = fabricsim_add_ep_write,
+};
+
+static const struct file_operations fabricsim_del_ep_fops = {
+ .owner = THIS_MODULE,
+ .write = fabricsim_del_ep_write,
+};
+
+static ssize_t fabricsim_bulk_add_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ int n = fabricsim_parse_int(buf, count, 0);
+ int ret = fabricsim_bulk_add(n);
+
+ return ret < 0 ? ret : count;
+}
+
+static ssize_t fabricsim_bulk_del_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ int ret = fabricsim_bulk_del();
+
+ return ret < 0 ? ret : count;
+}
+
+static const struct file_operations fabricsim_bulk_add_fops = {
+ .owner = THIS_MODULE,
+ .write = fabricsim_bulk_add_write,
+};
+
+static const struct file_operations fabricsim_bulk_del_fops = {
+ .owner = THIS_MODULE,
+ .write = fabricsim_bulk_del_write,
+};
+
+static ssize_t fabricsim_fail_errno_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ u32 val;
+ int ret;
+
+ ret = kstrtou32_from_user(buf, count, 0, &val);
+ if (ret)
+ return ret;
+ /* Zero stays the documented "clear to -ENOMEM" sentinel. */
+ if (val > MAX_ERRNO)
+ return -EINVAL;
+ fabricsim_fail_errno = val;
+ return count;
+}
+
+static ssize_t fabricsim_fail_errno_read(struct file *file, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ char kbuf[16];
+ int len;
+
+ len = scnprintf(kbuf, sizeof(kbuf), "%u\n", fabricsim_fail_errno);
+ return simple_read_from_buffer(buf, count, ppos, kbuf, len);
+}
+
+static const struct file_operations fabricsim_fail_errno_fops = {
+ .owner = THIS_MODULE,
+ .open = simple_open,
+ .read = fabricsim_fail_errno_read,
+ .write = fabricsim_fail_errno_write,
+};
+
+/*
+ * Validate and derive the module parameters before anything is registered.
+ * This runs before allocation, so it needs no unwind path.
+ */
+static int __init fabricsim_setup_params(void)
+{
+ /*
+ * Reject an unrecognised topology rather than falling back to mesh, so
+ * a typo cannot fake a shape.
+ */
+ if (strcmp(topology, "mesh") && strcmp(topology, "linear") &&
+ strcmp(topology, "switch")) {
+ pr_err("fabricsim: unknown topology \"%s\" (use mesh, linear or switch)\n",
+ topology);
+ return -EINVAL;
+ }
+
+ if (num_endpoints < 2)
+ num_endpoints = 2;
+ if (num_endpoints > FABRICSIM_MAX_INIT_EPS)
+ num_endpoints = FABRICSIM_MAX_INIT_EPS;
+ if (ports_per_ep < 1)
+ ports_per_ep = 1;
+ if (ports_per_ep > 16)
+ ports_per_ep = 16;
+
+ /*
+ * A mesh gives every endpoint (N-1) peers, so the busiest endpoint needs
+ * at least (N-1) ports. The switch shape only needs one port per leaf
+ * (a single half-edge to the opaque switch), so it is not bumped here.
+ */
+ if (strcmp(topology, "mesh") == 0 && ports_per_ep < num_endpoints - 1)
+ ports_per_ep = num_endpoints - 1;
+
+ /*
+ * A linear chain gives every interior node two neighbours, so it needs
+ * at least two ports; bump a too-small request rather than index past
+ * the endpoint's port array.
+ */
+ if (strcmp(topology, "linear") == 0 && num_endpoints > 2 &&
+ ports_per_ep < 2)
+ ports_per_ep = 2;
+
+ fabricsim_init_eps = num_endpoints;
+
+ return 0;
+}
+
+static int __init fabricsim_init(void)
+{
+ struct drm_fabric_desc fdesc;
+ int i, j, ret;
+
+ ret = fabricsim_setup_params();
+ if (ret)
+ return ret;
+
+ fdesc.type = DRM_FABRIC_TYPE_SYNTHETIC;
+ fdesc.name = "fabricsim";
+ fdesc.instance_id = 0x8086CAFE;
+
+ fabricsim_fabric = drm_fabric_register(&fdesc);
+ if (IS_ERR(fabricsim_fabric))
+ return PTR_ERR(fabricsim_fabric);
+
+ /* Root must exist before the per-endpoint debugfs subtrees. */
+ fabricsim_debugfs_root = debugfs_create_dir("drm_fabric_sim", NULL);
+ if (IS_ERR(fabricsim_debugfs_root))
+ fabricsim_debugfs_root = NULL;
+
+ mutex_lock(&fabricsim_lock);
+ for (i = 0; i < fabricsim_init_eps; i++) {
+ struct fabricsim_ep_priv *ep_priv =
+ fabricsim_make_ep(i, ports_per_ep);
+
+ if (IS_ERR(ep_priv)) {
+ ret = PTR_ERR(ep_priv);
+ mutex_unlock(&fabricsim_lock);
+ goto err_eps;
+ }
+ fabricsim_slots[i] = ep_priv;
+ }
+ mutex_unlock(&fabricsim_lock);
+
+ if (strcmp(topology, "linear") == 0)
+ fabricsim_link_linear();
+ else if (strcmp(topology, "switch") == 0)
+ fabricsim_link_switch();
+ else
+ fabricsim_link_mesh();
+
+ /* A linked port starts ACTIVE; an unlinked one stays as registered. */
+ for (i = 0; i < fabricsim_init_eps; i++) {
+ for (j = 0; j < fabricsim_slots[i]->num_ports; j++) {
+ struct fabricsim_port_priv *pp = &fabricsim_slots[i]->ports[j];
+
+ if (pp->port && pp->port->has_peer)
+ drm_fabric_port_set_oper(pp->port,
+ DRM_FABRIC_PORT_STATE_ACTIVE);
+ }
+ }
+
+ /* Root-level runtime lifecycle controls (test-only, not uAPI). */
+ if (fabricsim_debugfs_root) {
+ debugfs_create_file("add_endpoint", 0200, fabricsim_debugfs_root,
+ NULL, &fabricsim_add_ep_fops);
+ debugfs_create_file("del_endpoint", 0200, fabricsim_debugfs_root,
+ NULL, &fabricsim_del_ep_fops);
+
+ debugfs_create_file("bulk_add", 0200, fabricsim_debugfs_root,
+ NULL, &fabricsim_bulk_add_fops);
+ debugfs_create_file("bulk_del", 0200, fabricsim_debugfs_root,
+ NULL, &fabricsim_bulk_del_fops);
+
+ debugfs_create_bool("fail_register", 0644,
+ fabricsim_debugfs_root,
+ &fabricsim_fail_register);
+ debugfs_create_file("fail_errno", 0644,
+ fabricsim_debugfs_root,
+ NULL, &fabricsim_fail_errno_fops);
+ }
+
+ pr_info("fabricsim: registered %s topology with %d endpoints, %d ports/ep\n",
+ topology, fabricsim_init_eps, ports_per_ep);
+
+ return 0;
+
+err_eps:
+ mutex_lock(&fabricsim_lock);
+ for (i = FABRICSIM_MAX_EPS - 1; i >= 0; i--) {
+ if (fabricsim_slots[i]) {
+ fabricsim_destroy_ep(fabricsim_slots[i]);
+ fabricsim_slots[i] = NULL;
+ }
+ }
+ mutex_unlock(&fabricsim_lock);
+ debugfs_remove_recursive(fabricsim_debugfs_root);
+ WARN_ON(drm_fabric_unregister(fabricsim_fabric));
+ return ret;
+}
+
+static void __exit fabricsim_exit(void)
+{
+ int i;
+
+ /*
+ * fabricsim_exiting gates add/del first and the root subtree is removed
+ * last, so no handler can race this teardown.
+ */
+ mutex_lock(&fabricsim_lock);
+ fabricsim_exiting = true;
+ for (i = FABRICSIM_MAX_EPS - 1; i >= 0; i--) {
+ if (fabricsim_slots[i]) {
+ fabricsim_destroy_ep(fabricsim_slots[i]);
+ fabricsim_slots[i] = NULL;
+ }
+ }
+ mutex_unlock(&fabricsim_lock);
+
+ debugfs_remove_recursive(fabricsim_debugfs_root);
+ WARN_ON(drm_fabric_unregister(fabricsim_fabric));
+
+ pr_info("fabricsim: unloaded\n");
+}
+
+module_init(fabricsim_init);
+module_exit(fabricsim_exit);
+
+MODULE_AUTHOR("Intel Corporation");
+MODULE_AUTHOR("Konstantin Sinyuk <ksinyuk@kernel.org>");
+MODULE_DESCRIPTION("DRM Fabric fabricsim synthetic driver");
+MODULE_LICENSE("Dual MIT/GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 05/12] drm/fabric: add object-model KUnit tests
2026-08-24 8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
` (3 preceding siblings ...)
2026-08-24 8:09 ` [RFC PATCH 04/12] drm/fabric: add read-only synthetic provider Konstantin Sinyuk
@ 2026-08-24 8:09 ` Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 06/12] drm/fabric: add YNL query and policy selftests Konstantin Sinyuk
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
The object model has invariants a refactor can break: identity scope,
borrowed port lifetime, topology-generation updates and the context in
which provider callbacks run.
Build the tests into drm_fabric.o under CONFIG_DRM_FABRIC_KUNIT_TEST so
no test-only symbols need to be exported.
Coverage includes registration and identity uniqueness, object lifetime,
half-edge peer state, topology-generation changes and provider ops
wiring. Linear and mesh topologies are parameterized. Unregister rejects
a non-member pointer, including one carrying a registered object's
identifier.
Provide .kunitconfig for UML and .kunitconfig.debug for x86-64 with
KASAN, KMEMLEAK, UBSAN, PROVE_LOCKING and DEBUG_ATOMIC_SLEEP.
$ ./tools/testing/kunit/kunit.py run \
--kunitconfig=drivers/gpu/drm/fabric/.kunitconfig
[...]
Testing complete. Ran 31 tests: passed: 31
Signed-off-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
Documentation/gpu/drm-fabric.rst | 6 +
drivers/gpu/drm/fabric/.kunitconfig | 5 +
drivers/gpu/drm/fabric/.kunitconfig.debug | 16 +
drivers/gpu/drm/fabric/Kconfig | 16 +
drivers/gpu/drm/fabric/Makefile | 2 +
drivers/gpu/drm/fabric/drm_fabric.c | 4 +
drivers/gpu/drm/fabric/drm_fabric_test.c | 1312 +++++++++++++++++++++
7 files changed, 1361 insertions(+)
create mode 100644 drivers/gpu/drm/fabric/.kunitconfig
create mode 100644 drivers/gpu/drm/fabric/.kunitconfig.debug
create mode 100644 drivers/gpu/drm/fabric/drm_fabric_test.c
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index 8bd3633d41be..cd98a97e1662 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -351,3 +351,9 @@ and runtime endpoint add/remove. These files are unstable test controls and are
not part of the uAPI; the stable, reviewed interface is the YAML-described
Generic Netlink family. Tests mutate simulator state through debugfs and observe
the result over Generic Netlink.
+
+Testing
+=======
+
+The object model is covered by KUnit when ``CONFIG_DRM_FABRIC_KUNIT_TEST`` is
+enabled. The test source is folded into the core translation unit.
diff --git a/drivers/gpu/drm/fabric/.kunitconfig b/drivers/gpu/drm/fabric/.kunitconfig
new file mode 100644
index 000000000000..d6087f45f2ac
--- /dev/null
+++ b/drivers/gpu/drm/fabric/.kunitconfig
@@ -0,0 +1,5 @@
+CONFIG_KUNIT=y
+CONFIG_NET=y
+CONFIG_DRM=y
+CONFIG_DRM_FABRIC=y
+CONFIG_DRM_FABRIC_KUNIT_TEST=y
diff --git a/drivers/gpu/drm/fabric/.kunitconfig.debug b/drivers/gpu/drm/fabric/.kunitconfig.debug
new file mode 100644
index 000000000000..8add027fc766
--- /dev/null
+++ b/drivers/gpu/drm/fabric/.kunitconfig.debug
@@ -0,0 +1,16 @@
+CONFIG_KUNIT=y
+CONFIG_NET=y
+CONFIG_DRM=y
+CONFIG_DRM_FABRIC=y
+CONFIG_DRM_FABRIC_KUNIT_TEST=y
+CONFIG_DEBUG_KERNEL=y
+CONFIG_KASAN=y
+CONFIG_KASAN_GENERIC=y
+CONFIG_DEBUG_KMEMLEAK=y
+CONFIG_PROVE_LOCKING=y
+CONFIG_DEBUG_ATOMIC_SLEEP=y
+CONFIG_UBSAN=y
+CONFIG_UBSAN_BOUNDS=y
+CONFIG_UBSAN_SHIFT=y
+CONFIG_UBSAN_ENUM=y
+CONFIG_UBSAN_BOOL=y
diff --git a/drivers/gpu/drm/fabric/Kconfig b/drivers/gpu/drm/fabric/Kconfig
index 87115356baca..2a70cac85b1d 100644
--- a/drivers/gpu/drm/fabric/Kconfig
+++ b/drivers/gpu/drm/fabric/Kconfig
@@ -22,3 +22,19 @@ config DRM_FABRIC_SIM
error injection, and port-state transitions. These hooks are
NOT part of the drm_fabric uAPI and are used only by selftests.
Does not model UALink protocol traffic or memory semantics.
+
+config DRM_FABRIC_KUNIT_TEST
+ bool "DRM fabric object-model KUnit tests" if !KUNIT_ALL_TESTS
+ depends on DRM_FABRIC && KUNIT
+ depends on KUNIT=y || DRM_FABRIC=m
+ default KUNIT_ALL_TESTS
+ help
+ Enable KUnit coverage for the drm_fabric object model.
+
+ The tests are built into drm_fabric itself, so they need no exported
+ symbols or test-only accessors in the production source.
+
+ For more information on KUnit and unit tests in general, please refer
+ to the KUnit documentation in Documentation/dev-tools/kunit/.
+
+ If unsure, say N.
diff --git a/drivers/gpu/drm/fabric/Makefile b/drivers/gpu/drm/fabric/Makefile
index bc0a6c742164..05f1c96bb8de 100644
--- a/drivers/gpu/drm/fabric/Makefile
+++ b/drivers/gpu/drm/fabric/Makefile
@@ -5,3 +5,5 @@ drm-fabric-y := drm_fabric.o drm_fabric_netlink.o drm_fabric_nl.o
obj-$(CONFIG_DRM_FABRIC_SIM) += drm-fabric-sim.o
drm-fabric-sim-y := drm_fabric_sim.o
+
+# The tests are included in drm_fabric.o, not built as a separate object.
diff --git a/drivers/gpu/drm/fabric/drm_fabric.c b/drivers/gpu/drm/fabric/drm_fabric.c
index ce331656af70..bbc3224c3314 100644
--- a/drivers/gpu/drm/fabric/drm_fabric.c
+++ b/drivers/gpu/drm/fabric/drm_fabric.c
@@ -690,3 +690,7 @@ module_exit(drm_fabric_exit);
MODULE_AUTHOR("Intel Corporation");
MODULE_DESCRIPTION("DRM fabric infrastructure");
MODULE_LICENSE("Dual MIT/GPL");
+
+#if IS_ENABLED(CONFIG_DRM_FABRIC_KUNIT_TEST)
+#include "drm_fabric_test.c"
+#endif
diff --git a/drivers/gpu/drm/fabric/drm_fabric_test.c b/drivers/gpu/drm/fabric/drm_fabric_test.c
new file mode 100644
index 000000000000..3457675645f6
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_test.c
@@ -0,0 +1,1312 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/*
+ * KUnit tests for DRM Fabric object lifetime, identity, topology and
+ * provider callback registration.
+ */
+
+#include <kunit/test.h>
+#include <kunit/device.h>
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/mutex.h>
+#include <linux/string.h>
+
+#include <drm/drm_fabric.h>
+#include <uapi/drm/drm_fabric.h>
+
+#include "drm_fabric_internal.h"
+
+static struct drm_fabric_port *fabrictest_port(struct drm_fabric_endpoint *ep,
+ u32 index)
+{
+ return drm_fabric_endpoint_port(ep, index);
+}
+
+static struct device *fabrictest_alloc_dev(struct kunit *test)
+{
+ struct device *dev = kunit_device_register(test, "drm_fabric_test");
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+ return dev;
+}
+
+static void fabrictest_unregister_fabric(void *fab)
+{
+ drm_fabric_unregister(fab);
+}
+
+static void fabrictest_unregister_endpoint(void *ep)
+{
+ drm_fabric_endpoint_unregister(ep);
+}
+
+/* Production reads the generation under the lock; do the same here. */
+static u32 fabrictest_seq_read(void)
+{
+ guard(mutex)(&drm_fabric_lock);
+
+ return drm_fabric_base_seq;
+}
+
+static void fabrictest_seq_write(u32 val)
+{
+ guard(mutex)(&drm_fabric_lock);
+
+ drm_fabric_base_seq = val;
+}
+
+static void fabrictest_restore_seq(void *saved)
+{
+ fabrictest_seq_write(*(u32 *)saved);
+}
+
+/* Restore on exit so a seeded value cannot leak into a later test. */
+static void fabrictest_seed_seq(struct kunit *test, u32 val)
+{
+ u32 *saved = kunit_kzalloc(test, sizeof(*saved), GFP_KERNEL);
+
+ KUNIT_ASSERT_NOT_NULL(test, saved);
+ *saved = fabrictest_seq_read();
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_restore_seq, saved));
+
+ fabrictest_seq_write(val);
+}
+
+static void drm_fabric_test_fabric_register(struct kunit *test)
+{
+ struct drm_fabric_desc desc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-fabric",
+ .instance_id = 0xDEAD,
+ };
+ struct drm_fabric *fab;
+
+ fab = drm_fabric_register(&desc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ KUNIT_EXPECT_EQ(test, fab->type, DRM_FABRIC_TYPE_SYNTHETIC);
+ KUNIT_EXPECT_EQ(test, fab->instance_id, 0xDEADULL);
+}
+
+/*
+ * The non-empty case warns because it is a provider teardown bug, so it is not
+ * exercised here.
+ */
+static void drm_fabric_test_unregister_reports_removal(struct kunit *test)
+{
+ struct drm_fabric_desc desc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "unreg-ret",
+ };
+ struct drm_fabric *fab;
+
+ fab = drm_fabric_register(&desc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_EXPECT_EQ(test, drm_fabric_unregister(fab), 0);
+}
+
+static void drm_fabric_test_endpoint_requires_fabric(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",
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ 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);
+}
+
+/* Registration must not walk a NULL port array. */
+static void drm_fabric_test_endpoint_requires_port_array(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "no-port-array",
+ };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 1,
+ .name = "portless",
+ .parent = fabrictest_dev,
+ .ports = NULL,
+ .num_ports = 1,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(ep));
+ if (IS_ERR(ep))
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ep), -EINVAL);
+}
+
+/* Reject a non-member pointer without dereferencing it. */
+static void drm_fabric_test_unregister_rejects_non_member(struct kunit *test)
+{
+ struct drm_fabric *candidate;
+ u32 before;
+
+ candidate = kunit_kzalloc(test, sizeof(*candidate), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, candidate);
+
+ before = fabrictest_seq_read();
+ KUNIT_EXPECT_EQ(test, drm_fabric_unregister(candidate), -ENODEV);
+ /* A rejected teardown emits nothing, so the generation cannot move. */
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), before);
+}
+
+/*
+ * The check must compare the supplied pointer, not resolve the id it carries:
+ * an id-keyed lookup would erase whichever live fabric owns that id.
+ */
+static void drm_fabric_test_unregister_rejects_same_id(struct kunit *test)
+{
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "same-id-owner",
+ };
+ struct drm_fabric *fab, *twin;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ twin = kunit_kzalloc(test, sizeof(*twin), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, twin);
+ twin->id = fab->id;
+
+ KUNIT_EXPECT_EQ(test, drm_fabric_unregister(twin), -ENODEV);
+
+ /* Returning 0 proves the impostor did not erase the id's owner. */
+ kunit_remove_action(test, fabrictest_unregister_fabric, fab);
+ KUNIT_EXPECT_EQ(test, drm_fabric_unregister(fab), 0);
+}
+
+/*
+ * The endpoint registry is checked directly rather than through
+ * drm_fabric_endpoint_unregister(), whose only report channel for an
+ * unregistered pointer is a one-shot warning.
+ */
+static void drm_fabric_test_ep_membership_rejects_non_member(struct kunit *test)
+{
+ struct drm_fabric_endpoint *ep;
+
+ ep = kunit_kzalloc(test, sizeof(*ep), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ep);
+
+ scoped_guard(mutex, &drm_fabric_lock)
+ KUNIT_EXPECT_FALSE(test, drm_fabric_ep_is_registered(ep));
+}
+
+/* Endpoint counterpart of the same-id case: membership is pointer identity. */
+static void drm_fabric_test_ep_membership_rejects_same_id(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "ep-same-id-fab",
+ };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 7,
+ .name = "ep-same-id-owner",
+ .parent = fabrictest_dev,
+ };
+ struct drm_fabric_endpoint *ep, *twin;
+ struct drm_fabric *fab;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ twin = kunit_kzalloc(test, sizeof(*twin), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, twin);
+ twin->id = ep->id;
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ KUNIT_EXPECT_FALSE(test, drm_fabric_ep_is_registered(twin));
+ KUNIT_EXPECT_TRUE(test, drm_fabric_ep_is_registered(ep));
+ }
+}
+
+/*
+ * Collapses the register-vs-unregister race: unregister first, then attach to
+ * the stale pointer.
+ */
+static void drm_fabric_test_endpoint_register_stale_fabric(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_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "stale-parent",
+ };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 1,
+ .name = "orphaned-by-unreg",
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, drm_fabric_unregister(fab), 0);
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(ep));
+ if (IS_ERR(ep))
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ep), -ENODEV);
+}
+
+static void drm_fabric_test_instance_id_unique(struct kunit *test)
+{
+ struct drm_fabric_desc a = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-a",
+ .instance_id = 0x1357,
+ };
+ struct drm_fabric_desc dup = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-dup",
+ .instance_id = 0x1357,
+ };
+ struct drm_fabric_desc zero_a = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-z0", .instance_id = 0,
+ };
+ struct drm_fabric_desc zero_b = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-z1", .instance_id = 0,
+ };
+ struct drm_fabric *fab, *fab2, *dupf;
+
+ fab = drm_fabric_register(&a);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ dupf = drm_fabric_register(&dup);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(dupf));
+ if (IS_ERR(dupf))
+ KUNIT_EXPECT_EQ(test, PTR_ERR(dupf), -EEXIST);
+
+ fab = drm_fabric_register(&zero_a);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+ /* instance_id 0 is not special-cased. */
+ fab2 = drm_fabric_register(&zero_b);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(fab2));
+ if (IS_ERR(fab2))
+ KUNIT_EXPECT_EQ(test, PTR_ERR(fab2), -EEXIST);
+}
+
+static void drm_fabric_test_endpoint_register(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-ep-fab",
+ };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x42,
+ .name = "ep0",
+ .parent = fabrictest_dev,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ KUNIT_EXPECT_PTR_EQ(test, ep->fabric, fab);
+ KUNIT_EXPECT_EQ(test, ep->fabric_ep_id, 0x42ULL);
+}
+
+static void drm_fabric_test_port_register_peer(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-port-fab",
+ };
+ struct drm_fabric_port_desc pdesc = {
+ .index = 0,
+ .max_lane_count = 4,
+ .max_lane_signaling_rate_mbps = 200000,
+ };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x10,
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_peer peer = {
+ .peer_id = 0x20,
+ .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ .port_index = 1,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ port = fabrictest_port(ep, 0);
+ KUNIT_ASSERT_NOT_NULL(test, port);
+
+ KUNIT_EXPECT_EQ(test, port->index, 0);
+ KUNIT_EXPECT_EQ(test, port->max_lane_count, 4);
+ KUNIT_EXPECT_FALSE(test, port->has_peer);
+ KUNIT_EXPECT_EQ(test, ep->num_ports, 1);
+
+ KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(port, &peer), 0);
+ KUNIT_EXPECT_TRUE(test, port->has_peer);
+ KUNIT_EXPECT_EQ(test, port->peer.peer_id, 0x20ULL);
+ KUNIT_EXPECT_EQ(test, port->peer.port_index, 1);
+}
+
+/* Topology changes must invalidate an in-progress dump. */
+static void drm_fabric_test_base_seq_advances(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-base-seq",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x40,
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_peer peer = {
+ .peer_id = 0x41,
+ .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ .port_index = 0,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+ u32 seq;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ port = fabrictest_port(ep, 0);
+ KUNIT_ASSERT_NOT_NULL(test, port);
+
+ seq = fabrictest_seq_read();
+ KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(port, &peer), 0);
+ KUNIT_EXPECT_NE(test, fabrictest_seq_read(), seq);
+
+ seq = fabrictest_seq_read();
+ KUNIT_EXPECT_EQ(test, drm_fabric_port_unset_peer(port), 0);
+ KUNIT_EXPECT_NE(test, fabrictest_seq_read(), seq);
+
+ seq = fabrictest_seq_read();
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ KUNIT_EXPECT_NE(test, fabrictest_seq_read(), seq);
+
+ /* A no-op transition must not advance the generation. */
+ seq = fabrictest_seq_read();
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), seq);
+}
+
+/* The generation is a nonzero u32: wrapping must skip 0, not just increment. */
+static void drm_fabric_test_generation_wrap_nonzero(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-gen-wrap",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x4A,
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ port = fabrictest_port(ep, 0);
+ KUNIT_ASSERT_NOT_NULL(test, port);
+
+ /* Make the next state change advance the generation once. */
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_INACTIVE);
+
+ fabrictest_seed_seq(test, U32_MAX);
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ KUNIT_EXPECT_NE(test, fabrictest_seq_read(), 0);
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), 1);
+}
+
+static void drm_fabric_test_port_state_transition(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-state",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x30,
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ port = fabrictest_port(ep, 0);
+ KUNIT_ASSERT_NOT_NULL(test, port);
+
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_UNKNOWN);
+
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_DEGRADED);
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_DEGRADED);
+
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_INACTIVE);
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_INACTIVE);
+}
+
+/* An invalid provider state must not be committed. */
+static void drm_fabric_test_port_oper_state_rejects_invalid(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-state-invalid",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x32,
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ enum drm_fabric_port_state bad =
+ (enum drm_fabric_port_state)(DRM_FABRIC_PORT_STATE_DEGRADED + 1);
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+ struct drm_fabric_port *port;
+ u32 seq;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ port = fabrictest_port(ep, 0);
+ KUNIT_ASSERT_NOT_NULL(test, port);
+
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+ seq = fabrictest_seq_read();
+ drm_fabric_port_set_oper(port, bad);
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), seq);
+
+ drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_INACTIVE);
+ KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_INACTIVE);
+}
+
+static void drm_fabric_test_register_rejects_invalid_type(struct kunit *test)
+{
+ struct drm_fabric_desc above = {
+ .type = (enum drm_fabric_type)(DRM_FABRIC_TYPE_SYNTHETIC + 1),
+ .instance_id = 0x5a5a,
+ .name = "test-type-above",
+ };
+ struct drm_fabric_desc zero = {
+ .type = (enum drm_fabric_type)0,
+ .instance_id = 0x5a5a,
+ .name = "test-type-zero",
+ };
+ struct drm_fabric_desc good = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .instance_id = 0x5a5a,
+ .name = "test-type-good",
+ };
+ struct drm_fabric *fab, *bad;
+ u32 seq = fabrictest_seq_read();
+
+ bad = drm_fabric_register(&above);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(bad));
+ if (IS_ERR(bad))
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bad), -EINVAL);
+ else
+ drm_fabric_unregister(bad);
+
+ bad = drm_fabric_register(&zero);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(bad));
+ if (IS_ERR(bad))
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bad), -EINVAL);
+ else
+ drm_fabric_unregister(bad);
+
+ /* Refused before publication, so the generation cannot have advanced. */
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), seq);
+
+ /*
+ * Same instance id as both refusals: drm_fabric_has_instance() would
+ * answer -EEXIST here if either had left a registry entry behind.
+ */
+ fab = drm_fabric_register(&good);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+}
+
+/* Verify reciprocity and far-side port indices for every K_4 edge. */
+static void drm_fabric_test_mesh_kn_topology(struct kunit *test)
+{
+#define KN_EPS 4
+#define KN_PORTS_PER_EP (KN_EPS - 1)
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "mesh-kn",
+ .instance_id = 0x1002,
+ };
+ struct drm_fabric_port_desc pdescs[KN_PORTS_PER_EP];
+ struct drm_fabric_endpoint_desc edesc;
+ struct drm_fabric_peer peer;
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *eps[KN_EPS];
+ struct drm_fabric_port *ports[KN_EPS][KN_PORTS_PER_EP];
+ int i, j, k, port_idx, peer_port;
+
+ memset(pdescs, 0, sizeof(pdescs));
+ for (j = 0; j < KN_PORTS_PER_EP; j++) {
+ pdescs[j].index = j;
+ pdescs[j].max_lane_count = 4;
+ pdescs[j].max_lane_signaling_rate_mbps = 200000;
+ }
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ for (i = 0; i < KN_EPS; i++) {
+ memset(&edesc, 0, sizeof(edesc));
+ edesc.fabric_ep_id = 0x100 + i;
+ edesc.parent = fabrictest_dev;
+ edesc.ports = pdescs;
+ edesc.num_ports = KN_PORTS_PER_EP;
+
+ eps[i] = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(eps[i]));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test,
+ fabrictest_unregister_endpoint, eps[i]));
+
+ for (j = 0; j < KN_PORTS_PER_EP; j++) {
+ ports[i][j] = fabrictest_port(eps[i], j);
+ KUNIT_ASSERT_NOT_NULL(test, ports[i][j]);
+ }
+ }
+
+ /* Wire K_4: ep[i] port[j] -> ep[target] */
+ for (i = 0; i < KN_EPS; i++) {
+ port_idx = 0;
+ for (j = 0; j < KN_EPS; j++) {
+ if (i == j)
+ continue;
+
+ /* Compute peer's port index pointing back to us. */
+ peer_port = 0;
+ for (k = 0; k < KN_EPS; k++) {
+ if (k == j)
+ continue;
+ if (k == i)
+ break;
+ peer_port++;
+ }
+
+ peer.peer_id = eps[j]->fabric_ep_id;
+ peer.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL;
+ peer.port_index = peer_port;
+ drm_fabric_port_set_peer(ports[i][port_idx], &peer);
+ port_idx++;
+ }
+ }
+
+ for (i = 0; i < KN_EPS; i++) {
+ for (j = 0; j < KN_PORTS_PER_EP; j++) {
+ struct drm_fabric_port *p = ports[i][j];
+ struct drm_fabric_port *pp;
+ u64 peer_ep_id;
+ u32 peer_pidx;
+ int peer_ep_idx, pi;
+
+ KUNIT_EXPECT_TRUE_MSG(test, p->has_peer,
+ "ep%d port%d has no peer", i, j);
+ if (!p->has_peer)
+ continue;
+
+ peer_ep_id = p->peer.peer_id;
+ peer_pidx = p->peer.port_index;
+
+ peer_ep_idx = -1;
+ for (pi = 0; pi < KN_EPS; pi++) {
+ if (eps[pi]->fabric_ep_id == peer_ep_id) {
+ peer_ep_idx = pi;
+ break;
+ }
+ }
+ KUNIT_EXPECT_GE_MSG(test, peer_ep_idx, 0,
+ "ep%d port%d peer EP not found", i, j);
+ if (peer_ep_idx < 0)
+ continue;
+ KUNIT_EXPECT_LT(test, peer_pidx, (u32)KN_PORTS_PER_EP);
+ if (peer_pidx >= (u32)KN_PORTS_PER_EP)
+ continue;
+
+ pp = ports[peer_ep_idx][peer_pidx];
+ KUNIT_EXPECT_TRUE(test, pp->has_peer);
+ KUNIT_EXPECT_EQ(test, pp->peer.peer_id, eps[i]->fabric_ep_id);
+ KUNIT_EXPECT_EQ(test, pp->peer.port_index, (u32)j);
+ }
+ }
+#undef KN_EPS
+#undef KN_PORTS_PER_EP
+}
+
+static int fabrictest_stats_get(struct drm_fabric_port *port,
+ struct drm_fabric_port_stats *stats)
+{
+ stats->read_bytes = 4096;
+ stats->write_bytes = 2048;
+ stats->link_down_count = 2;
+ stats->retrain_count = 3;
+ return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_stats_ops = {
+ .port_stats_get = fabrictest_stats_get,
+};
+
+/* This does not exercise netlink dispatch or error propagation. */
+static void drm_fabric_test_port_stats_ops_registration(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-stats",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0x5A,
+ .parent = fabrictest_dev,
+ .ops = &fabrictest_stats_ops,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_endpoint_desc edesc_noops = {
+ .fabric_ep_id = 0x5B,
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_port_stats stats = {};
+ struct drm_fabric_endpoint *ep, *ep_noops;
+ struct drm_fabric_port *port;
+ struct drm_fabric *fab;
+ u32 gen;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ port = fabrictest_port(ep, 0);
+ KUNIT_ASSERT_NOT_NULL(test, port);
+
+ KUNIT_ASSERT_NOT_NULL(test, ep->ops);
+ KUNIT_ASSERT_NOT_NULL(test, ep->ops->port_stats_get);
+
+ /* A stats read is not a topology change: seq must not move. */
+ gen = fabrictest_seq_read();
+ KUNIT_EXPECT_EQ(test, ep->ops->port_stats_get(port, &stats), 0);
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), gen);
+ KUNIT_EXPECT_EQ(test, stats.read_bytes, 4096ULL);
+ KUNIT_EXPECT_EQ(test, stats.write_bytes, 2048ULL);
+ KUNIT_EXPECT_EQ(test, stats.link_down_count, 2ULL);
+ KUNIT_EXPECT_EQ(test, stats.retrain_count, 3ULL);
+
+ ep_noops = drm_fabric_endpoint_register(fab, &edesc_noops);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep_noops));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_noops));
+ KUNIT_EXPECT_TRUE(test, !ep_noops->ops || !ep_noops->ops->port_stats_get);
+}
+
+/*
+ * Unregistering an endpoint that has a peer link must clear only that
+ * endpoint's own port record; it must not touch the still-registered far
+ * side's peer record. (Contrast with drm_fabric_port_unset_peer(), which
+ * clears a peer explicitly and is covered separately.)
+ */
+static void drm_fabric_test_local_unplug_keeps_edge(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-unplug",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+ struct drm_fabric_endpoint_desc eadesc = {
+ .fabric_ep_id = 0xA0,
+ .name = "unplug-a",
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_endpoint_desc ebdesc = {
+ .fabric_ep_id = 0xB0,
+ .name = "unplug-b",
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_peer to_b = {
+ .peer_id = 0xB0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ };
+ struct drm_fabric_peer to_a = {
+ .peer_id = 0xA0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep_a, *ep_b;
+ struct drm_fabric_port *pa, *pb;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
+
+ ep_b = drm_fabric_endpoint_register(fab, &ebdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep_b));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_b));
+
+ pa = fabrictest_port(ep_a, 0);
+ pb = fabrictest_port(ep_b, 0);
+ KUNIT_ASSERT_NOT_NULL(test, pa);
+ KUNIT_ASSERT_NOT_NULL(test, pb);
+
+ KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &to_b), 0);
+ KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pb, &to_a), 0);
+ KUNIT_EXPECT_TRUE(test, pa->has_peer);
+
+ /*
+ * Remove B without retracting its peer first, modelling abrupt provider
+ * teardown.
+ */
+ kunit_release_action(test, fabrictest_unregister_endpoint, ep_b);
+
+ /* The surviving half-edge must be byte-unchanged: no field mutated. */
+ KUNIT_EXPECT_TRUE(test, pa->has_peer);
+ KUNIT_EXPECT_MEMEQ(test, &pa->peer, &to_b, sizeof(pa->peer));
+}
+
+/*
+ * A's peer record names a port index, not an object; registering and then
+ * unregistering an unrelated third endpoint must not perturb it.
+ */
+static void drm_fabric_test_remote_peer_retained(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-remote",
+ };
+ struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+ struct drm_fabric_endpoint_desc eadesc = {
+ .fabric_ep_id = 0xA0,
+ .name = "remote-a",
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ struct drm_fabric_endpoint_desc ecdesc = {
+ .fabric_ep_id = 0xC0,
+ .name = "remote-c",
+ .parent = fabrictest_dev,
+ .ports = &pdesc,
+ .num_ports = 1,
+ };
+ /* 0xBEEF has no local endpoint object. */
+ struct drm_fabric_peer remote = {
+ .peer_id = 0xBEEF, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep_a, *ep_c;
+ struct drm_fabric_port *pa;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
+
+ pa = fabrictest_port(ep_a, 0);
+ KUNIT_ASSERT_NOT_NULL(test, pa);
+
+ KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &remote), 0);
+ KUNIT_EXPECT_TRUE(test, pa->has_peer);
+
+ ep_c = drm_fabric_endpoint_register(fab, &ecdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep_c));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_c));
+
+ kunit_release_action(test, fabrictest_unregister_endpoint, ep_c);
+
+ KUNIT_EXPECT_TRUE(test, pa->has_peer);
+ KUNIT_EXPECT_MEMEQ(test, &pa->peer, &remote, sizeof(pa->peer));
+}
+
+/*
+ * Removing an endpoint with multiple peered ports must bump the topology
+ * generation exactly once, not once per port torn down.
+ */
+static void drm_fabric_test_subtree_delete_single_bump(struct kunit *test)
+{
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-subtree",
+ };
+ struct drm_fabric_port_desc pdescs[3] = {
+ { .index = 0, .max_lane_count = 4 },
+ { .index = 1, .max_lane_count = 4 },
+ { .index = 2, .max_lane_count = 4 },
+ };
+ struct drm_fabric_endpoint_desc edesc = {
+ .fabric_ep_id = 0xD0,
+ .name = "subtree-ep",
+ .parent = fabrictest_dev,
+ .ports = pdescs,
+ .num_ports = 3,
+ };
+ struct drm_fabric_peer peer = {
+ .peer_id = 0xD1, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ };
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *ep;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ ep = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+ /* Two of the three ports carry a half-edge. */
+ KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 0), &peer), 0);
+ KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 1), &peer), 0);
+
+ fabrictest_seed_seq(test, 100);
+ kunit_release_action(test, fabrictest_unregister_endpoint, ep);
+ KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), 101);
+}
+
+static void drm_fabric_test_switch_topology(struct kunit *test)
+{
+#define SW_LEAVES 3
+ /*
+ * Each leaf carries one half-edge to an opaque switch that is not a
+ * registered endpoint, so this asserts half-edge serialization, not any
+ * leaf -> switch -> leaf traversal.
+ */
+ const u64 sw_id = 0x5000;
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC,
+ .name = "test-switch",
+ };
+ struct drm_fabric_port_desc leaf_pdesc = { .index = 0, .max_lane_count = 4 };
+ struct drm_fabric_endpoint_desc edesc;
+ struct drm_fabric_peer peer;
+ struct drm_fabric *fab;
+ struct drm_fabric_endpoint *leaves[SW_LEAVES];
+ struct drm_fabric_port *leaf_ports[SW_LEAVES];
+ int i;
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ for (i = 0; i < SW_LEAVES; i++) {
+ memset(&edesc, 0, sizeof(edesc));
+ edesc.fabric_ep_id = 0x600 + i;
+ edesc.parent = fabrictest_dev;
+ edesc.ports = &leaf_pdesc;
+ edesc.num_ports = 1;
+ leaves[i] = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(leaves[i]));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test,
+ fabrictest_unregister_endpoint,
+ leaves[i]));
+
+ leaf_ports[i] = fabrictest_port(leaves[i], 0);
+ KUNIT_ASSERT_NOT_NULL(test, leaf_ports[i]);
+
+ peer.peer_id = sw_id;
+ peer.peer_type = DRM_FABRIC_PEER_TYPE_SWITCH;
+ peer.port_index = i;
+ KUNIT_ASSERT_EQ(test,
+ drm_fabric_port_set_peer(leaf_ports[i], &peer), 0);
+ }
+
+ /* Switch peer IDs do not share the endpoint identity namespace. */
+ for (i = 0; i < SW_LEAVES; i++) {
+ KUNIT_EXPECT_TRUE(test, leaf_ports[i]->has_peer);
+ KUNIT_EXPECT_EQ(test, leaf_ports[i]->peer.peer_type,
+ (u32)DRM_FABRIC_PEER_TYPE_SWITCH);
+ KUNIT_EXPECT_EQ(test, leaf_ports[i]->peer.peer_id, sw_id);
+ KUNIT_EXPECT_EQ(test, leaf_ports[i]->peer.port_index, (u32)i);
+ KUNIT_EXPECT_NE(test, leaves[i]->fabric_ep_id, sw_id);
+ }
+#undef SW_LEAVES
+}
+
+enum fabrictest_shape { FT_SHAPE_LINEAR, FT_SHAPE_MESH };
+
+struct fabrictest_topo_param {
+ const char *name;
+ enum fabrictest_shape shape;
+ int n_eps;
+ int ports_per_ep;
+};
+
+static const struct fabrictest_topo_param fabrictest_topo_params[] = {
+ { "linear-2", FT_SHAPE_LINEAR, 2, 1 },
+ { "linear-3", FT_SHAPE_LINEAR, 3, 2 },
+ { "linear-5", FT_SHAPE_LINEAR, 5, 2 },
+ { "linear-8", FT_SHAPE_LINEAR, 8, 2 },
+ { "mesh-2", FT_SHAPE_MESH, 2, 1 },
+ { "mesh-3", FT_SHAPE_MESH, 3, 2 },
+ { "mesh-4", FT_SHAPE_MESH, 4, 3 },
+ { "mesh-6", FT_SHAPE_MESH, 6, 5 },
+};
+
+static void fabrictest_topo_desc(const struct fabrictest_topo_param *p,
+ char *desc)
+{
+ strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(fabrictest_topo, fabrictest_topo_params, fabrictest_topo_desc);
+
+#define FT_MAX_EPS 8
+#define FT_MAX_PORTS 16
+
+/* Count peered ports to validate each generated topology's degree. */
+static int fabrictest_peer_degree(struct drm_fabric_endpoint *ep, int n_ports)
+{
+ int j, deg = 0;
+
+ for (j = 0; j < n_ports; j++) {
+ struct drm_fabric_port *port = fabrictest_port(ep, j);
+
+ if (port && port->has_peer)
+ deg++;
+ }
+ return deg;
+}
+
+/*
+ * Chain eps[0]-eps[1]-...-eps[n-1]; endpoints get one link, interior nodes
+ * get two, each consuming the next free port.
+ */
+static void fabrictest_verify_linear_topology(struct kunit *test,
+ const struct fabrictest_topo_param *p,
+ struct drm_fabric_endpoint **eps)
+{
+ int cursor[FT_MAX_EPS] = {0};
+ struct drm_fabric_peer peer;
+ int i;
+
+ for (i = 0; i < p->n_eps - 1; i++) {
+ int ai = cursor[i]++;
+ int bi = cursor[i + 1]++;
+
+ peer = (struct drm_fabric_peer){
+ .peer_id = eps[i + 1]->fabric_ep_id,
+ .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ .port_index = bi,
+ };
+ drm_fabric_port_set_peer(fabrictest_port(eps[i], ai), &peer);
+
+ peer = (struct drm_fabric_peer){
+ .peer_id = eps[i]->fabric_ep_id,
+ .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ .port_index = ai,
+ };
+ drm_fabric_port_set_peer(fabrictest_port(eps[i + 1], bi), &peer);
+ }
+
+ for (i = 0; i < p->n_eps; i++) {
+ int want = (i == 0 || i == p->n_eps - 1) ? 1 : 2;
+
+ KUNIT_EXPECT_EQ_MSG(test,
+ fabrictest_peer_degree(eps[i], p->ports_per_ep),
+ want, "linear ep%d degree", i);
+ }
+}
+
+/*
+ * Fully-connected K_N: every endpoint peers with every other one.
+ */
+static void fabrictest_verify_mesh_topology(struct kunit *test,
+ const struct fabrictest_topo_param *p,
+ struct drm_fabric_endpoint **eps)
+{
+ struct drm_fabric_peer peer;
+ int i, j, k;
+
+ for (i = 0; i < p->n_eps; i++) {
+ int port_idx = 0;
+
+ for (j = 0; j < p->n_eps; j++) {
+ int peer_port = 0;
+
+ if (i == j)
+ continue;
+ for (k = 0; k < p->n_eps; k++) {
+ if (k == j)
+ continue;
+ if (k == i)
+ break;
+ peer_port++;
+ }
+ peer = (struct drm_fabric_peer){
+ .peer_id = eps[j]->fabric_ep_id,
+ .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+ .port_index = peer_port,
+ };
+ drm_fabric_port_set_peer(fabrictest_port(eps[i], port_idx),
+ &peer);
+ port_idx++;
+ }
+ }
+
+ for (i = 0; i < p->n_eps; i++)
+ KUNIT_EXPECT_EQ_MSG(test,
+ fabrictest_peer_degree(eps[i], p->ports_per_ep),
+ p->n_eps - 1, "mesh ep%d degree", i);
+
+ /* Spot-check reciprocity; exhaustive K_4 coverage is tested separately. */
+ for (j = 0; j < p->n_eps - 1; j++) {
+ struct drm_fabric_port *port = fabrictest_port(eps[0], j);
+ struct drm_fabric_port *back;
+ int pi, peer_ep = -1;
+
+ KUNIT_ASSERT_NOT_NULL(test, port);
+ KUNIT_ASSERT_TRUE(test, port->has_peer);
+
+ for (pi = 0; pi < p->n_eps; pi++)
+ if (eps[pi]->fabric_ep_id == port->peer.peer_id) {
+ peer_ep = pi;
+ break;
+ }
+ KUNIT_EXPECT_GE(test, peer_ep, 0);
+ if (peer_ep < 0)
+ continue;
+ back = fabrictest_port(eps[peer_ep], port->peer.port_index);
+ KUNIT_ASSERT_NOT_NULL(test, back);
+ KUNIT_EXPECT_TRUE(test, back->has_peer);
+ KUNIT_EXPECT_EQ(test, back->peer.peer_id,
+ eps[0]->fabric_ep_id);
+ }
+}
+
+/*
+ * Parameterized over fabrictest_topo_params: linear chains and full meshes at
+ * several sizes, checked generically via fabrictest_peer_degree().
+ */
+static void drm_fabric_test_topology_param(struct kunit *test)
+{
+ const struct fabrictest_topo_param *p = test->param_value;
+ struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+ struct drm_fabric_desc fdesc = {
+ .type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "test-topo",
+ };
+ struct drm_fabric_port_desc pdescs[FT_MAX_PORTS];
+ struct drm_fabric_endpoint *eps[FT_MAX_EPS];
+ struct drm_fabric_endpoint_desc edesc;
+ struct drm_fabric *fab;
+ int i, j;
+
+ KUNIT_ASSERT_LE(test, p->n_eps, FT_MAX_EPS);
+ KUNIT_ASSERT_LE(test, p->ports_per_ep, FT_MAX_PORTS);
+
+ memset(pdescs, 0, sizeof(pdescs));
+ for (j = 0; j < p->ports_per_ep; j++) {
+ pdescs[j].index = j;
+ pdescs[j].max_lane_count = 4;
+ pdescs[j].max_lane_signaling_rate_mbps = 200000;
+ }
+
+ fab = drm_fabric_register(&fdesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+ for (i = 0; i < p->n_eps; i++) {
+ memset(&edesc, 0, sizeof(edesc));
+ edesc.fabric_ep_id = 0x100 + i;
+ edesc.parent = fabrictest_dev;
+ edesc.ports = pdescs;
+ edesc.num_ports = p->ports_per_ep;
+
+ eps[i] = drm_fabric_endpoint_register(fab, &edesc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(eps[i]));
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test,
+ fabrictest_unregister_endpoint, eps[i]));
+ }
+
+ if (p->shape == FT_SHAPE_LINEAR)
+ fabrictest_verify_linear_topology(test, p, eps);
+ else
+ fabrictest_verify_mesh_topology(test, p, eps);
+}
+
+static struct kunit_case drm_fabric_test_cases[] = {
+ KUNIT_CASE(drm_fabric_test_fabric_register),
+ 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_requires_port_array),
+ KUNIT_CASE(drm_fabric_test_endpoint_register_stale_fabric),
+ KUNIT_CASE(drm_fabric_test_unregister_rejects_non_member),
+ KUNIT_CASE(drm_fabric_test_unregister_rejects_same_id),
+ KUNIT_CASE(drm_fabric_test_ep_membership_rejects_non_member),
+ KUNIT_CASE(drm_fabric_test_ep_membership_rejects_same_id),
+ KUNIT_CASE(drm_fabric_test_port_register_peer),
+ KUNIT_CASE(drm_fabric_test_base_seq_advances),
+ KUNIT_CASE(drm_fabric_test_generation_wrap_nonzero),
+ KUNIT_CASE(drm_fabric_test_port_state_transition),
+ KUNIT_CASE(drm_fabric_test_port_oper_state_rejects_invalid),
+ KUNIT_CASE(drm_fabric_test_register_rejects_invalid_type),
+ KUNIT_CASE(drm_fabric_test_mesh_kn_topology),
+ KUNIT_CASE(drm_fabric_test_port_stats_ops_registration),
+ KUNIT_CASE(drm_fabric_test_local_unplug_keeps_edge),
+ KUNIT_CASE(drm_fabric_test_remote_peer_retained),
+ KUNIT_CASE(drm_fabric_test_subtree_delete_single_bump),
+ KUNIT_CASE(drm_fabric_test_switch_topology),
+ KUNIT_CASE_PARAM(drm_fabric_test_topology_param,
+ fabrictest_topo_gen_params),
+ {}
+};
+
+static struct kunit_suite drm_fabric_test_suite = {
+ .name = "drm_fabric",
+ .test_cases = drm_fabric_test_cases,
+};
+
+kunit_test_suite(drm_fabric_test_suite);
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 06/12] drm/fabric: add YNL query and policy selftests
2026-08-24 8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
` (4 preceding siblings ...)
2026-08-24 8:09 ` [RFC PATCH 05/12] drm/fabric: add object-model KUnit tests Konstantin Sinyuk
@ 2026-08-24 8:09 ` Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 07/12] drm/fabric: add topology-provisioning core Konstantin Sinyuk
2026-08-24 8:09 ` [RFC PATCH 08/12] drm/fabric: add provisioning netlink uAPI Konstantin Sinyuk
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
The read-only Generic Netlink ABI has userspace-visible behavior KUnit
cannot reach: policy validation, multipart dumps, notifications and family
introspection. These paths require a live netlink socket.
Add eleven kselftest programs using the in-tree YNL client against
fabricsim. They cover queries and filters, notifications, malformed policy
input, interrupted dumps, nested port-cursor resumption after endpoint
removal, mixed per-port statistics support, topology churn, opaque switch
peers and provider error propagation.
Shared helpers manage module lifetime and restore fabricsim state between
programs. Add the selftest configuration, README and a regeneration guard
for the generated uAPI header, kernel header and operation/policy source.
$ make -C tools/testing/selftests TARGETS=drivers/gpu/drm_fabric \
run_tests
[...]
ok 1 selftests: drivers/gpu/drm_fabric: check-spec-regen.sh
[...]
ok 11 selftests: drivers/gpu/drm_fabric: harness_reset_abi.py
The run reports 89 results across eleven programs, all passing in a
booted virtme-ng guest.
Signed-off-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
Documentation/gpu/drm-fabric.rst | 7 +
tools/testing/selftests/Makefile | 1 +
.../selftests/drivers/gpu/drm_fabric/Makefile | 41 ++
.../drivers/gpu/drm_fabric/README.rst | 136 +++++
.../gpu/drm_fabric/check-spec-regen.sh | 114 ++++
.../selftests/drivers/gpu/drm_fabric/config | 8 +
.../drivers/gpu/drm_fabric/dump_intr_abi.py | 361 +++++++++++++
.../drivers/gpu/drm_fabric/dump_scale_abi.py | 178 +++++++
.../drivers/gpu/drm_fabric/fabric_abi.py | 401 +++++++++++++++
.../drivers/gpu/drm_fabric/fault_abi.py | 105 ++++
.../gpu/drm_fabric/harness_reset_abi.py | 113 ++++
.../drivers/gpu/drm_fabric/hotplug_abi.py | 204 ++++++++
.../drivers/gpu/drm_fabric/lib_drm_fabric.py | 481 +++++++++++++++++
.../drivers/gpu/drm_fabric/nl_policy_probe.py | 485 ++++++++++++++++++
.../drivers/gpu/drm_fabric/port_cursor_abi.py | 401 +++++++++++++++
.../gpu/drm_fabric/port_stats_cap_abi.py | 374 ++++++++++++++
.../selftests/drivers/gpu/drm_fabric/settings | 1 +
.../drivers/gpu/drm_fabric/switch_abi.py | 108 ++++
18 files changed, 3519 insertions(+)
create mode 100644 tools/testing/selftests/drivers/gpu/drm_fabric/Makefile
create mode 100644 tools/testing/selftests/drivers/gpu/drm_fabric/README.rst
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/check-spec-regen.sh
create mode 100644 tools/testing/selftests/drivers/gpu/drm_fabric/config
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/dump_intr_abi.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/dump_scale_abi.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/fabric_abi.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/fault_abi.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/harness_reset_abi.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/hotplug_abi.py
create mode 100644 tools/testing/selftests/drivers/gpu/drm_fabric/lib_drm_fabric.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/nl_policy_probe.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/port_cursor_abi.py
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/port_stats_cap_abi.py
create mode 100644 tools/testing/selftests/drivers/gpu/drm_fabric/settings
create mode 100755 tools/testing/selftests/drivers/gpu/drm_fabric/switch_abi.py
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index cd98a97e1662..bc7b87c766cc 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -357,3 +357,10 @@ Testing
The object model is covered by KUnit when ``CONFIG_DRM_FABRIC_KUNIT_TEST`` is
enabled. The test source is folded into the core translation unit.
+
+Generic Netlink ABI tests live under
+``tools/testing/selftests/drivers/gpu/drm_fabric``. They cover the YNL query
+paths, malformed policy input, generated-header synchronization, dump-cursor
+correctness across endpoint removal, ``NLM_F_DUMP_INTR`` handling, the opaque
+switch half-edge, and provider fault handling. See that directory's ``README.rst``
+for build and execution commands.
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index b622052ec3e9..c90eb5d33ec6 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -18,6 +18,7 @@ TARGETS += devices/error_logs
TARGETS += devices/probe
TARGETS += dmabuf-heaps
TARGETS += drivers/dma-buf
+TARGETS += drivers/gpu/drm_fabric
TARGETS += drivers/ntsync
TARGETS += drivers/s390x/uvdevice
TARGETS += drivers/net
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/Makefile b/tools/testing/selftests/drivers/gpu/drm_fabric/Makefile
new file mode 100644
index 000000000000..54d756979d97
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/Makefile
@@ -0,0 +1,41 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# kselftests for the drm-fabric Generic Netlink ABI.
+#
+# Each TEST_PROGS entry is invoked separately by the kselftest harness; suites
+# self-load modules when needed (check-spec-regen.sh is host-only).
+#
+# Run:
+# make -C tools/testing/selftests TARGETS=drivers/gpu/drm_fabric run_tests
+
+TEST_PROGS := \
+ check-spec-regen.sh \
+ fabric_abi.py \
+ nl_policy_probe.py \
+ dump_intr_abi.py \
+ port_cursor_abi.py \
+ port_stats_cap_abi.py \
+ hotplug_abi.py \
+ dump_scale_abi.py \
+ switch_abi.py \
+ fault_abi.py \
+ harness_reset_abi.py
+
+TEST_FILES := lib_drm_fabric.py
+
+include ../../../lib.mk
+
+# A TEST_PROGS entry that loses its executable bit still runs, because the
+# kselftest runner falls back to the shebang, but only after a warning that is
+# easy to miss in a long run. Catch it at build time instead.
+all: check-test-progs-mode
+
+check-test-progs-mode:
+ @for prog in $(TEST_PROGS); do \
+ test -x "$$prog" || { \
+ echo "$$prog: in TEST_PROGS but not executable" >&2; \
+ exit 1; \
+ }; \
+ done
+
+.PHONY: check-test-progs-mode
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/README.rst b/tools/testing/selftests/drivers/gpu/drm_fabric/README.rst
new file mode 100644
index 000000000000..6c24581db1f2
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/README.rst
@@ -0,0 +1,136 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================
+drm_fabric selftests
+====================
+
+These selftests exercise the ``drm-fabric`` query uAPI against
+``drm_fabric_sim`` using the in-tree YNL library. KUnit covers the core object
+model.
+
+Tree layout
+-----------
+
+``Documentation/netlink/specs/drm_fabric.yaml``
+ Netlink specification and source of truth for generated artifacts.
+
+``include/uapi/drm/drm_fabric.h``
+ Generated uAPI header; checked by ``check-spec-regen.sh``.
+
+``drivers/gpu/drm/fabric/drm_fabric_nl.[ch]``
+ Generated kernel policy and operation tables; checked by
+ ``check-spec-regen.sh``.
+
+Suites
+------
+
+``check-spec-regen.sh``
+ Regenerates each artifact from the YAML spec and asserts an exact match.
+
+``fabric_abi.py``
+ Queries, events and topology.
+
+``nl_policy_probe.py``
+ Raw Generic Netlink policy probes and family introspection.
+
+``dump_intr_abi.py``
+ ``NLM_F_DUMP_INTR`` on a generation bump mid-dump, and on ``NLMSG_DONE``
+ when the bump lands after the last entry.
+
+``port_cursor_abi.py``
+ Nested port-dump cursor (``cb->args``) across endpoints under removal.
+
+``port_stats_cap_abi.py``
+ Heterogeneous per-port stats: mid-list ``-EOPNOTSUPP`` skipped, other errno
+ ends the dump.
+
+``hotplug_abi.py``
+ Endpoint hotplug: CREATE/DELETE notifications.
+
+``dump_scale_abi.py``
+ Dump resume under many endpoints (``bulk_add``).
+
+``switch_abi.py``
+ Opaque switch peers whose identifiers do not resolve to an endpoint
+ (``topology=switch``).
+
+``fault_abi.py``
+ Provider fault injection: errno propagation and no leaked endpoint
+ (``fail_*``).
+
+``harness_reset_abi.py``
+ Recovery after a SIGKILL-terminated predecessor.
+
+``lib_drm_fabric.py``
+ Shared helpers.
+
+Expected skips
+--------------
+
+A SKIP means a required precondition was unavailable.
+
+Environment
+ ``check-spec-regen.sh`` needs PyYAML and writable temporary storage.
+
+Per case
+ A case skips when a required control, parameter or family capability is
+ unavailable.
+
+Whole suite
+ A program skips when it cannot establish its initial topology.
+
+Timing
+ The two ``dump_intr_abi.py`` boundary cases may skip if the concurrent
+ topology change misses the required dump boundary.
+
+KUnit
+-----
+
+Keep the source tree free of ``.config`` and use an object directory:
+
+.. code-block:: sh
+
+ export KBUILD_OUTPUT="$PWD/.kunit/dev-kernel"
+
+.. code-block:: sh
+
+ ./tools/testing/kunit/kunit.py run \
+ --kunitconfig drivers/gpu/drm/fabric/.kunitconfig 'drm_fabric*'
+
+Debug configuration:
+
+.. code-block:: sh
+
+ ./tools/testing/kunit/kunit.py run --arch x86_64 \
+ --kunitconfig drivers/gpu/drm/fabric/.kunitconfig.debug \
+ --timeout 900 --qemu_args '-m 2048' 'drm_fabric*'
+
+KASAN, UBSAN, kmemleak, lockdep or atomic-sleep reports fail the run.
+
+Netlink ABI
+-----------
+
+Needs root and a booted kernel carrying the modules. See ``config`` for the
+Kconfig fragment; the runner applies the 300-second timeout from ``settings``.
+
+.. code-block:: sh
+
+ sudo make -C tools/testing/selftests TARGETS=drivers/gpu/drm_fabric run_tests
+
+virtme-ng
+---------
+
+Build out-of-tree, boot with ``vng`` and run the same target in the guest:
+
+.. code-block:: sh
+
+ O=.kunit/vng-drm-fabric
+ vng --kconfig \
+ --config tools/testing/selftests/drivers/gpu/drm_fabric/config "O=$O"
+ make -j"$(nproc)" "O=$O" LOCALVERSION=-virtme
+ vng --run "$O" --user root -- \
+ env FABRIC_DIR="$PWD/$O/drivers/gpu/drm/fabric" \
+ make -C tools/testing/selftests TARGETS=drivers/gpu/drm_fabric run_tests
+
+Dependencies (Debian/Ubuntu): ``python3``, ``python3-yaml``,
+``qemu-system-x86``, ``virtme-ng`` (``pip install --user virtme-ng``).
\ No newline at end of file
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/check-spec-regen.sh b/tools/testing/selftests/drivers/gpu/drm_fabric/check-spec-regen.sh
new file mode 100755
index 000000000000..aaf67bd04862
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/check-spec-regen.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+#
+# Check that every generated drm_fabric netlink artifact matches its spec.
+#
+# Documentation/netlink/specs/drm_fabric.yaml is authoritative for three
+# generated files that must not be hand-edited: the uAPI header and the
+# kernel-side policy and operation tables. Stale kernel tables still build and
+# pass every test here, because those tests exercise whatever family the tables
+# describe. Reconciliation is directory-scoped, so an artifact generated outside
+# those three directories is not visible. Host only; skips when the spec, a
+# generated file or the ynl generator (python3 + PyYAML) is missing.
+
+DIR="$(dirname "$(readlink -f "$0")")"
+
+. "${DIR}"/../../../kselftest/ktap_helpers.sh
+
+# Six levels up from this directory is the kernel tree root.
+KDIR="${KDIR:-$(readlink -f "${DIR}/../../../../../..")}"
+
+SPEC="Documentation/netlink/specs/drm_fabric.yaml"
+GEN="tools/net/ynl/pyynl/ynl_gen_c.py"
+[ -f "${KDIR}/${GEN}" ] || GEN="tools/net/ynl/ynl_gen_c.py"
+
+FAB="drivers/gpu/drm/fabric"
+UAPI_H="include/uapi/drm/drm_fabric.h"
+NL_C="${FAB}/drm_fabric_nl.c"
+NL_H="${FAB}/drm_fabric_nl.h"
+
+ktap_print_header
+
+if [ ! -f "${KDIR}/${SPEC}" ] || [ ! -f "${KDIR}/${GEN}" ] || \
+ [ ! -f "${KDIR}/${UAPI_H}" ] || [ ! -f "${KDIR}/${NL_C}" ] || \
+ [ ! -f "${KDIR}/${NL_H}" ]; then
+ ktap_skip_all "drm_fabric spec, a generated file or the ynl generator is missing (set KDIR)"
+ exit "${KSFT_SKIP}"
+fi
+
+if ! command -v python3 >/dev/null 2>&1 || ! python3 -c 'import yaml' 2>/dev/null; then
+ ktap_skip_all "python3 with PyYAML is required"
+ exit "${KSFT_SKIP}"
+fi
+
+# An unwritable tmpdir is an environment limit, not a mismatch: skip.
+if ! tmp=$(mktemp -d 2>/dev/null); then
+ ktap_skip_all "no writable temporary directory"
+ exit "${KSFT_SKIP}"
+fi
+trap 'rm -rf "${tmp}"' EXIT
+
+ktap_set_plan 4
+
+# A generated file carries both a YNL-GEN banner and this spec's path.
+for f in "${UAPI_H}" "${NL_C}" "${NL_H}"; do
+ printf '%s\n' "${f}"
+done | sort >"${tmp}/declared"
+
+sed 's|/[^/]*$||' "${tmp}/declared" | sort -u >"${tmp}/dirs"
+
+: >"${tmp}/found"
+while read -r d; do
+ for f in "${KDIR}/${d}"/*.c "${KDIR}/${d}"/*.h; do
+ [ -f "${f}" ] || continue
+ grep -q '^/\* YNL-GEN ' "${f}" || continue
+ grep -qF -- "${SPEC}" "${f}" || continue
+ printf '%s\n' "${f#"${KDIR}/"}"
+ done
+done <"${tmp}/dirs" | sort >"${tmp}/found"
+
+if diff -u "${tmp}/declared" "${tmp}/found" >"${tmp}/diff"; then
+ ktap_test_pass "generated artifacts in the tree are the ones checked here"
+else
+ sed 's/^/# /' "${tmp}/diff"
+ ktap_print_msg "a file generated from ${SPEC} is not on this check's list"
+ ktap_test_fail "generated artifacts in the tree are the ones checked here"
+fi
+
+# Run from KDIR with a relative spec path so banner and guard match.
+check_generated()
+{
+ committed="$1"
+ mode="$2"
+ kind="$3"
+ name="$4"
+ # Includes derive from the basename; give each its own directory.
+ out_dir="${tmp}/${mode}-${kind}"
+ out="${out_dir}/$(basename "${committed}")"
+ mkdir -p "${out_dir}"
+
+ if ! ( cd "${KDIR}" && python3 "${GEN}" --mode "${mode}" --"${kind}" \
+ --spec "${SPEC}" -o "${out}" ) 2>"${tmp}/err"; then
+ sed 's/^/# /' "${tmp}/err"
+ ktap_test_fail "${name}"
+ return
+ fi
+
+ if diff -u "${KDIR}/${committed}" "${out}" >"${tmp}/diff"; then
+ ktap_test_pass "${name}"
+ else
+ sed 's/^/# /' "${tmp}/diff"
+ ktap_print_msg "regenerate with: tools/net/ynl/ynl-regen.sh -f"
+ ktap_test_fail "${name}"
+ fi
+}
+
+check_generated "${UAPI_H}" uapi header \
+ "drm_fabric uAPI header matches netlink spec"
+check_generated "${NL_C}" kernel source \
+ "drm_fabric netlink ops and policy match netlink spec"
+check_generated "${NL_H}" kernel header \
+ "drm_fabric netlink kernel header matches netlink spec"
+
+ktap_finished
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/config b/tools/testing/selftests/drivers/gpu/drm_fabric/config
new file mode 100644
index 000000000000..6eaab8a7d771
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/config
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0
+# Kernel config fragment required to run the drm_fabric kselftests.
+# Merge with: scripts/kconfig/merge_config.sh or make kselftest-merge.
+CONFIG_NET=y
+CONFIG_DRM=y
+CONFIG_DEBUG_FS=y
+CONFIG_DRM_FABRIC=m
+CONFIG_DRM_FABRIC_SIM=m
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/dump_intr_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/dump_intr_abi.py
new file mode 100755
index 000000000000..0f913c38bbc3
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/dump_intr_abi.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Tests NLM_F_DUMP_INTR when a topology change lands mid-dump or after the
+last entry. pyynl decodes attributes but never surfaces nlmsg_flags, so
+this talks raw Generic Netlink.
+
+Needs drm_fabric + drm_fabric_sim, fabricsim debugfs (bulk_add), and root.
+"""
+
+import glob
+import os
+import re
+import socket
+import struct
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L # noqa: E402 - shared KTAP/module helpers (no pyynl)
+
+# --- Netlink / generic-netlink constants (cf. nl_policy_probe.py) ---------
+
+NETLINK_GENERIC = 16
+NLMSG_ERROR = 0x2
+NLMSG_DONE = 0x3
+NLM_F_REQUEST = 0x01
+NLM_F_MULTI = 0x02
+NLM_F_DUMP = 0x300
+NLM_F_DUMP_INTR = 0x10
+NLMSG_HDRLEN = 16
+GENL_HDRLEN = 4
+CTRL_ID = 0x10
+CTRL_CMD_GETFAMILY = 3
+CTRL_ATTR_FAMILY_NAME = 2
+CTRL_ATTR_FAMILY_ID = 1
+
+# Population large enough that ENDPOINT_GET spans several dump skbs (so there is
+# a between-batch window to mutate). Overridable for slow/fast machines.
+SCALE = int(os.environ.get("DUMP_INTR_SCALE", "600"))
+
+
+def _align4(n):
+ return (n + 3) & ~3
+
+
+def _nla(atype, payload):
+ length = 4 + len(payload)
+ pad = b"\x00" * (_align4(length) - length)
+ return struct.pack("=HH", length, atype) + payload + pad
+
+
+def _msg(family_id, cmd, seq, flags, payload=b""):
+ body = struct.pack("=BBH", cmd, 1, 0) + payload
+ total = NLMSG_HDRLEN + len(body)
+ return struct.pack("=IHHII", total, family_id, flags, seq, 0) + body
+
+
+def _open():
+ s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
+ s.bind((0, 0))
+ s.settimeout(5)
+ return s
+
+
+def _resolve_family(sock, name):
+ sock.send(_msg(CTRL_ID, CTRL_CMD_GETFAMILY, 1, NLM_F_REQUEST,
+ _nla(CTRL_ATTR_FAMILY_NAME, name + b"\x00")))
+ data = sock.recv(8192)
+ (_, mtype, _, _, _) = struct.unpack_from("=IHHII", data, 0)
+ if mtype == NLMSG_ERROR:
+ return None
+ off = NLMSG_HDRLEN + GENL_HDRLEN
+ while off + 4 <= len(data):
+ (alen, atype) = struct.unpack_from("=HH", data, off)
+ if alen < 4:
+ break
+ if atype == CTRL_ATTR_FAMILY_ID:
+ # CTRL_ATTR_FAMILY_ID is a u16; tolerate a u32 encoding too.
+ if alen >= 8:
+ return struct.unpack_from("=I", data, off + 4)[0]
+ if alen >= 6:
+ return struct.unpack_from("=H", data, off + 4)[0]
+ off += _align4(alen)
+ return None
+
+
+def _cmd_id(wanted, fallback):
+ """Resolve @wanted from the generated uAPI drm_fabric_cmd enum."""
+ try:
+ text = open(L.UAPI_HEADER).read()
+ m = re.search(r"enum\s+drm_fabric_cmd\s*\{(.*?)\}", text, re.S)
+ if m:
+ n = 0
+ for raw in re.sub(r"/\*.*?\*/", "", m.group(1), flags=re.S).split(","):
+ item = raw.strip()
+ if not item:
+ continue
+ if "=" in item:
+ name, val = item.split("=", 1)
+ name, n = name.strip(), int(val.strip(), 0)
+ else:
+ name = item
+ if name == wanted:
+ return n
+ n += 1
+ except (OSError, ValueError):
+ pass
+ return fallback
+
+
+def _read_msgs(sock):
+ """Read one dump datagram as (msgs, timed_out): (nlmsg_type, nlmsg_flags)
+ pairs. A timeout is reported explicitly, never conflated with a real
+ NLMSG_DONE.
+ """
+ try:
+ data = sock.recv(65536)
+ except socket.timeout:
+ return [], True
+ msgs, off = [], 0
+ while off + NLMSG_HDRLEN <= len(data):
+ (mlen, mtype, mflags, _, _) = struct.unpack_from("=IHHII", data, off)
+ if mlen < NLMSG_HDRLEN:
+ break
+ msgs.append((mtype, mflags))
+ off += _align4(mlen)
+ return msgs, False
+
+
+def _read_batch(sock):
+ """Read one dump datagram as (list_of_flags, saw_done, saw_error, timed_out)."""
+ msgs, timed_out = _read_msgs(sock)
+ if timed_out:
+ return [], False, False, True
+ return ([f for (_, f) in msgs],
+ any(t == NLMSG_DONE for (t, _) in msgs),
+ any(t == NLMSG_ERROR for (t, _) in msgs),
+ False)
+
+
+def _dump(sock, fam, cmd, mutate_after_first=None):
+ """Run an ENDPOINT_GET dump batch-by-batch; returns (batches, intr_seen,
+ err_seen, saw_done, timed_out). @mutate_after_first, if given, runs
+ once between the first and second batch.
+ """
+ sock.send(_msg(fam, cmd, 2, NLM_F_REQUEST | NLM_F_DUMP))
+ batches, intr, err, done, timed_out, mutated = 0, False, False, False, False, False
+ while not done:
+ flags, done, e, to = _read_batch(sock)
+ if to:
+ timed_out = True
+ break
+ if not flags:
+ break
+ batches += 1
+ err = err or e
+ if any(f & NLM_F_DUMP_INTR for f in flags):
+ intr = True
+ if mutate_after_first and not mutated:
+ mutate_after_first()
+ mutated = True
+ if batches > 10000: # runaway guard
+ break
+ return batches, intr, err, done, timed_out
+
+
+class Cfg:
+ def __init__(self, fam, cmd, stats_cmd):
+ self.fam = fam
+ self.cmd = cmd
+ self.stats_cmd = stats_cmd
+
+
+def test_multi_skb_dump_available(ksft, cfg):
+ """Precondition: the population makes ENDPOINT_GET span >1 dump batch."""
+ s = _open()
+ batches, _, err, done, to = _dump(s, cfg.fam, cfg.cmd)
+ s.close()
+ ok = ksft.check(batches >= 2 and not err and done and not to,
+ "dump-spans-multiple-batches",
+ "batches=%d err=%s done=%s timeout=%s (raise DUMP_INTR_SCALE)"
+ % (batches, err, done, to))
+ if not ok:
+ cfg.abort = True # the INTR cases below are meaningless single-batch
+
+
+def test_no_intr_when_quiescent(ksft, cfg):
+ """A quiescent dump must complete with a real NLMSG_DONE (not a socket
+ timeout) and no NLM_F_DUMP_INTR; a stalled dump is a failure, not a
+ silent pass.
+ """
+ s = _open()
+ batches, intr, err, done, to = _dump(s, cfg.fam, cfg.cmd)
+ s.close()
+ ksft.check(done and not to and not intr and not err, "quiescent-dump-no-intr",
+ "done=%s timeout=%s intr=%s err=%s batches=%d"
+ % (done, to, intr, err, batches))
+
+
+def _find_oper_state():
+ """A fabricsim per-port oper_state debugfs knob, if any (baseline ports)."""
+ m = glob.glob(os.path.join(L.DEBUGFS, "*", "*", "oper_state"))
+ return m[0] if m else None
+
+
+def test_intr_on_mutation_mid_dump(ksft, cfg):
+ """A base_seq bump during a multi-skb dump must raise NLM_F_DUMP_INTR.
+
+ oper_state writes are synchronous, unlike bulk_add, so the generation
+ changes before netlink's one-skb-ahead prefill snapshots it.
+ """
+ oper = _find_oper_state()
+ if not oper:
+ ksft.skip("mutation-mid-dump-sets-intr", "no fabricsim oper_state knob")
+ return
+ rel = os.path.relpath(oper, L.DEBUGFS)
+ states = ("active", "degraded")
+
+ s = _open()
+ s.send(_msg(cfg.fam, cfg.cmd, 3, NLM_F_REQUEST | NLM_F_DUMP))
+ intr = err = timed_out = False
+ batches = i = 0
+ done = False
+ while not done:
+ flags, done, e, to = _read_batch(s)
+ if to:
+ timed_out = True
+ break
+ if not flags:
+ break
+ batches += 1
+ err = err or e
+ if any(f & NLM_F_DUMP_INTR for f in flags):
+ intr = True
+ # Synchronous generation bump between batches (state must change to
+ # take effect, so alternate the two values).
+ try:
+ L.dbg_write(rel, states[i % 2])
+ i += 1
+ except OSError:
+ pass
+ if batches > 10000:
+ break
+ s.close()
+
+ # The dump must both observe the interruption and still terminate cleanly
+ # (a real NLMSG_DONE, not a stall).
+ ksft.check(intr and done and not timed_out and not err,
+ "mutation-mid-dump-sets-intr",
+ "intr=%s done=%s timeout=%s err=%s batches=%d"
+ % (intr, done, timed_out, err, batches))
+
+
+def test_intr_on_post_exhaustion_mutation(ksft, cfg):
+ """A topology change after the final entry must still be reported on
+ NLMSG_DONE.
+
+ A dump handler that samples the generation only once it has a record in
+ hand leaves a hole: a batch that finds the cursor already exhausted emits
+ nothing, so it never samples, and NLMSG_DONE goes out carrying the
+ generation from the previous batch.
+ """
+ s = _open()
+ s.send(_msg(cfg.fam, cfg.stats_cmd, 4, NLM_F_REQUEST | NLM_F_DUMP))
+
+ name = "post-exhaustion-mutation-sets-intr"
+ try:
+ msgs, timed_out = _read_msgs(s)
+ entries = sum(1 for (t, _) in msgs if t == cfg.fam)
+ if timed_out or not entries or any(t == NLMSG_DONE for (t, _) in msgs):
+ ksft.skip(name, "PORT_STATS_GET did not park mid-dump "
+ "(entries=%d timeout=%s)" % (entries, timed_out))
+ return
+
+ # Retire everything ahead of the cursor while the dump is parked. The
+ # dump only advances when we read, so settling here cannot let it run
+ # past the mutation.
+ try:
+ L.dbg_write("bulk_del", 0)
+ except OSError as exc:
+ ksft.skip(name, "bulk_del failed: %s" % exc)
+ return
+ L.settle(0.3)
+
+ # Netlink prefills one skb ahead, so the next read still delivers
+ # entries serialized before the mutation. Those carry the old
+ # generation and leave the pending inconsistency untouched.
+ done_flags, entry_intr, err = None, False, False
+ for _ in range(10000):
+ msgs, timed_out = _read_msgs(s)
+ if timed_out or not msgs:
+ break
+ for (mtype, mflags) in msgs:
+ if mtype == cfg.fam:
+ entry_intr = entry_intr or bool(mflags & NLM_F_DUMP_INTR)
+ elif mtype == NLMSG_ERROR:
+ err = True
+ elif mtype == NLMSG_DONE:
+ done_flags = mflags
+ if done_flags is not None:
+ break
+
+ # An entry that already carried the flag means the interruption was
+ # reported mid-dump and the consistency check reset with it, so
+ # NLMSG_DONE need not repeat it. That is the mid-dump path, covered by
+ # the case above, and it cannot stand in for this one.
+ if entry_intr:
+ ksft.skip(name, "interruption reported on an entry; the "
+ "exhaustion path was not isolated")
+ return
+
+ ksft.check(done_flags is not None and
+ bool(done_flags & NLM_F_DUMP_INTR) and not err, name,
+ "done_flags=%s err=%s"
+ % ("none" if done_flags is None else hex(done_flags), err))
+ finally:
+ s.close()
+ # Restore the population for whatever runs next.
+ try:
+ L.dbg_write("bulk_add", SCALE)
+ except OSError:
+ pass
+ L.settle(0.3)
+
+
+CASES = (
+ test_multi_skb_dump_available,
+ test_no_intr_when_quiescent,
+ test_intr_on_mutation_mid_dump,
+ test_intr_on_post_exhaustion_mutation,
+)
+
+
+def main():
+ ksft = L.Ksft()
+
+ with L.fabricsim(ksft, need_debugfs=True, need_control="bulk_add",
+ open_family=False):
+ sock = _open()
+ fam = _resolve_family(sock, L.FAMILY.encode())
+ sock.close()
+ if not fam:
+ ksft.skip_all("cannot resolve drm-fabric genl family")
+
+ # Grow the population so the dump pages across several skbs.
+ try:
+ L.dbg_write("bulk_add", SCALE)
+ except OSError as exc:
+ ksft.skip_all("bulk_add failed: %s" % exc)
+ L.settle(0.3)
+
+ cfg = Cfg(fam,
+ _cmd_id("DRM_FABRIC_CMD_ENDPOINT_GET", 2),
+ _cmd_id("DRM_FABRIC_CMD_PORT_STATS_GET", 4))
+ L.run_cases(ksft, cfg, CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/dump_scale_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/dump_scale_abi.py
new file mode 100755
index 000000000000..fedf35f51eff
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/dump_scale_abi.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Forces netlink dump pagination (cb->args) with fabricsim's bulk_add/bulk_del,
+past what the small object counts in other suites would ever trigger, and
+verifies every dump returns the full set exactly once, including under
+concurrent churn.
+
+Requires drm_fabric + drm_fabric_sim with fabricsim debugfs; run as root.
+"""
+
+import os
+import sys
+import threading
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L
+
+# Enough endpoints that the ENDPOINT_GET reply spans several skbs (each entry
+# carries id/fabric-id/fabric-ep-id/name/dev-name/bus-name/admin nests).
+SCALE = int(os.environ.get("DUMP_SCALE", "300"))
+
+
+def ep_ids(fab):
+ return [e["endpoint"]["endpoint-id"] for e in fab.dump("endpoint-get", {})]
+
+
+def port_count(fab):
+ return len(list(fab.dump("port-get", {})))
+
+
+class Cfg:
+ """Carries the pre-scale baseline counts so the teardown case can assert
+ the population is restored exactly."""
+
+ def __init__(self, fab, nl_error, base_ids, base_ports, fid):
+ self.fab = fab
+ self.NlError = nl_error
+ self.base_ids = base_ids
+ self.base_n = len(base_ids)
+ self.base_ports = base_ports
+ self.fid = fid
+
+
+def test_scale_dump(ksft, cfg):
+ fab = cfg.fab
+ try:
+ L.dbg_write("bulk_add", SCALE)
+ except OSError as exc:
+ ksft.skip_all("bulk_add failed: %s" % exc)
+ L.wait_until(lambda: len(ep_ids(fab)) >= cfg.base_n + SCALE, timeout=10)
+
+ ids = ep_ids(fab)
+ ksft.check(len(ids) == cfg.base_n + SCALE, "scale-endpoint-dump-count",
+ "want %d got %d" % (cfg.base_n + SCALE, len(ids)))
+ ksft.check(len(set(ids)) == len(ids), "scale-endpoint-dump-unique",
+ "%d dups" % (len(ids) - len(set(ids))))
+ ksft.check(set(cfg.base_ids).issubset(set(ids)),
+ "scale-endpoint-dump-keeps-baseline")
+
+ pc = port_count(fab)
+ ksft.check(pc == cfg.base_ports + SCALE, "scale-port-dump-count",
+ "want %d got %d" % (cfg.base_ports + SCALE, pc))
+
+ if cfg.fid is not None:
+ flt = [e["endpoint"] for e in fab.dump("endpoint-get",
+ {"fabric-id": cfg.fid})]
+ ksft.check(all(e.get("fabric-id") == cfg.fid for e in flt) and
+ len(flt) >= SCALE, "scale-endpoint-dump-filtered",
+ "got %d members" % len(flt))
+ else:
+ ksft.skip("scale-endpoint-dump-filtered", "fabricsim fabric absent")
+
+
+def test_dump_consistency_under_churn(ksft, cfg):
+ """Hammer multi-skb dumps while a helper thread churns the population."""
+ fab = cfg.fab
+ pre_churn = len(ep_ids(fab))
+ L.dbg_write("bulk_add", SCALE)
+ L.wait_until(lambda: len(ep_ids(fab)) > pre_churn, timeout=10)
+ stop = threading.Event()
+ churn_err = []
+
+ def churn():
+ while not stop.is_set():
+ try:
+ L.dbg_write("bulk_del", 0)
+ # Re-check before re-populating so the last iteration does not
+ # add a fresh SCALE population that would race the teardown.
+ if stop.is_set():
+ break
+ L.dbg_write("bulk_add", SCALE)
+ except OSError:
+ # racy debugfs writes may transiently fail; not a dump bug
+ pass
+ except Exception as exc: # noqa: BLE001
+ churn_err.append(str(exc))
+ return
+
+ worst_dups = 0
+ dump_err = None
+ # The join below is the real synchronization point; the case aborts if it
+ # times out. Mark the worker daemon so a wedged iteration cannot also hang
+ # interpreter shutdown after that failure has already been reported.
+ t = threading.Thread(target=churn, daemon=True)
+ t.start()
+ try:
+ for _ in range(60):
+ d = ep_ids(fab)
+ worst_dups = max(worst_dups, len(d) - len(set(d)))
+ except Exception as exc: # noqa: BLE001
+ dump_err = str(exc)
+ finally:
+ stop.set()
+ t.join(timeout=30) # each churn iteration is bounded
+
+ # A worker that will not terminate is a harness failure, not something to
+ # leave running into the next case; abort so the teardown case cannot race
+ # an in-flight bulk_add.
+ if not ksft.check(not t.is_alive(), "dump-churn-worker-terminates",
+ "churn worker still alive after join"):
+ cfg.abort = True
+ return
+
+ # Assert *no duplicates* (the invariant a broken cb->args resume would
+ # violate), not *no omissions*: under concurrent churn the population
+ # legitimately changes between skbs, so a missing id is expected here and
+ # only a duplicated id signals a dump-resume bug.
+ ksft.check(worst_dups == 0 and dump_err is None and not churn_err,
+ "dump-consistency-no-dup-under-churn",
+ "dups=%d dump_err=%s churn_err=%s"
+ % (worst_dups, dump_err, churn_err[:1]))
+
+ # Worker has exited: drain the churn population back to baseline so the
+ # teardown case (and the next suite) starts from a known, quiescent count.
+ L.dbg_write("bulk_del", 0)
+ L.wait_until(lambda: len(ep_ids(fab)) == cfg.base_n, timeout=10)
+
+
+def test_scale_teardown(ksft, cfg):
+ fab = cfg.fab
+ try:
+ L.dbg_write("bulk_del", 1)
+ except OSError as exc:
+ ksft.not_ok("scale-teardown", "bulk_del failed: %s" % exc)
+ return
+ L.wait_until(lambda: len(ep_ids(fab)) == cfg.base_n, timeout=10)
+ ksft.check(len(ep_ids(fab)) == cfg.base_n, "scale-teardown-restores-baseline",
+ "want %d got %d" % (cfg.base_n, len(ep_ids(fab))))
+
+
+CASES = (
+ test_scale_dump,
+ test_dump_consistency_under_churn,
+ test_scale_teardown,
+)
+
+
+def main():
+ ksft = L.Ksft()
+ _, NlError = L.import_ynl()
+
+ with L.fabricsim(ksft, need_debugfs=True, need_control="bulk_add") as fab:
+ base_ids = ep_ids(fab)
+ base_ports = port_count(fab)
+ fid = None
+ for f in fab.dump("fabric-get", {}):
+ if f["fabric"]["name"] == "fabricsim":
+ fid = f["fabric"]["fabric-id"]
+ break
+
+ L.run_cases(ksft, Cfg(fab, NlError, base_ids, base_ports, fid), CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/fabric_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/fabric_abi.py
new file mode 100755
index 000000000000..94cc1078a365
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/fabric_abi.py
@@ -0,0 +1,401 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Full drm-fabric genetlink ABI coverage via YNL against fabricsim: every
+command (do + dump) and event, asserted on decoded reply dicts so checks
+are immune to CLI text changes.
+
+Usage: fabric_abi.py [--no-load] (--no-load: modules already loaded)
+"""
+
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L
+
+EVT_DURATION = float(os.environ.get("EVT_DURATION", "3"))
+# Multicast subscription is synchronous (setsockopt), so a brief settle before
+# triggering is enough; wait_ntf() then polls with a deadline for the arrival.
+EVT_SETTLE = float(os.environ.get("EVT_SETTLE", "0.2"))
+
+
+def _unload_providers():
+ L.rmmod("drm_fabric_sim")
+ L.rmmod("drm_fabric")
+
+
+def fabric_names(fab):
+ return [e["fabric"]["name"] for e in fab.dump("fabric-get", {})]
+
+
+def ep_list(fab, filt=None):
+ return [e["endpoint"] for e in fab.dump("endpoint-get", filt or {})]
+
+
+def port_list(fab, ep_id):
+ return [p["port"] for p in fab.dump("port-get", {"endpoint-id": ep_id})]
+
+
+def peers_in(ports):
+ return [p["peer"]["peer-id"] for p in ports if "peer" in p]
+
+
+def _reload_sim(shape):
+ L.rmmod("drm_fabric_sim")
+ L.insmod("drm-fabric-sim.ko", "topology=%s" % shape)
+ L.wait_until(lambda: L.module_loaded("drm_fabric_sim"))
+
+
+class Cfg:
+ def __init__(self, fab, fid, dfs, no_load, nl_error):
+ self.fab = fab
+ self.fid = fid
+ self.dfs = dfs
+ self.no_load = no_load
+ self.NlError = nl_error
+
+
+def test_fabric_get(ksft, cfg):
+ fab = cfg.fab
+ ksft.check("fabricsim" in fabric_names(fab), "fabric-get-dump")
+ f = fab.do("fabric-get", {"fabric-id": cfg.fid})
+ ksft.check(f["fabric"]["name"] == "fabricsim", "fabric-get-do")
+
+
+def test_endpoint_get_dump(ksft, cfg):
+ fab = cfg.fab
+ eps = ep_list(fab)
+ ksft.check(len(eps) == 4, "endpoint-get-dump", "got %d" % len(eps))
+ epf = ep_list(fab, {"fabric-id": cfg.fid})
+ ksft.check(len(epf) == 4, "endpoint-get-dump-filtered", "got %d" % len(epf))
+
+
+def test_endpoint_get_do(ksft, cfg):
+ fab = cfg.fab
+ e0 = fab.do("endpoint-get", {"endpoint-id": 0})
+ ksft.check(e0["endpoint"]["name"] == "sim-ep0", "endpoint-get-do")
+ e = fab.do("endpoint-get", {"dev-name": "fabricsim.0"})
+ ksft.check(e["endpoint"]["name"] == "sim-ep0", "endpoint-get-do-by-dev-name")
+ e = fab.do("endpoint-get", {"dev-name": "fabricsim.1", "bus-name": "platform"})
+ ksft.check(e["endpoint"]["name"] == "sim-ep1",
+ "endpoint-get-do-by-dev-name-bus")
+ e = fab.do("endpoint-get", {"endpoint-id": 0, "dev-name": "fabricsim.0"})
+ ksft.check(e["endpoint"]["name"] == "sim-ep0",
+ "endpoint-get-do-id-and-dev-name-agree")
+
+
+def test_endpoint_get_do_errors(ksft, cfg):
+ fab, NlError = cfg.fab, cfg.NlError
+ try:
+ fab.do("endpoint-get", {"endpoint-id": 0, "dev-name": "fabricsim.1"})
+ ksft.not_ok("endpoint-get-do-id-dev-name-conflict-einval", "accepted")
+ except NlError as exc:
+ ksft.check(L.nl_errno(exc) == 22, # EINVAL
+ "endpoint-get-do-id-dev-name-conflict-einval",
+ "errno=%d" % L.nl_errno(exc))
+ try:
+ fab.do("endpoint-get", {"bus-name": "platform"})
+ ksft.not_ok("endpoint-get-do-bus-name-only-einval", "accepted")
+ except NlError as exc:
+ ksft.check(L.nl_errno(exc) == 22, "endpoint-get-do-bus-name-only-einval",
+ "errno=%d" % L.nl_errno(exc))
+ try:
+ fab.do("endpoint-get", {"dev-name": "nope.99"})
+ ksft.not_ok("endpoint-get-do-dev-name-enoent", "accepted")
+ except NlError as exc:
+ ksft.check(L.nl_errno(exc) == 2, "endpoint-get-do-dev-name-enoent",
+ "errno=%d" % L.nl_errno(exc))
+
+
+def test_port_get(ksft, cfg):
+ fab = cfg.fab
+ p0 = port_list(fab, 0)
+ ksft.check(len(p0) == 4, "port-get-dump", "got %d" % len(p0))
+ port = fab.do("port-get", {"endpoint-id": 0, "port-index": 0})["port"]
+ ksft.check(port["oper-state"] == "active" and "peer" in port, "port-get-do")
+ ksft.check(port.get("peer", {}).get("peer-id") == 257,
+ "port-get-peer-data")
+
+
+def test_port_stats(ksft, cfg):
+ fab = cfg.fab
+ st = fab.do("port-stats-get", {"endpoint-id": 0, "port-index": 0})["port-stats"]
+ ksft.check(st.get("read-bytes", -1) == 0 and st.get("write-bytes", -1) == 0,
+ "port-stats-get-do")
+ stats = fab.dump("port-stats-get", {"endpoint-id": 0})
+ ksft.check(len(stats) == 4, "port-stats-get-dump", "got %d" % len(stats))
+
+
+def test_activity_stats(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("activity-stats-increment", "debugfs not available")
+ return
+ fab = cfg.fab
+ L.dbg_write("ep0/port0/read_rate", 1024)
+ L.dbg_write("ep0/port0/write_rate", 512)
+ L.dbg_write("ep0/port0/activity_enable", 1)
+
+ def _stats():
+ return fab.do("port-stats-get",
+ {"endpoint-id": 0, "port-index": 0})["port-stats"]
+
+ L.wait_until(lambda: _stats().get("read-bytes", 0) > 0 and
+ _stats().get("write-bytes", 0) > 0)
+ L.dbg_write("ep0/port0/activity_enable", 0)
+ s = _stats()
+ ksft.check(s.get("read-bytes", 0) > 0 and s.get("write-bytes", 0) > 0,
+ "activity-stats-increment",
+ "read=%s write=%s" % (s.get("read-bytes"), s.get("write-bytes")))
+
+
+def test_inject_link_down(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("inject-link-down-oper-inactive", "debugfs not available")
+ return
+ fab = cfg.fab
+ L.dbg_write("ep1/port0/inject", "link_down")
+ p = fab.do("port-get", {"endpoint-id": 1, "port-index": 0})["port"]
+ ksft.check(p["oper-state"] == "inactive", "inject-link-down-oper-inactive",
+ "oper=%s" % p["oper-state"])
+ L.dbg_write("ep1/port0/inject", "recover_to_active")
+
+
+def test_oper_state_degraded(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("debugfs-oper-state-degraded", "debugfs not available")
+ return
+ fab = cfg.fab
+ L.dbg_write("ep2/port0/oper_state", "degraded")
+ p = fab.do("port-get", {"endpoint-id": 2, "port-index": 0})["port"]
+ ksft.check(p["oper-state"] == "degraded", "debugfs-oper-state-degraded",
+ "oper=%s" % p["oper-state"])
+ L.dbg_write("ep2/port0/oper_state", "active")
+
+
+def test_topology_kn(ksft, cfg):
+ fab = cfg.fab
+ peers = set(peers_in(port_list(fab, 0)))
+ ksft.check(len(peers) == 3, "topology-kn-distinct-peers",
+ "distinct peers=%d" % len(peers))
+ p = fab.do("port-get", {"endpoint-id": 1, "port-index": 0})["port"]
+ ksft.check(p.get("peer", {}).get("peer-id") == 256,
+ "topology-kn-bidirectional")
+ p = fab.do("port-get", {"endpoint-id": 0, "port-index": 3})["port"]
+ ksft.check("peer" not in p, "port-no-peer")
+
+
+def test_counters_stop(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("counters-stop-after-disable", "debugfs not available")
+ return
+ fab = cfg.fab
+
+ def _rb():
+ return fab.do("port-stats-get",
+ {"endpoint-id": 0, "port-index": 0})["port-stats"]["read-bytes"]
+
+ r1 = _rb()
+ # Proving a *non-event* (counters must NOT advance after disable) needs a
+ # real wait; poll a bounded window and assert the value never moved.
+ moved = L.wait_until(lambda: _rb() != r1, timeout=0.5)
+ ksft.check(not moved, "counters-stop-after-disable",
+ "read-bytes moved from %s to %s" % (r1, _rb()))
+
+
+def test_port_state_cycle(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("port-state-inject-cycle", "debugfs not available")
+ return
+ fab = cfg.fab
+ L.dbg_write("ep2/port1/oper_state", "active")
+ L.dbg_write("ep2/port1/inject", "degrade")
+ s1 = fab.do("port-get", {"endpoint-id": 2, "port-index": 1})["port"]["oper-state"]
+ L.dbg_write("ep2/port1/inject", "link_down")
+ s2 = fab.do("port-get", {"endpoint-id": 2, "port-index": 1})["port"]["oper-state"]
+ L.dbg_write("ep2/port1/inject", "recover_to_active")
+ s3 = fab.do("port-get", {"endpoint-id": 2, "port-index": 1})["port"]["oper-state"]
+ ksft.check(s1 == "degraded" and s2 == "inactive" and s3 == "active",
+ "port-state-inject-cycle", "%s,%s,%s" % (s1, s2, s3))
+
+
+def test_port_change_ntf(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("port-change-ntf-notification", "debugfs not available")
+ return
+ fab = cfg.fab
+ L.dbg_write("ep1/port1/oper_state", "active")
+ ev = L.DrmFabric()
+ ev.ntf_subscribe(L.MCAST_MONITOR)
+ L.settle(EVT_SETTLE)
+ L.dbg_write("ep1/port1/oper_state", "degraded")
+ got = L.wait_ntf(ev, "port-change-ntf", timeout=EVT_DURATION,
+ match=lambda n: n["msg"]["port"].get("oper-state") == "degraded")
+ ksft.check(got is not None, "port-change-ntf-notification")
+ # The event carries the post-change topology-generation (nonzero); a
+ # subsequent GET reports a generation that is >= the event's.
+ if got is not None:
+ egen = got["msg"].get("topology-generation")
+ ggen = fab.do("port-get",
+ {"endpoint-id": 1, "port-index": 1}).get("topology-generation")
+ ksft.check(egen is not None and egen != 0 and
+ ggen is not None and ggen >= egen,
+ "port-change-ntf-topology-generation",
+ "event=%s get=%s" % (egen, ggen))
+ L.dbg_write("ep1/port1/oper_state", "active")
+
+
+def test_linear_topology(ksft, cfg):
+ """Reload the sim into the linear topology and assert the chain shape.
+ Restores the default mesh K_4 on the way out (even on failure), so
+ this cannot cascade into later cases that assume the default topology.
+ """
+ if cfg.no_load:
+ ksft.skip("linear-topology-chain", "skipped with --no-load")
+ ksft.skip("linear-topology-adjacent-peers", "skipped with --no-load")
+ ksft.skip("linear-topology-end-no-extra-peer", "skipped with --no-load")
+ ksft.skip("linear-topology-debugfs-works",
+ "skipped with --no-load or no debugfs")
+ return
+ fab = cfg.fab
+ _reload_sim("linear")
+ try:
+ n0 = len(peers_in(port_list(fab, 0)))
+ n1 = len(peers_in(port_list(fab, 1)))
+ n3 = len(peers_in(port_list(fab, 3)))
+ ksft.check(n0 == 1 and n1 == 2 and n3 == 1, "linear-topology-chain",
+ "ep0=%d ep1=%d ep3=%d" % (n0, n1, n3))
+ ps = peers_in(port_list(fab, 0))
+ ksft.check(ps and ps[0] == 257, "linear-topology-adjacent-peers",
+ "ep0 peers=%s" % ps)
+ p = fab.do("port-get", {"endpoint-id": 0, "port-index": 1})["port"]
+ ksft.check("peer" not in p, "linear-topology-end-no-extra-peer")
+ if cfg.dfs:
+ L.dbg_write("ep0/port0/oper_state", "degraded")
+ p = fab.do("port-get", {"endpoint-id": 0, "port-index": 0})["port"]
+ ksft.check(p["oper-state"] == "degraded",
+ "linear-topology-debugfs-works")
+ L.dbg_write("ep0/port0/oper_state", "active")
+ else:
+ ksft.skip("linear-topology-debugfs-works",
+ "skipped with --no-load or no debugfs")
+ finally:
+ # Always return to the default mesh K_4 shape for the cases that follow.
+ _reload_sim("mesh")
+
+
+def test_reload_mesh(ksft, cfg):
+ """Defensively re-establish the default mesh K_N topology (idempotent)
+ and assert it, guaranteeing the precondition for the RAS/NTF cases
+ that follow even if an earlier reload failed.
+ """
+ if cfg.no_load:
+ ksft.skip("reload-mesh-topology-restored", "skipped with --no-load")
+ return
+ fab = cfg.fab
+ _reload_sim("mesh")
+ n0 = len(peers_in(port_list(fab, 0)))
+ ksft.check(n0 == 3, "reload-mesh-topology-restored", "ep0 peers=%d" % n0)
+
+
+def test_port_change_ntf_full(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("port-change-ntf-full-port-nest", "debugfs not available")
+ return
+ L.dbg_write("ep0/port0/oper_state", "active")
+ ev = L.DrmFabric()
+ ev.ntf_subscribe(L.MCAST_MONITOR)
+ L.settle(EVT_SETTLE)
+ L.dbg_write("ep0/port0/oper_state", "degraded")
+ got = L.wait_ntf(
+ ev, "port-change-ntf", timeout=EVT_DURATION,
+ match=lambda n: "endpoint-id" in n["msg"]["port"] and
+ "port-index" in n["msg"]["port"])
+ ksft.check(got is not None, "port-change-ntf-full-port-nest")
+ L.dbg_write("ep0/port0/oper_state", "active")
+
+
+def test_link_down_exact_count(ksft, cfg):
+ if not cfg.dfs:
+ ksft.skip("inject-link-down-exact-count", "debugfs not available")
+ return
+ fab = cfg.fab
+ base = fab.do("port-stats-get",
+ {"endpoint-id": 3, "port-index": 1})["port-stats"]
+ c0 = base.get("link-down-count", 0)
+ for _ in range(3):
+ L.dbg_write("ep3/port1/inject", "link_down")
+ s = fab.do("port-stats-get",
+ {"endpoint-id": 3, "port-index": 1})["port-stats"]
+ ksft.check(s.get("link-down-count", 0) == c0 + 3,
+ "inject-link-down-exact-count",
+ "expected %d got %s" % (c0 + 3, s.get("link-down-count")))
+ L.dbg_write("ep3/port1/inject", "recover_to_active")
+
+
+# Ordered scenario: each case builds on the topology/state left by the prior
+# one (e.g. the linear reload precedes its assertions, and the mesh reload
+# restores K_N for the stats cases). Keep this list in order.
+CASES = (
+ test_fabric_get,
+ test_endpoint_get_dump,
+ test_endpoint_get_do,
+ test_endpoint_get_do_errors,
+ test_port_get,
+ test_port_stats,
+ test_activity_stats,
+ test_inject_link_down,
+ test_oper_state_degraded,
+ test_topology_kn,
+ test_counters_stop,
+ test_port_state_cycle,
+ test_port_change_ntf,
+ test_linear_topology,
+ test_reload_mesh,
+ test_port_change_ntf_full,
+ test_link_down_exact_count,
+)
+
+
+def main():
+ ksft = L.Ksft()
+ _, NlError = L.import_ynl()
+
+ if not L.is_root():
+ ksft.skip_all("must run as root (genetlink + debugfs + insmod)")
+
+ no_load = "--no-load" in sys.argv[1:]
+
+ if not no_load:
+ L.rmmod("drm_fabric_sim")
+ L.rmmod("drm_fabric")
+ # Arm teardown before loading so a partial load is unwound, and so the
+ # topology reshapes this suite performs are restored even if the timeout
+ # killer sends SIGTERM (which a bare atexit would miss).
+ L.on_teardown(_unload_providers)
+ if not L.insmod("drm-fabric.ko") or not L.insmod("drm-fabric-sim.ko"):
+ ksft.skip_all("could not load drm_fabric + drm_fabric_sim modules")
+ L.wait_until(lambda: L.module_loaded("drm_fabric_sim"))
+
+ if not L.module_loaded("drm_fabric"):
+ ksft.skip_all("drm_fabric not loaded")
+ if not L.module_loaded("drm_fabric_sim"):
+ ksft.skip_all("drm_fabric_sim not loaded")
+
+ try:
+ fab = L.DrmFabric()
+ except (OSError, NlError) as exc:
+ ksft.skip_all("cannot open drm-fabric family: %s" % exc)
+
+ # fabric-id 0 is reserved; discover the live provider fabric id.
+ fabrics = fab.dump("fabric-get", {})
+ fid = fabrics[0]["fabric"]["fabric-id"] if fabrics else 1
+
+ cfg = Cfg(fab, fid, L.debugfs_available(), no_load, NlError)
+ L.run_cases(ksft, cfg, CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/fault_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/fault_abi.py
new file mode 100755
index 000000000000..8ea2d1de93d7
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/fault_abi.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Provider fault injection via fabricsim's fail_register debugfs toggle (cf.
+netdevsim's should_fail): a failed provider-driven endpoint create must
+surface the provider's errno and leak no endpoint, succeeding once the
+fault is cleared.
+
+Requires drm_fabric + drm_fabric_sim with fabricsim debugfs; run as root.
+"""
+
+import errno
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L
+
+
+def eps_by_name(fab):
+ return {e["endpoint"]["name"]: e["endpoint"]
+ for e in fab.dump("endpoint-get", {})}
+
+
+def slot_of(name):
+ return int(name.rsplit("ep", 1)[1])
+
+
+def add_via(fab, control, nports=1):
+ """Drive a debugfs hotplug-add; return the new endpoint dict (or None)."""
+ before = set(eps_by_name(fab))
+ L.dbg_write(control, nports)
+ new = L.wait_until(lambda: set(eps_by_name(fab)) - before)
+ return eps_by_name(fab)[next(iter(new))] if len(new) == 1 else None
+
+
+def del_via(fab, name, slot):
+ L.dbg_write("del_endpoint", slot)
+ L.wait_until(lambda: name not in eps_by_name(fab))
+
+
+def fabricsim_fid(fab):
+ for f in fab.dump("fabric-get", {}):
+ if f["fabric"]["name"] == "fabricsim":
+ return f["fabric"]["fabric-id"]
+ return None
+
+
+def set_fault(name, on):
+ L.dbg_write(name, "Y" if on else "N")
+
+
+class Cfg:
+ def __init__(self, fab, fid):
+ self.fab = fab
+ self.fid = fid
+
+
+def test_register_fault(ksft, cfg):
+ """A failed provider create surfaces -ENOMEM and leaks no endpoint."""
+ fab = cfg.fab
+ n_before = len(eps_by_name(fab))
+ set_fault("fail_register", True)
+ try:
+ reg_errno = None
+ try:
+ L.dbg_write("add_endpoint", 1)
+ except OSError as exc:
+ reg_errno = exc.errno
+ # Confirming a non-event needs a bounded wait: poll a short window for a
+ # late endpoint after the synchronous -ENOMEM.
+ grew = L.wait_until(lambda: len(eps_by_name(fab)) != n_before, timeout=0.3)
+ ksft.check(reg_errno == errno.ENOMEM, "fault-register-returns-enomem",
+ "errno=%s" % reg_errno)
+ ksft.check(not grew and len(eps_by_name(fab)) == n_before,
+ "fault-register-no-leak",
+ "count changed %d -> %d" % (n_before, len(eps_by_name(fab))))
+ finally:
+ set_fault("fail_register", False)
+ created = add_via(fab, "add_endpoint", nports=1)
+ ksft.check(created is not None, "fault-cleared-register-ok")
+ if created is not None:
+ del_via(fab, created["name"], slot_of(created["name"]))
+
+
+CASES = (
+ test_register_fault,
+)
+
+
+def main():
+ ksft = L.Ksft()
+
+ with L.fabricsim(ksft, need_debugfs=True, need_control="fail_register") as fab:
+ fid = fabricsim_fid(fab)
+ if fid is None:
+ ksft.skip_all("fabricsim fabric not present")
+
+ L.run_cases(ksft, Cfg(fab, fid), CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/harness_reset_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/harness_reset_abi.py
new file mode 100755
index 000000000000..211613185a39
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/harness_reset_abi.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Verify recovery after a SIGKILL-terminated test. reset_sim_or_fail() must
+restore topology isolation at the next test's entry.
+
+Requires root, YNL, drm_fabric, and drm_fabric_sim.
+"""
+
+import os
+import signal
+import subprocess
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L # noqa: E402 - shared KTAP/module helpers
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+
+# Helper process: load the switch shape, then idle so the parent can kill it
+# mid-life. It deliberately installs no cleanup -- SIGKILL would bypass it.
+_CHILD = (
+ "import sys, time\n"
+ "sys.path.insert(0, %r)\n"
+ "import lib_drm_fabric as L\n"
+ "L.rmmod('drm_fabric_sim')\n"
+ "if not L.module_loaded('drm_fabric'):\n"
+ " L.insmod('drm-fabric.ko')\n"
+ "L.insmod('drm-fabric-sim.ko', 'topology=switch')\n"
+ "time.sleep(120)\n"
+) % _HERE
+
+
+def _has_switch_peer(fab):
+ """True if any leaf port carries a TYPE=switch half-edge (switch shape)."""
+ for e in fab.dump("endpoint-get", {}):
+ ep_id = e["endpoint"]["endpoint-id"]
+ for p in fab.dump("port-get", {"endpoint-id": ep_id}):
+ peer = p["port"].get("peer")
+ if peer and peer.get("type") == "switch":
+ return True
+ return False
+
+
+class Cfg:
+ def __init__(self, nl_error):
+ self.NlError = nl_error
+
+
+def test_sigkill_topology_recovery(ksft, cfg):
+ """A SIGKILL-leaked switch shape must not survive the next entry reset."""
+ # 1. Bring up the switch shape in a helper and confirm it is observable.
+ child = subprocess.Popen([sys.executable, "-c", _CHILD])
+ try:
+ fab = L.DrmFabric()
+ loaded = L.wait_until(
+ lambda: L.module_loaded("drm_fabric_sim") and _has_switch_peer(fab),
+ timeout=10.0)
+ if not loaded:
+ child.send_signal(signal.SIGKILL)
+ ksft.skip("harness-reset-sigkill-recovery",
+ "helper could not establish switch shape")
+ return
+
+ # 2. Terminate through the SIGKILL path: no cleanup runs, so the switch
+ # sim stays loaded exactly as a hard-timed-out test would leave it.
+ child.send_signal(signal.SIGKILL)
+ child.wait()
+ finally:
+ if child.poll() is None:
+ child.send_signal(signal.SIGKILL)
+ child.wait()
+
+ stale = L.module_loaded("drm_fabric_sim")
+ ksft.check(stale, "harness-reset-sigkill-leaves-stale-sim",
+ "sim unexpectedly unloaded by the killed helper")
+
+ # 3. The next test's entry reset must recover a known default shape.
+ # Mid-case: a result was already emitted above, so a failed reset here
+ # must become a not_ok(), not a skip_all() 0-plan (invalid once results
+ # are on stdout). reset_sim_or_fail() already reported the failure, so
+ # bail out rather than emitting a second, precondition-less check.
+ if not L.reset_sim_or_fail(ksft, "harness-reset-sigkill-recovery-setup",
+ topology="mesh"):
+ return
+ fab = L.DrmFabric()
+ ksft.check(not _has_switch_peer(fab),
+ "harness-reset-sigkill-recovery",
+ "switch half-edge survived reset_sim(mesh)")
+
+
+CASES = (
+ test_sigkill_topology_recovery,
+)
+
+
+def main():
+ ksft = L.Ksft()
+ _, NlError = L.import_ynl()
+ if not L.is_root():
+ ksft.skip_all("must run as root (insmod + genetlink)")
+ # This suite drives module load/unload itself rather than via fabricsim().
+ try:
+ L.run_cases(ksft, Cfg(NlError), CASES)
+ finally:
+ # Leave a sane default shape for whatever suite runs next.
+ L.sim_restore_default()
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/hotplug_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/hotplug_abi.py
new file mode 100755
index 000000000000..19a3405fade9
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/hotplug_abi.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Endpoint hotplug via fabricsim's debugfs lifecycle controls (add_endpoint/
+del_endpoint, cf. netdevsim's new_port/del_port): CREATE/DELETE events
+observed over the read-only query ABI and notifications; only the hotplug
+stimulus uses the debugfs controls.
+
+Usage: hotplug_abi.py [--no-load]
+"""
+
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L
+
+EVT_DURATION = float(os.environ.get("EVT_DURATION", "3"))
+# Subscription is synchronous (setsockopt); a brief settle suffices before
+# triggering, after which wait_ntf() polls with a deadline.
+EVT_SETTLE = float(os.environ.get("EVT_SETTLE", "0.2"))
+
+
+def eps_by_name(fab):
+ return {e["endpoint"]["name"]: e["endpoint"]
+ for e in fab.dump("endpoint-get", {})}
+
+
+def fabricsim_fid(fab):
+ for f in fab.dump("fabric-get", {}):
+ if f["fabric"]["name"] == "fabricsim":
+ return f["fabric"]["fabric-id"]
+ return None
+
+
+def slot_of(name):
+ return int(name.rsplit("ep", 1)[1])
+
+
+def add_ep(fab, control, nports=None):
+ """Add via debugfs; diff the name set (polling) to return the new endpoint."""
+ before = set(eps_by_name(fab))
+ L.dbg_write(control, nports if nports is not None else 1)
+ new = L.wait_until(lambda: set(eps_by_name(fab)) - before)
+ if len(new) != 1:
+ return None
+ return eps_by_name(fab)[next(iter(new))]
+
+
+def del_ep(fab, slot, name):
+ L.dbg_write("del_endpoint", slot)
+ return L.wait_until(lambda: name not in eps_by_name(fab))
+
+
+class Cfg:
+ def __init__(self, fab, fid, nl_error):
+ self.fab = fab
+ self.fid = fid
+ self.NlError = nl_error
+
+def _gen(fab):
+ """Current global topology-generation, read via a stable initial port."""
+ return fab.do("port-get",
+ {"endpoint-id": 0, "port-index": 0}).get("topology-generation")
+
+
+def test_provider_topology_lifecycle(ksft, cfg):
+ """Provider grows and shrinks the topology within its fabric.
+
+ Non-destructive: only the endpoints it adds are removed.
+ """
+ fab = cfg.fab
+
+ # (a) provider-established initial adjacency (mesh) is observable read-only.
+ p0 = fab.do("port-get", {"endpoint-id": 0, "port-index": 0})["port"]
+ peer = p0.get("peer")
+ ksft.check(peer is not None and peer.get("type") == "accel",
+ "topology-initial-adjacency-visible", "peer=%s" % peer)
+
+ ev = L.DrmFabric()
+ ev.ntf_subscribe(L.MCAST_MONITOR)
+ L.settle(EVT_SETTLE)
+
+ n0 = len(eps_by_name(fab))
+ g0 = _gen(fab)
+
+ # (b) grow: add two endpoints; the first is asserted to announce a CREATE.
+ ep_c = add_ep(fab, "add_endpoint", nports=2)
+ c_evt = L.wait_ntf(ev, "endpoint-create-ntf", timeout=EVT_DURATION)
+ ep_d = add_ep(fab, "add_endpoint", nports=2)
+
+ if ep_c is None or ep_d is None:
+ for e in (ep_c, ep_d):
+ if e:
+ del_ep(fab, slot_of(e["name"]), e["name"])
+ for name in ("topology-grow-two-members", "topology-grow-create-ntf",
+ "topology-grow-advances-generation",
+ "topology-hotplug-is-member",
+ "topology-hotplug-endpoint-unlinked",
+ "topology-reads-do-not-advance-generation",
+ "topology-shrink-delete-ntf",
+ "topology-shrink-restores-baseline"):
+ ksft.not_ok(name, "grow failed (c=%s d=%s)" % (ep_c, ep_d))
+ return
+
+ ksft.check(len(eps_by_name(fab)) == n0 + 2, "topology-grow-two-members",
+ "n0=%d now=%d" % (n0, len(eps_by_name(fab))))
+ ksft.check(c_evt is not None, "topology-grow-create-ntf")
+
+ g_grown = _gen(fab)
+ ksft.check(g0 is not None and g_grown is not None and g_grown > g0,
+ "topology-grow-advances-generation",
+ "g0=%s grown=%s" % (g0, g_grown))
+ ksft.check(ep_c.get("fabric-id") == cfg.fid, "topology-hotplug-is-member",
+ "fabric-id=%s" % ep_c.get("fabric-id"))
+
+ # (c) a late arrival is not auto-wired: the provider links explicitly.
+ pc = fab.do("port-get",
+ {"endpoint-id": ep_c["endpoint-id"], "port-index": 0})["port"]
+ ksft.check(pc.get("peer") is None, "topology-hotplug-endpoint-unlinked",
+ "unexpected peer=%s" % pc.get("peer"))
+
+ # (d) pure reads (dump + stats GET) never advance generation.
+ g_pre_reads = _gen(fab)
+ eps_by_name(fab)
+ fab.do("port-stats-get", {"endpoint-id": ep_c["endpoint-id"],
+ "port-index": 0})
+ g_post_reads = _gen(fab)
+ ksft.check(g_post_reads == g_pre_reads,
+ "topology-reads-do-not-advance-generation",
+ "pre=%s post=%s" % (g_pre_reads, g_post_reads))
+
+ # shrink back to baseline; assert one DELETE event and the restored count.
+ ev2 = L.DrmFabric()
+ ev2.ntf_subscribe(L.MCAST_MONITOR)
+ L.settle(EVT_SETTLE)
+ del_ep(fab, slot_of(ep_d["name"]), ep_d["name"])
+ d_evt = L.wait_ntf(ev2, "endpoint-delete-ntf", timeout=EVT_DURATION)
+ del_ep(fab, slot_of(ep_c["name"]), ep_c["name"])
+ ksft.check(d_evt is not None, "topology-shrink-delete-ntf")
+ ksft.check(len(eps_by_name(fab)) == n0, "topology-shrink-restores-baseline",
+ "n0=%d now=%d" % (n0, len(eps_by_name(fab))))
+
+
+
+def test_hotplug_lifecycle(ksft, cfg):
+ """Hotplug one endpoint and unplug it: CREATE, DELETE, membership."""
+ fab = cfg.fab
+ ev = L.DrmFabric()
+ ev.ntf_subscribe(L.MCAST_MONITOR)
+ L.settle(EVT_SETTLE)
+ n_before = len(eps_by_name(fab))
+ new_ep = add_ep(fab, "add_endpoint", nports=2)
+ add_evt = L.wait_ntf(ev, "endpoint-create-ntf", timeout=EVT_DURATION)
+
+ ksft.check(new_ep is not None and len(eps_by_name(fab)) == n_before + 1,
+ "hotplug-add-appears",
+ "n_before=%d new=%s" % (n_before, new_ep))
+ ksft.check(add_evt is not None, "hotplug-add-endpoint-create-ntf")
+ if new_ep is None:
+ ksft.not_ok("hotplug-add-is-member", "add_endpoint produced no endpoint")
+ ksft.not_ok("hotplug-del-disappears", "add failed")
+ ksft.not_ok("hotplug-del-endpoint-delete-ntf", "add failed")
+ return
+
+ name = new_ep["name"]
+ ksft.check(new_ep.get("fabric-id") == cfg.fid, "hotplug-add-is-member",
+ "fabric-id=%s" % new_ep.get("fabric-id"))
+
+ try:
+ ev = L.DrmFabric()
+ ev.ntf_subscribe(L.MCAST_MONITOR)
+ L.settle(EVT_SETTLE)
+ gone = del_ep(fab, slot_of(name), name)
+ del_evt = L.wait_ntf(ev, "endpoint-delete-ntf", timeout=EVT_DURATION)
+ ksft.check(gone, "hotplug-del-disappears")
+ ksft.check(del_evt is not None, "hotplug-del-endpoint-delete-ntf")
+ finally:
+ if name in eps_by_name(fab):
+ del_ep(fab, slot_of(name), name)
+
+
+CASES = (
+ test_provider_topology_lifecycle,
+ test_hotplug_lifecycle,
+)
+
+
+def main():
+ ksft = L.Ksft()
+ _, NlError = L.import_ynl()
+
+ with L.fabricsim(ksft, need_debugfs=True, need_control="add_endpoint") as fab:
+ fid = fabricsim_fid(fab)
+ if fid is None:
+ ksft.skip_all("fabricsim fabric not present")
+
+ L.run_cases(ksft, Cfg(fab, fid, NlError), CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/lib_drm_fabric.py b/tools/testing/selftests/drivers/gpu/drm_fabric/lib_drm_fabric.py
new file mode 100644
index 000000000000..30fb0edb02b9
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/lib_drm_fabric.py
@@ -0,0 +1,481 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Shared helpers for the drm-fabric selftests (cf.
+tools/testing/selftests/net/lib/py): YNL family binding to drm_fabric.yaml,
+path/module discovery, the DrmFabric wrapper, and a KTAP emitter (Ksft)
+over kselftest/ksft.py.
+"""
+
+import atexit
+import contextlib
+import os
+import signal
+import subprocess
+import sys
+import time
+
+# Path discovery.
+# This file lives at tools/testing/selftests/drivers/gpu/drm_fabric/, six
+# directory levels below the kernel tree root, which ROOT resolves to.
+HERE = os.path.dirname(os.path.abspath(__file__))
+ROOT = os.path.abspath(os.path.join(HERE, "..", "..", "..", "..", "..", ".."))
+
+SPEC = os.environ.get("SPEC") or os.path.join(
+ ROOT, "Documentation", "netlink", "specs", "drm_fabric.yaml")
+UAPI_HEADER = os.environ.get("UAPI_HEADER") or os.path.join(
+ ROOT, "include", "uapi", "drm", "drm_fabric.h")
+
+# Modules: the QEMU harness sets FABRIC_DIR=/modules; otherwise build output
+# lives next to the source.
+FABRIC_DIR = os.environ.get("FABRIC_DIR") or os.path.join(
+ ROOT, "drivers", "gpu", "drm", "fabric")
+
+DEBUGFS = "/sys/kernel/debug/drm_fabric_sim"
+
+FAMILY = "drm-fabric"
+MCAST_MONITOR = "monitor"
+
+
+def _ynl_dir():
+ env = os.environ.get("YNL_DIR")
+ if env:
+ return env
+ return os.path.join(ROOT, "tools", "net", "ynl")
+
+
+def import_ynl():
+ """Import YnlFamily/NlError from the in-tree YNL library, or SKIP."""
+ ynl_dir = _ynl_dir()
+ if ynl_dir not in sys.path:
+ sys.path.insert(0, ynl_dir)
+ try:
+ from pyynl.lib import YnlFamily, NlError
+ except ModuleNotFoundError as exc:
+ print("1..0 # SKIP cannot import YNL library from %s (%s)"
+ % (ynl_dir, exc))
+ sys.exit(4)
+ return YnlFamily, NlError
+
+
+def _ksft_dir_candidates():
+ """Where the kernel's generic kselftest/ksft.py may live."""
+ yield os.path.normpath(os.path.join(HERE, "..", "..", "..", "kselftest"))
+ yield os.path.join(ROOT, "tools", "testing", "selftests", "kselftest")
+ ynl_root = os.path.normpath(os.path.join(_ynl_dir(), "..", "..", ".."))
+ yield os.path.join(ynl_root, "tools", "testing", "selftests", "kselftest")
+
+
+_ksft_mod = None
+
+
+def import_ksft():
+ """Import the kernel's kselftest/ksft.py, or None if unavailable (inline
+ TAP fallback below)."""
+ global _ksft_mod
+ if _ksft_mod is not None:
+ return _ksft_mod or None
+ for cand in _ksft_dir_candidates():
+ if os.path.isfile(os.path.join(cand, "ksft.py")):
+ if cand not in sys.path:
+ sys.path.insert(0, cand)
+ import ksft as _k
+ _ksft_mod = _k
+ return _k
+ _ksft_mod = False # cache "looked, not found"
+ return None
+
+
+# KTAP emitter
+
+class Ksft:
+ """KTAP emitter delegating to the kernel's kselftest/ksft.py.
+
+ Thin ergonomic adapter (check/ok/skip/finish) over the in-tree primitives
+ (``test_result_*``/``set_plan``/``finished``) so the suites do not reinvent
+ TAP. Falls back to inline printing only when ksft.py is not importable.
+ """
+
+ def __init__(self):
+ self._k = import_ksft()
+ self.cnt = 0
+ self.fail = 0
+ self._started = False
+ # Timing. Suite wall starts at construction so it also covers the
+ # module load / family setup main() does before the first result.
+ # Per-case and per-step deltas accrue as results are emitted; the
+ # digest is printed by finish() as KTAP "# time:" diagnostics.
+ self._t0 = time.monotonic()
+ self._last = None # time of the previous result
+ self._cur = None # [name, t_start, cnt_at_start]
+ self._cases = [] # [(name, dur_s, steps), ...]
+ self._slow_step = ("", 0.0) # (name, longest inter-result gap)
+
+ def _tick(self, name):
+ """Record the wall gap since the previous result (a "step")."""
+ now = time.monotonic()
+ if self._last is None:
+ self._last = now # first result: no prior to measure
+ return
+ delta = now - self._last
+ self._last = now
+ if delta > self._slow_step[1]:
+ self._slow_step = (name, delta)
+
+ def case_begin(self, name):
+ self._cur = [name, time.monotonic(), self.cnt]
+
+ def case_end(self):
+ if self._cur is None:
+ return
+ name, t_start, cnt0 = self._cur
+ self._cases.append((name, time.monotonic() - t_start, self.cnt - cnt0))
+ self._cur = None
+
+ def start(self):
+ if self._started:
+ return
+ self._started = True
+ if self._k:
+ self._k.print_header()
+ else:
+ print("TAP version 13")
+
+ def skip_all(self, reason):
+ # A 0-plan skip is the whole result (standard KTAP, both backends),
+ # which is only valid before any other result has been printed. A
+ # mid-case call is a bug in the caller, not a runtime condition to
+ # report as KTAP -- surface it loudly instead of emitting a plan
+ # that contradicts results already on stdout.
+ if self.cnt != 0:
+ raise RuntimeError(
+ "skip_all() called after %d result(s) already emitted "
+ "(reason=%r); mid-case failures must use not_ok() (e.g. "
+ "via reset_sim_or_fail()), not skip_all()" % (self.cnt, reason))
+ print("1..0 # SKIP %s" % reason)
+ sys.exit(4)
+
+ def ok(self, name):
+ self.start()
+ self._tick(name)
+ self.cnt += 1
+ if self._k:
+ self._k.test_result_pass(name)
+ else:
+ print("ok %d %s" % (self.cnt, name))
+
+ def not_ok(self, name, detail=""):
+ self.start()
+ self._tick(name)
+ self.cnt += 1
+ self.fail += 1
+ if self._k:
+ if detail:
+ self._k.print_msg(detail)
+ self._k.test_result_fail(name)
+ else:
+ print("not ok %d %s" % (self.cnt, name))
+ if detail:
+ print(" # %s" % detail)
+
+ def skip(self, name, reason=""):
+ self.start()
+ self._tick(name)
+ self.cnt += 1
+ if self._k:
+ if reason:
+ self._k.print_msg("%s: %s" % (name, reason))
+ self._k.test_result_skip(name)
+ else:
+ print("ok %d %s # SKIP %s" % (self.cnt, name, reason))
+
+ def check(self, cond, name, detail=""):
+ if cond:
+ self.ok(name)
+ else:
+ self.not_ok(name, detail)
+ return bool(cond)
+
+ def _emit_timing(self):
+ """Print per-case and per-suite wall-clock as "# time:" KTAP diagnostics
+ (ignored by TAP parsers). Per-case lines are gated behind
+ FABRIC_TIMING; the one-line suite summary is always emitted.
+ """
+ wall = time.monotonic() - self._t0
+ suite = os.path.basename(sys.argv[0]) or "suite"
+ if os.environ.get("FABRIC_TIMING"):
+ for name, dur, steps in self._cases:
+ print("# time: case=%s wall=%.3fs steps=%d" % (name, dur, steps))
+ slow = max(self._cases, default=("-", 0.0, 0), key=lambda c: c[1])
+ print("# time: suite=%s wall=%.3fs cases=%d steps=%d "
+ "slowest-case=%s(%.3fs) slowest-step=%s(%.3fs)"
+ % (suite, wall, len(self._cases), self.cnt,
+ slow[0], slow[1], self._slow_step[0], self._slow_step[1]))
+
+ def finish(self):
+ self.start()
+ self._emit_timing()
+ if self._k:
+ self._k.set_plan(self.cnt)
+ self._k.finished() # prints totals + exits 0/1 by pass+skip
+ else:
+ print("1..%d" % self.cnt)
+ print("")
+ print("# %d/%d passed, %d failed"
+ % (self.cnt - self.fail, self.cnt, self.fail))
+ sys.exit(1 if self.fail else 0)
+
+
+# Case dispatch
+
+def run_cases(ksft, cfg, cases):
+ """Dispatch each case, isolating an exception to its own result."""
+ for fn in cases:
+ ksft.case_begin(fn.__name__)
+ try:
+ fn(ksft, cfg)
+ except Exception as exc: # noqa: BLE001 - isolate one case's failure
+ ksft.not_ok(fn.__name__, "unhandled exception: %r" % exc)
+ finally:
+ ksft.case_end()
+ if getattr(cfg, "abort", False):
+ break
+
+
+# YNL wrapper
+
+def DrmFabric(**kwargs):
+ """Construct a YnlFamily bound to the drm_fabric spec (schema off)."""
+ YnlFamily, _ = import_ynl()
+ if not os.path.isfile(SPEC):
+ Ksft().skip_all("drm_fabric.yaml not found at %s" % SPEC)
+ # schema='' skips slow jsonschema validation, matching the net selftests.
+ return YnlFamily(SPEC, schema="", **kwargs)
+
+
+def nl_errno(exc):
+ """Positive errno carried by a netlink exception."""
+ return getattr(exc, "error", 0)
+
+
+def family_has_op(fab, name):
+ """True if the loaded family advertises operation @name: a query-only
+ build has none of the topology-mutation ops (fabric-new, fabric-del,
+ endpoint-set, port-set, port-peer-new/del), so cases exercising them are
+ filtered rather than raising KeyError.
+ """
+ return name in getattr(fab, "ops", {})
+
+
+# System helpers (kselftest runs as root)
+
+def is_root():
+ return os.geteuid() == 0
+
+
+def module_loaded(name):
+ return os.path.isdir("/sys/module/%s" % name)
+
+
+def insmod(ko, *args):
+ path = ko if os.path.isabs(ko) else os.path.join(FABRIC_DIR, ko)
+ return subprocess.call(["insmod", path, *args],
+ stderr=subprocess.DEVNULL) == 0
+
+
+def rmmod(name):
+ subprocess.call(["rmmod", name], stderr=subprocess.DEVNULL)
+
+
+def debugfs_available():
+ return os.path.isdir(DEBUGFS)
+
+
+def dbg_write(rel, val):
+ with open(os.path.join(DEBUGFS, rel), "w") as fh:
+ fh.write(str(val))
+
+
+def settle(seconds=0.2):
+ time.sleep(seconds)
+
+
+def wait_until(predicate, timeout=3.0, interval=0.02):
+ """Poll @predicate until truthy or @timeout elapses; return the last value."""
+ deadline = time.monotonic() + timeout
+ val = predicate()
+ while not val and time.monotonic() < deadline:
+ time.sleep(interval)
+ val = predicate()
+ return val
+
+
+def wait_ntf(ev, want_name, timeout=3.0, match=None):
+ """First notification named @want_name within @timeout, else None."""
+ deadline = time.monotonic() + timeout
+ while True:
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ return None
+ for ntf in ev.poll_ntf(duration=min(remaining, 0.25)):
+ if ntf["name"] != want_name:
+ continue
+ if match is None or match(ntf):
+ return ntf
+
+
+# Teardown that survives the timeout killer
+
+_teardowns = []
+_teardown_armed = False
+
+
+def _run_teardowns():
+ """Run registered teardowns once, most-recent first, swallowing errors."""
+ while _teardowns:
+ fn = _teardowns.pop()
+ try:
+ fn()
+ except Exception: # noqa: BLE001 - teardown must not mask the exit
+ pass
+
+
+def _sig_teardown(signum, _frame):
+ _run_teardowns()
+ # Restore the default disposition and re-raise so the exit status still
+ # reflects the signal (the kselftest runner treats it as a failure/timeout).
+ signal.signal(signum, signal.SIG_DFL)
+ os.kill(os.getpid(), signum)
+
+
+def on_teardown(fn):
+ """Register @fn for normal exit and SIGTERM/SIGINT.
+
+ atexit alone misses the timeout runner's SIGTERM. SIGKILL cannot be
+ caught, so a killed predecessor is recovered at the next test's entry.
+ """
+ global _teardown_armed
+ if not _teardown_armed:
+ atexit.register(_run_teardowns)
+ for sig in (signal.SIGTERM, signal.SIGINT):
+ try:
+ signal.signal(sig, _sig_teardown)
+ except (ValueError, OSError):
+ pass # not on the main thread; atexit still covers clean exit
+ _teardown_armed = True
+ _teardowns.append(fn)
+
+
+# Suite fixture
+
+def _providers_unload():
+ rmmod("drm_fabric_sim")
+ rmmod("drm_fabric")
+
+
+def sim_restore_default():
+ # Drop the shape the suite loaded and put the default topology back.
+ rmmod("drm_fabric_sim")
+ if module_loaded("drm_fabric"):
+ insmod("drm-fabric-sim.ko")
+ wait_until(lambda: module_loaded("drm_fabric_sim"))
+
+
+def _reset_sim_steps(topology):
+ """Drop whatever a previous test left loaded, then load @topology.
+
+ A test killed with SIGKILL runs no Python cleanup, so isolation is
+ re-established here, at the next test's entry. Returns (ok, reason) so
+ each caller can pick its own KTAP path.
+ """
+ if not is_root():
+ return False, "must run as root (insmod)"
+ rmmod("drm_fabric_sim")
+ if not module_loaded("drm_fabric") and not insmod("drm-fabric.ko"):
+ return False, "could not load drm_fabric"
+ if not insmod("drm-fabric-sim.ko", "topology=%s" % topology):
+ return False, "could not load drm_fabric_sim topology=%s" % topology
+ if not wait_until(lambda: module_loaded("drm_fabric_sim")):
+ return False, "drm_fabric_sim did not appear after reset"
+ return True, ""
+
+
+def reset_sim(ksft, topology="mesh"):
+ """Load @topology, or skip the suite.
+
+ Setup-time only: skip_all() is invalid once a result has been emitted.
+ """
+ ok, reason = _reset_sim_steps(topology)
+ if not ok:
+ ksft.skip_all(reason)
+
+
+def reset_sim_or_fail(ksft, name, topology="mesh"):
+ """Load @topology, or fail the current case as @name.
+
+ On False the caller must bail out; the not_ok() already stands.
+ """
+ ok, reason = _reset_sim_steps(topology)
+ if not ok:
+ ksft.not_ok(name, reason)
+ return False
+ return True
+
+
+@contextlib.contextmanager
+def fabricsim(ksft, topology=None, need_debugfs=False, need_control=None,
+ open_family=True):
+ """Bring the providers up, yield a bound family, arrange teardown.
+
+ A missing precondition skips the suite. @topology reloads the sim even
+ under --no-load and restores the default on exit. @open_family=False
+ yields None for suites opening their own socket.
+ """
+ _, NlError = import_ynl()
+
+ if not is_root():
+ ksft.skip_all("must run as root (genetlink + debugfs + insmod)")
+
+ if topology is not None:
+ # Entry reset: dropping any sim left by a previous (possibly
+ # SIGKILL-terminated) test before loading this shape is what makes a
+ # topology-changing suite start from a known state. See reset_sim().
+ rmmod("drm_fabric_sim")
+ loaded_core = False
+ if not module_loaded("drm_fabric"):
+ if not insmod("drm-fabric.ko"):
+ ksft.skip_all("could not load drm_fabric")
+ loaded_core = True
+ # Arm teardown before loading the sim so a failed sim load (or any
+ # later skip) still restores the default shape and unwinds a core we
+ # loaded here, rather than leaking it into the next suite.
+ on_teardown(_providers_unload if loaded_core else sim_restore_default)
+ if not insmod("drm-fabric-sim.ko", "topology=%s" % topology):
+ ksft.skip_all("could not load drm_fabric_sim topology=%s" % topology)
+ wait_until(lambda: module_loaded("drm_fabric_sim"))
+ elif "--no-load" not in sys.argv[1:] and not module_loaded("drm_fabric"):
+ rmmod("drm_fabric_sim")
+ rmmod("drm_fabric")
+ # Arm teardown before loading so a partial load (core up, sim load
+ # failed) is unwound instead of leaking a module into the next suite.
+ on_teardown(_providers_unload)
+ if not insmod("drm-fabric.ko") or not insmod("drm-fabric-sim.ko"):
+ ksft.skip_all("could not load drm_fabric + drm_fabric_sim modules")
+ wait_until(lambda: module_loaded("drm_fabric_sim"))
+
+ if not module_loaded("drm_fabric_sim"):
+ ksft.skip_all("drm_fabric_sim not loaded")
+ if need_debugfs and not debugfs_available():
+ ksft.skip_all("fabricsim debugfs not available (runtime controls)")
+ if need_control and not os.path.exists(os.path.join(DEBUGFS, need_control)):
+ ksft.skip_all("fabricsim lacks '%s' control (old module)" % need_control)
+
+ if not open_family:
+ yield None
+ return
+
+ try:
+ fab = DrmFabric()
+ except (OSError, NlError) as exc:
+ ksft.skip_all("cannot open drm-fabric family: %s" % exc)
+ yield fab
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/nl_policy_probe.py b/tools/testing/selftests/drivers/gpu/drm_fabric/nl_policy_probe.py
new file mode 100755
index 000000000000..0d4d60d45e5a
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/nl_policy_probe.py
@@ -0,0 +1,485 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Adversarial raw-netlink probes the YNL suites cannot reach: malformed
+attrs (wrong type, unknown id, truncated nest, out-of-range enum, missing
+required) must return a clean NLMSG_ERROR, never an oops; a liveness dump
+confirms nothing wedged the family. Also introspects the family and emits
+TAP.
+
+Topology-mutation policy probes arrive with the provisioning ABI; this
+query-only build defines no mutation commands or attributes to probe.
+"""
+
+import errno
+import os
+import re
+import socket
+import struct
+import subprocess
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L # noqa: E402 - shared KTAP emitter only (no pyynl)
+
+# Netlink / generic-netlink constants
+NETLINK_GENERIC = 16
+
+NLMSG_ERROR = 0x2
+NLMSG_DONE = 0x3
+
+NLM_F_REQUEST = 0x01
+NLM_F_ACK = 0x04
+NLM_F_DUMP = 0x300
+
+GENL_ID_CTRL = 0x10
+CTRL_CMD_GETFAMILY = 3
+# uapi/linux/genetlink.h CTRL_ATTR_* enum:
+# 1 FAMILY_ID, 2 FAMILY_NAME, 3 VERSION, 4 HDRSIZE, 5 MAXATTR,
+# 6 OPS, 7 MCAST_GROUPS
+CTRL_ATTR_FAMILY_ID = 1
+CTRL_ATTR_FAMILY_NAME = 2
+CTRL_ATTR_VERSION = 3
+CTRL_ATTR_OPS = 6
+CTRL_ATTR_MCAST_GROUPS = 7
+# Within a CTRL_ATTR_OPS entry:
+CTRL_ATTR_OP_ID = 1
+CTRL_ATTR_OP_FLAGS = 2
+# Within a CTRL_ATTR_MCAST_GROUPS entry:
+CTRL_ATTR_MCAST_GRP_NAME = 1
+
+# genetlink op flags (uapi/linux/genetlink.h)
+GENL_ADMIN_PERM = 0x01
+
+NLA_F_NESTED = 0x8000
+NLA_TYPE_MASK = ~(NLA_F_NESTED | 0x4000)
+
+NLMSG_HDRLEN = 16
+GENL_HDRLEN = 4
+
+FAMILY_NAME = b"drm-fabric"
+
+EXPECTED_VERSION = 1
+MCAST_MONITOR = b"monitor"
+
+
+# Command / attribute ids: derive from the uAPI header.
+#
+# Hand-written ids drift the moment someone reorders an enum, leaving the probe
+# silently fuzzing the wrong command. Parse them from the canonical uapi header
+# (or its sibling/initramfs copy) so a reorder is reflected automatically;
+# deliberately no fallback table -- skip the whole suite if the header cannot
+# be found, rather than risk probing under a stale guess.
+
+def _find_uapi_header():
+ cand = os.environ.get("UAPI_HEADER")
+ if cand and os.path.isfile(cand):
+ return cand
+ here = os.path.dirname(os.path.abspath(__file__))
+ root = os.path.abspath(os.path.join(here, *([".."] * 6)))
+ for p in (os.path.join(root, "include", "uapi", "drm", "drm_fabric.h"),
+ "/opt/spec/drm_fabric.h"):
+ if os.path.isfile(p):
+ return p
+ return None
+
+
+def _parse_all_enums(text):
+ """Merge values from every enum block into one symbol table.
+
+ Commands use an anonymous enum and attributes a named one, so parsing by
+ enum name is brittle. Symbols are assumed unique; collisions are
+ last-wins.
+ """
+ out = {}
+ for body in re.findall(r"enum\s*(?:\w+\s*)?\{(.*?)\}", text, re.S):
+ body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
+ body = re.sub(r"//[^\n]*", "", body)
+ nxt = 0
+ for raw in body.split(","):
+ item = raw.strip()
+ if not item:
+ continue
+ if "=" in item:
+ name, val = item.split("=", 1)
+ name = name.strip()
+ try:
+ nxt = int(val.strip(), 0)
+ except ValueError:
+ continue
+ else:
+ name = item
+ if re.match(r"^[A-Za-z_]\w*$", name):
+ out[name] = nxt
+ nxt += 1
+ return out
+
+
+def _load_ids():
+ # Committed fallbacks (kept in sync with drm_fabric.h, query-only build).
+ syms = {"DRM_FABRIC_CMD_FABRIC_GET": 1, "DRM_FABRIC_CMD_PORT_GET": 3,
+ "DRM_FABRIC_A_FABRIC_ID": 5, "DRM_FABRIC_A_ENDPOINT_ID": 6,
+ "DRM_FABRIC_A_PORT_INDEX": 7, "DRM_FABRIC_A_PEER": 10}
+ src = "fallback literals"
+ hdr = _find_uapi_header()
+ if hdr:
+ parsed = _parse_all_enums(open(hdr).read())
+ if "DRM_FABRIC_CMD_PORT_GET" in parsed and "DRM_FABRIC_A_FABRIC_ID" in parsed:
+ syms, src = parsed, hdr
+ return syms, src
+
+
+_SYMS, _ID_SRC = _load_ids()
+
+CMD_FABRIC_GET = _SYMS["DRM_FABRIC_CMD_FABRIC_GET"]
+CMD_PORT_GET = _SYMS["DRM_FABRIC_CMD_PORT_GET"]
+
+A_FABRIC_ID = _SYMS["DRM_FABRIC_A_FABRIC_ID"]
+A_ENDPOINT_ID = _SYMS["DRM_FABRIC_A_ENDPOINT_ID"]
+A_PORT_INDEX = _SYMS["DRM_FABRIC_A_PORT_INDEX"]
+
+# An attribute id guaranteed to be past the family's top-level maxattr, so the
+# kernel strict-rejects it. Derived from the parsed ids (one past the largest
+# symbol) rather than a magic literal, which would silently stop testing strict
+# rejection once the attribute set grows past it.
+A_UNKNOWN = max(_SYMS.values()) + 1
+
+
+# NLA builders
+
+def _align4(n):
+ return (n + 3) & ~3
+
+
+def nla(attr_type, payload):
+ length = 4 + len(payload)
+ pad = b"\x00" * (_align4(length) - length)
+ return struct.pack("=HH", length, attr_type) + payload + pad
+
+
+def nla_u32(attr_type, val):
+ return nla(attr_type, struct.pack("=I", val & 0xFFFFFFFF))
+
+
+def nla_u64(attr_type, val):
+ return nla(attr_type, struct.pack("=Q", val & 0xFFFFFFFFFFFFFFFF))
+
+
+def build_msg(family_id, cmd, seq, payload, flags=NLM_F_REQUEST | NLM_F_ACK):
+ body = struct.pack("=BBH", cmd, 1, 0) + payload
+ total = NLMSG_HDRLEN + len(body)
+ nlh = struct.pack("=IHHII", total, family_id, flags, seq, 0)
+ return nlh + body
+
+
+# Socket helpers
+
+def open_sock():
+ s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
+ s.bind((0, 0))
+ s.settimeout(3)
+ return s
+
+
+def iter_attrs(blob):
+ off = 0
+ while off + 4 <= len(blob):
+ (alen, atype) = struct.unpack_from("=HH", blob, off)
+ if alen < 4:
+ break
+ payload = blob[off + 4:off + alen]
+ yield atype, payload
+ off += _align4(alen)
+
+
+def parse_dgram(data):
+ """Split one recv() buffer into (nlmsg_type, errno) tuples.
+
+ errno is None when mlen is too short to hold the 4 payload bytes it
+ claims -- distinct from a genuine zero status.
+ """
+ out = []
+ off = 0
+ while off + NLMSG_HDRLEN <= len(data):
+ (mlen, mtype, _, _, _) = struct.unpack_from("=IHHII", data, off)
+ if mlen < NLMSG_HDRLEN:
+ break
+ if mtype in (NLMSG_ERROR, NLMSG_DONE):
+ if mlen >= NLMSG_HDRLEN + 4:
+ (err,) = struct.unpack_from("=i", data, off + NLMSG_HDRLEN)
+ else:
+ err = None
+ out.append((mtype, err))
+ else:
+ out.append((mtype, 0))
+ off += _align4(mlen)
+ return out
+
+
+def drain(sock, first_timeout=0.5, more_timeout=0.3):
+ """Read every datagram the kernel queued in response to one request."""
+ msgs = []
+ sock.settimeout(first_timeout)
+ try:
+ msgs += parse_dgram(sock.recv(16384))
+ except socket.timeout:
+ return msgs
+ sock.settimeout(more_timeout)
+ while True:
+ try:
+ msgs += parse_dgram(sock.recv(16384))
+ except socket.timeout:
+ break
+ return msgs
+
+
+def resolve_family(sock, name):
+ seq = 1
+ msg = build_msg(GENL_ID_CTRL, CTRL_CMD_GETFAMILY, seq,
+ nla(CTRL_ATTR_FAMILY_NAME, name + b"\x00"))
+ sock.send(msg)
+ try:
+ data = sock.recv(8192)
+ except socket.timeout:
+ return None
+ (_, mtype, _, _, _) = struct.unpack_from("=IHHII", data, 0)
+ if mtype == NLMSG_ERROR:
+ return None
+ attrs = data[NLMSG_HDRLEN + GENL_HDRLEN:]
+ for atype, payload in iter_attrs(attrs):
+ if atype == CTRL_ATTR_FAMILY_ID:
+ if len(payload) >= 4:
+ return struct.unpack_from("=I", payload, 0)[0]
+ if len(payload) >= 2:
+ return struct.unpack_from("=H", payload, 0)[0]
+ return None
+
+
+def get_family_info(sock, name):
+ """Introspect the family via CTRL_CMD_GETFAMILY.
+
+ Returns {version, ops: {op_id: flags}, mcast: set(names)} or None,
+ letting callers confirm version, admin-perm on mutators, and the
+ monitor group.
+ """
+ seq = 2
+ msg = build_msg(GENL_ID_CTRL, CTRL_CMD_GETFAMILY, seq,
+ nla(CTRL_ATTR_FAMILY_NAME, name + b"\x00"),
+ flags=NLM_F_REQUEST)
+ sock.send(msg)
+ try:
+ data = sock.recv(65536)
+ except socket.timeout:
+ return None
+ (_, mtype, _, _, _) = struct.unpack_from("=IHHII", data, 0)
+ if mtype == NLMSG_ERROR:
+ return None
+
+ info = {"version": None, "ops": {}, "mcast": set()}
+ attrs = data[NLMSG_HDRLEN + GENL_HDRLEN:]
+ for atype, payload in iter_attrs(attrs):
+ atype &= NLA_TYPE_MASK
+ if atype == CTRL_ATTR_VERSION and len(payload) >= 4:
+ info["version"] = struct.unpack_from("=I", payload, 0)[0]
+ elif atype == CTRL_ATTR_OPS:
+ for _, op_blob in iter_attrs(payload): # one entry per op
+ op_id = op_flags = None
+ for sub, val in iter_attrs(op_blob):
+ sub &= NLA_TYPE_MASK
+ if sub == CTRL_ATTR_OP_ID and len(val) >= 4:
+ op_id = struct.unpack_from("=I", val, 0)[0]
+ elif sub == CTRL_ATTR_OP_FLAGS and len(val) >= 4:
+ op_flags = struct.unpack_from("=I", val, 0)[0]
+ if op_id is not None:
+ info["ops"][op_id] = op_flags or 0
+ elif atype == CTRL_ATTR_MCAST_GROUPS:
+ for _, grp_blob in iter_attrs(payload):
+ for sub, val in iter_attrs(grp_blob):
+ sub &= NLA_TYPE_MASK
+ if sub == CTRL_ATTR_MCAST_GRP_NAME:
+ info["mcast"].add(val.rstrip(b"\x00"))
+ return info
+
+
+# The KTAP emitter (L.Ksft) is shared with the YNL suites: one emitter, and a
+# dynamic plan printed at finish() instead of a hard-coded count that drifts
+# every time a case is added or removed.
+
+_SEQ = [100]
+
+
+def case_rejected(tap, name, sock, fid, cmd, payload, expect):
+ """Pass iff the kernel rejected with one of @expect (positive errno
+ values; the netlink error is negative, so we compare -e). The specific
+ code matters: e.g. -EINVAL for a malformed attribute, not a generic
+ failure.
+ """
+ _SEQ[0] += 1
+ sock.send(build_msg(fid, cmd, _SEQ[0], payload))
+ msgs = drain(sock)
+ rejected = [-e for (t, e) in msgs
+ if t == NLMSG_ERROR and e is not None and e != 0]
+ if not msgs:
+ tap.not_ok(name, "no response (possible hang)")
+ elif not rejected:
+ tap.not_ok(name, "accepted (no error returned)")
+ elif rejected[0] in expect:
+ tap.ok("%s (errno=%d)" % (name, rejected[0]))
+ else:
+ want = "/".join(errno.errorcode.get(e, str(e)) for e in sorted(expect))
+ tap.not_ok(name, "errno=%d (%s), expected %s"
+ % (rejected[0], errno.errorcode.get(rejected[0], "?"), want))
+
+
+def _maybe_load_modules():
+ """Standalone runs self-load; a pre-loading harness passes --no-load.
+ Returns True iff this run loaded the providers, so the caller can
+ register teardown.
+ """
+ if "--no-load" in sys.argv[1:]:
+ return False
+ if os.path.isdir("/sys/module/drm_fabric"):
+ return False
+ here = os.path.dirname(os.path.abspath(__file__))
+ root = os.path.abspath(os.path.join(here, *([".."] * 6)))
+ fdir = os.environ.get("FABRIC_DIR") or os.path.join(
+ root, "drivers", "gpu", "drm", "fabric")
+ loaded = False
+ for ko in ("drm-fabric.ko", "drm-fabric-sim.ko"):
+ path = os.path.join(fdir, ko)
+ if os.path.isfile(path):
+ if subprocess.call(["insmod", path],
+ stderr=subprocess.DEVNULL) == 0:
+ loaded = True
+ return loaded
+
+
+def _unload_providers():
+ subprocess.call(["rmmod", "drm_fabric_sim"], stderr=subprocess.DEVNULL)
+ subprocess.call(["rmmod", "drm_fabric"], stderr=subprocess.DEVNULL)
+
+
+class Cfg:
+ def __init__(self, sock, fid):
+ self.sock = sock
+ self.fid = fid
+
+
+def test_malformed_requests(ksft, cfg):
+ sock, fid = cfg.sock, cfg.fid
+ # Malformed framing/attributes must fail validation with -EINVAL.
+ EINVAL = {errno.EINVAL}
+
+ case_rejected(ksft, "wrong-type-short-u32", sock, fid, CMD_FABRIC_GET,
+ nla(A_FABRIC_ID, struct.pack("=H", 1)), EINVAL)
+
+ case_rejected(ksft, "unknown-attribute-id", sock, fid, CMD_FABRIC_GET,
+ nla_u32(A_FABRIC_ID, 1) + nla_u32(A_UNKNOWN, 0), EINVAL)
+
+ case_rejected(ksft, "missing-required-port-index", sock, fid, CMD_PORT_GET,
+ nla_u32(A_ENDPOINT_ID, 0), EINVAL)
+
+
+def test_liveness(ksft, cfg):
+ """A dump that doesn't hang or error is not enough: it must also carry
+ a well-formed, zero-status terminal NLMSG_DONE, or a wedge/regression in
+ the dump's termination path would go unnoticed. An empty-but-valid dump
+ (no data records, just a clean DONE) is still a pass.
+ """
+ sock, fid = cfg.sock, cfg.fid
+ _SEQ[0] += 1
+ sock.send(build_msg(fid, CMD_FABRIC_GET, _SEQ[0], b"",
+ flags=NLM_F_REQUEST | NLM_F_DUMP))
+ msgs = drain(sock)
+ errs = [e for (t, e) in msgs if t == NLMSG_ERROR and e != 0]
+ dones = [e for (t, e) in msgs if t == NLMSG_DONE]
+ if not msgs:
+ ksft.not_ok("liveness-dump-after-fuzz", "no response (possible hang)")
+ elif errs:
+ ksft.not_ok("liveness-dump-after-fuzz", "dump errno=%s" % errs[0])
+ elif not dones:
+ ksft.not_ok("liveness-dump-after-fuzz",
+ "no terminal DONE (dump possibly truncated)")
+ elif dones[0] is None:
+ ksft.not_ok("liveness-dump-after-fuzz", "malformed terminal DONE")
+ elif dones[0] != 0:
+ ksft.not_ok("liveness-dump-after-fuzz",
+ "terminal DONE error=%d" % dones[0])
+ else:
+ ksft.ok("liveness-dump-after-fuzz")
+
+
+def test_family_introspection(ksft, cfg):
+ """Via CTRL_CMD_GETFAMILY: version, admin-perm gating, mcast surface."""
+ getter_ids = [_SYMS[n] for n in (
+ "DRM_FABRIC_CMD_FABRIC_GET", "DRM_FABRIC_CMD_ENDPOINT_GET",
+ "DRM_FABRIC_CMD_PORT_GET", "DRM_FABRIC_CMD_PORT_STATS_GET")
+ if n in _SYMS]
+
+ info = get_family_info(cfg.sock, FAMILY_NAME)
+ if not info:
+ for nm in ("genl-family-version", "genl-mcast-monitor-present",
+ "genl-getters-not-admin-perm"):
+ ksft.not_ok(nm, "CTRL_CMD_GETFAMILY introspection failed")
+ return
+
+ if info["version"] == EXPECTED_VERSION:
+ ksft.ok("genl-family-version (v%d)" % info["version"])
+ else:
+ ksft.not_ok("genl-family-version",
+ "got %s want %d" % (info["version"], EXPECTED_VERSION))
+
+ if MCAST_MONITOR in info["mcast"]:
+ ksft.ok("genl-mcast-monitor-present")
+ else:
+ ksft.not_ok("genl-mcast-monitor-present",
+ "groups=%s" % info["mcast"])
+
+ ops = info["ops"]
+ # A query-only build exposes getters only: each must be ungated (no
+ # GENL_ADMIN_PERM), so a normal namespace can enumerate topology.
+ seen_get = [c for c in getter_ids if c in ops]
+ bad_get = [c for c in seen_get if ops[c] & GENL_ADMIN_PERM]
+ if seen_get and not bad_get:
+ ksft.ok("genl-getters-not-admin-perm (%d cmds)" % len(seen_get))
+ else:
+ ksft.not_ok("genl-getters-not-admin-perm",
+ "seen=%s wrongly-gated=%s" % (seen_get, bad_get))
+
+
+CASES = (
+ test_malformed_requests,
+ test_liveness,
+ test_family_introspection,
+)
+
+
+def main():
+ tap = L.Ksft()
+
+ if os.geteuid() != 0:
+ tap.skip_all("root is required to load drm_fabric modules")
+
+ if _maybe_load_modules():
+ L.on_teardown(_unload_providers)
+
+ try:
+ sock = open_sock()
+ except OSError as exc:
+ tap.skip_all("cannot open genetlink socket: %s" % exc)
+
+ fid = resolve_family(sock, FAMILY_NAME)
+ if not fid:
+ tap.skip_all("drm-fabric genl family not registered "
+ "(load drm_fabric.ko)")
+
+ sys.stderr.write("# attribute/command ids from: %s\n" % _ID_SRC)
+
+ cfg = Cfg(sock, fid)
+ L.run_cases(tap, cfg, CASES)
+ tap.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/port_cursor_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/port_cursor_abi.py
new file mode 100755
index 000000000000..eef6378d5ec2
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/port_cursor_abi.py
@@ -0,0 +1,401 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""Exercise nested port-dump resume across endpoint removal.
+
+A resumed dump must not apply the removed endpoint's saved port cursor
+to its successor. Verify that each endpoint observed after removal starts
+at port index 0 for both PORT_GET and PORT_STATS_GET.
+"""
+
+import os
+import re
+import socket
+import struct
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L # noqa: E402 - shared KTAP/module helpers
+
+# --- Netlink / generic-netlink constants (cf. dump_intr_abi.py) -----------
+
+NETLINK_GENERIC = 16
+NLMSG_ERROR = 0x2
+NLMSG_DONE = 0x3
+NLM_F_REQUEST = 0x01
+NLM_F_DUMP = 0x300
+NLMSG_HDRLEN = 16
+GENL_HDRLEN = 4
+NLA_HDRLEN = 4
+NLA_TYPE_MASK = 0x3FFF
+CTRL_ID = 0x10
+CTRL_CMD_GETFAMILY = 3
+CTRL_ATTR_FAMILY_NAME = 2
+CTRL_ATTR_FAMILY_ID = 1
+
+SEQ = 2 # request sequence; replies in the dump must echo it
+
+# Multi-port endpoints so a batch boundary can land *inside* an endpoint (the
+# only case that exercises a non-zero saved port cursor). 15 is near the sim's
+# 16-port cap and rarely divides the per-batch port capacity evenly.
+PORTS = int(os.environ.get("PORT_CURSOR_PORTS", "15"))
+SCALE = int(os.environ.get("PORT_CURSOR_SCALE", "160"))
+
+
+def _align4(n):
+ return (n + 3) & ~3
+
+
+def _nla(atype, payload):
+ length = NLA_HDRLEN + len(payload)
+ pad = b"\x00" * (_align4(length) - length)
+ return struct.pack("=HH", length, atype) + payload + pad
+
+
+def _msg(family_id, cmd, seq, flags, payload=b""):
+ body = struct.pack("=BBH", cmd, 1, 0) + payload
+ total = NLMSG_HDRLEN + len(body)
+ return struct.pack("=IHHII", total, family_id, flags, seq, 0) + body
+
+
+def _open():
+ s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
+ s.bind((0, 0))
+ s.settimeout(5)
+ return s
+
+
+def _resolve_family(sock, name):
+ sock.send(_msg(CTRL_ID, CTRL_CMD_GETFAMILY, 1, NLM_F_REQUEST,
+ _nla(CTRL_ATTR_FAMILY_NAME, name + b"\x00")))
+ data = sock.recv(8192)
+ (_, mtype, _, _, _) = struct.unpack_from("=IHHII", data, 0)
+ if mtype == NLMSG_ERROR:
+ return None
+ off = NLMSG_HDRLEN + GENL_HDRLEN
+ while off + NLA_HDRLEN <= len(data):
+ (alen, atype) = struct.unpack_from("=HH", data, off)
+ if alen < NLA_HDRLEN:
+ break
+ if atype == CTRL_ATTR_FAMILY_ID and alen >= 6:
+ return struct.unpack_from("=H", data, off + 4)[0]
+ off += _align4(alen)
+ return None
+
+
+def _enum(name):
+ """Parse `enum <name> { ... }` from the uAPI header into {member: value}.
+ A value referencing another enumerator (the generated `MAX = (__MAX - 1)`
+ sentinel) fails int() and is skipped rather than silently mis-numbering.
+ """
+ out = {}
+ try:
+ text = open(L.UAPI_HEADER).read()
+ except OSError:
+ return out
+ m = re.search(r"enum\s+%s\s*\{(.*?)\}" % re.escape(name), text, re.S)
+ if not m:
+ return out
+ n = 0
+ for raw in re.sub(r"/\*.*?\*/", "", m.group(1), flags=re.S).split(","):
+ item = raw.strip()
+ if not item:
+ continue
+ if "=" in item:
+ key, val = item.split("=", 1)
+ key = key.strip()
+ try:
+ n = int(val.strip(), 0)
+ except ValueError:
+ continue # references another enumerator; skip sentinel
+ else:
+ key = item
+ out[key] = n
+ n += 1
+ return out
+
+
+_HDR_PRESENT = os.path.isfile(L.UAPI_HEADER)
+_CMD = _enum("drm_fabric_cmd")
+_A = _enum("drm_fabric_a")
+_PA = _enum("drm_fabric_a_port_attrs")
+_PSA = _enum("drm_fabric_a_port_stats_attrs")
+
+# (wanted name, parsed dict, fallback id) -- fallbacks match the current ABI and
+# are only trusted when the header is absent (see main()).
+_WANTED = (
+ ("DRM_FABRIC_CMD_PORT_GET", _CMD, 3),
+ ("DRM_FABRIC_CMD_PORT_STATS_GET", _CMD, 4),
+ ("DRM_FABRIC_A_PORT", _A, 3),
+ ("DRM_FABRIC_A_PORT_STATS", _A, 4),
+ ("DRM_FABRIC_A_PORT_ATTRS_PORT_INDEX", _PA, 1),
+ ("DRM_FABRIC_A_PORT_ATTRS_ENDPOINT_ID", _PA, 2),
+ ("DRM_FABRIC_A_PORT_STATS_ATTRS_ENDPOINT_ID", _PSA, 2),
+ ("DRM_FABRIC_A_PORT_STATS_ATTRS_PORT_INDEX", _PSA, 3),
+)
+CMD_PORT_GET = _CMD.get("DRM_FABRIC_CMD_PORT_GET", 3)
+CMD_PORT_STATS_GET = _CMD.get("DRM_FABRIC_CMD_PORT_STATS_GET", 4)
+A_PORT = _A.get("DRM_FABRIC_A_PORT", 3)
+A_PORT_STATS = _A.get("DRM_FABRIC_A_PORT_STATS", 4)
+PA_PORT_INDEX = _PA.get("DRM_FABRIC_A_PORT_ATTRS_PORT_INDEX", 1)
+PA_ENDPOINT_ID = _PA.get("DRM_FABRIC_A_PORT_ATTRS_ENDPOINT_ID", 2)
+PSA_ENDPOINT_ID = _PSA.get("DRM_FABRIC_A_PORT_STATS_ATTRS_ENDPOINT_ID", 2)
+PSA_PORT_INDEX = _PSA.get("DRM_FABRIC_A_PORT_STATS_ATTRS_PORT_INDEX", 3)
+
+
+def _walk(buf, base, end):
+ """Yield (masked_type, payload) for well-formed nlattrs in buf[base:end]."""
+ off = base
+ while off + NLA_HDRLEN <= end:
+ (alen, atype) = struct.unpack_from("=HH", buf, off)
+ if alen < NLA_HDRLEN or off + alen > end:
+ break # malformed: stop rather than over-read
+ yield atype & NLA_TYPE_MASK, buf[off + NLA_HDRLEN:off + alen]
+ off += _align4(alen)
+
+
+def _parse_entry(payload, outer, ep_attr, idx_attr):
+ """Extract (endpoint_id, port_index) from one dump reply payload. @outer
+ is the top-level nest (A_PORT/A_PORT_STATS); @ep_attr/@idx_attr are the
+ member ids inside it.
+ """
+ ep_id = port_idx = None
+ for atype, data in _walk(payload, 0, len(payload)):
+ if atype != outer:
+ continue
+ for btype, bdata in _walk(data, 0, len(data)):
+ if btype == ep_attr and len(bdata) >= 4:
+ ep_id = struct.unpack_from("=I", bdata, 0)[0]
+ elif btype == idx_attr and len(bdata) >= 4:
+ port_idx = struct.unpack_from("=I", bdata, 0)[0]
+ return ep_id, port_idx
+
+
+def _read_batch(sock, fam, seq, outer, ep_attr, idx_attr):
+ """Read one dump datagram with strict structural validation.
+
+ Returns (pairs, last_ep, done, err). Any malformed length, unexpected
+ family/sequence, or unparseable reply sets err (reported, not silently
+ dropped).
+ """
+ pairs, last_ep, done, err = [], None, False, False
+ try:
+ data = sock.recv(65536)
+ except socket.timeout:
+ return pairs, last_ep, True, True # a stall mid-dump is a failure here
+ off, end = 0, len(data)
+ while off + NLMSG_HDRLEN <= end:
+ (mlen, mtype, _, mseq, _) = struct.unpack_from("=IHHII", data, off)
+ if mlen < NLMSG_HDRLEN or off + mlen > end:
+ err = True
+ break
+ if mtype == NLMSG_DONE:
+ done = True
+ elif mtype == NLMSG_ERROR:
+ err = True
+ elif mtype == fam and mseq == seq:
+ body = off + NLMSG_HDRLEN + GENL_HDRLEN
+ ep_id, port_idx = _parse_entry(memoryview(data)[body:off + mlen],
+ outer, ep_attr, idx_attr)
+ if ep_id is not None and port_idx is not None:
+ pairs.append((ep_id, port_idx))
+ last_ep = ep_id
+ else:
+ err = True
+ else:
+ err = True
+ off += _align4(mlen)
+ return pairs, last_ep, done, err
+
+
+class Cfg:
+ def __init__(self, fam, fab):
+ self.fam = fam
+ self.fab = fab # YnlFamily handle (drm-fabric)
+ self.ep_slot = {} # endpoint-id -> fabricsim slot
+ self.ep_ports = {} # endpoint-id -> its own port count
+ self.abort = False
+
+
+def _ensure_population(cfg):
+ """Top up to SCALE endpoints and rebuild the endpoint-id -> slot map.
+
+ add_endpoint reuses freed slots, so the map is rebuilt each time.
+ """
+ have = len(cfg.fab.dump("endpoint-get", {}))
+ for _ in range(SCALE - have):
+ try:
+ L.dbg_write("add_endpoint", PORTS)
+ except OSError:
+ break
+ ep_slot = {}
+ for e in cfg.fab.dump("endpoint-get", {}):
+ ep = e["endpoint"]
+ m = re.match(r"sim-ep(\d+)$", ep.get("name", ""))
+ if m:
+ ep_slot[ep["endpoint-id"]] = int(m.group(1))
+ cfg.ep_slot = ep_slot
+
+ # Per-endpoint port count, taken from each endpoint's own topology rather
+ # than the global PORTS: the baseline population and runtime-added endpoints
+ # can differ in width, and the "provably mid-dump" oracle below must compare
+ # against the specific endpoint being suspended, not a module-wide setting.
+ ep_ports = {}
+ for p in cfg.fab.dump("port-get", {}):
+ port = p["port"]
+ ep_ports[port["endpoint-id"]] = ep_ports.get(port["endpoint-id"], 0) + 1
+ cfg.ep_ports = ep_ports
+
+
+def _reload():
+ L.rmmod("drm_fabric_sim")
+ if not L.module_loaded("drm_fabric"):
+ if not L.insmod("drm-fabric.ko"):
+ return False
+ # A tiny baseline; the multi-port population is added below.
+ if not L.insmod("drm-fabric-sim.ko", "num_endpoints=2", "ports_per_ep=2",
+ "topology=linear"):
+ return False
+ L.wait_until(lambda: L.module_loaded("drm_fabric_sim"))
+ return True
+
+
+def _restore_default():
+ """Restore fabricsim's default shape so the next suite (sharing the loaded
+ module) does not inherit this suite's small/churned population."""
+ L.rmmod("drm_fabric_sim")
+ if not L.module_loaded("drm_fabric"):
+ L.insmod("drm-fabric.ko")
+ L.insmod("drm-fabric-sim.ko")
+ L.wait_until(lambda: L.module_loaded("drm_fabric_sim"))
+
+
+def _dump_removal_keeps_leading_ports(ksft, cfg, cmd, outer, ep_attr, idx_attr,
+ tag):
+ """Assert every endpoint still starts at port 0 after a mid-endpoint
+ removal. The stats variant also runs the provider callback off the
+ topology lock during resume.
+ """
+ _ensure_population(cfg)
+ s = _open()
+ s.send(_msg(cfg.fam, cmd, SEQ, NLM_F_REQUEST | NLM_F_DUMP))
+
+ seen = {} # ep_id -> set(port_index) seen so far
+ deleted = set()
+ batches = exercised = 0
+ done = err = False
+ while not done:
+ pairs, last_ep, done, e = _read_batch(s, cfg.fam, SEQ,
+ outer, ep_attr, idx_attr)
+ err = err or e
+ if pairs:
+ batches += 1
+ for ep_id, port_idx in pairs:
+ seen.setdefault(ep_id, set()).add(port_idx)
+ # Delete only when provably mid-dump: the cumulative port set is a
+ # strict, non-empty subset of that endpoint's own width, i.e. the
+ # saved cursor is (last_ep, port_idx>0).
+ ep_width = cfg.ep_ports.get(last_ep, PORTS)
+ if (last_ep is not None and last_ep not in deleted
+ and last_ep in cfg.ep_slot
+ and 0 < len(seen.get(last_ep, ())) < ep_width):
+ try:
+ L.dbg_write("del_endpoint", cfg.ep_slot[last_ep])
+ deleted.add(last_ep)
+ exercised += 1
+ except OSError:
+ pass
+ if batches > 10000:
+ break
+ s.close()
+
+ if not ksft.check(batches >= 2 and not err,
+ "%s-dump-spans-multiple-batches" % tag,
+ "batches=%d err=%s (raise PORT_CURSOR_SCALE)"
+ % (batches, err)):
+ cfg.abort = True
+ return
+
+ if not ksft.check(exercised >= 1,
+ "%s-dump-exercised-mid-endpoint-removal" % tag,
+ "no batch suspended mid-endpoint "
+ "(raise PORT_CURSOR_PORTS/PORT_CURSOR_SCALE)"):
+ cfg.abort = True
+ return
+
+ # The invariant: no endpoint may appear missing its leading ports.
+ bad = {ep: sorted(ports)[:3] for ep, ports in seen.items()
+ if 0 not in ports}
+ ksft.check(not bad, "%s-no-leading-ports-dropped" % tag,
+ "endpoints missing port 0: %s"
+ % ", ".join("ep%d=%s" % (e, p) for e, p in bad.items()))
+
+
+def test_port_get_dump_removal_keeps_leading_ports(ksft, cfg):
+ """Nested PORT_GET cursor survives mid-dump endpoint removal."""
+ _dump_removal_keeps_leading_ports(ksft, cfg, CMD_PORT_GET, A_PORT,
+ PA_ENDPOINT_ID, PA_PORT_INDEX, "port-get")
+
+
+def test_port_stats_dump_removal_keeps_leading_ports(ksft, cfg):
+ """Nested PORT_STATS_GET cursor + unlocked stats callback survive
+ mid-dump endpoint removal."""
+ _dump_removal_keeps_leading_ports(ksft, cfg, CMD_PORT_STATS_GET,
+ A_PORT_STATS, PSA_ENDPOINT_ID,
+ PSA_PORT_INDEX, "port-stats-get")
+
+
+CASES = (
+ test_port_get_dump_removal_keeps_leading_ports,
+ test_port_stats_dump_removal_keeps_leading_ports,
+)
+
+
+def main():
+ ksft = L.Ksft()
+ _, NlError = L.import_ynl() # early SKIP if the YNL lib is missing
+
+ if not L.is_root():
+ ksft.skip_all("must run as root (genetlink + debugfs)")
+
+ # Trust parsed ids only when they are complete; require every wanted enum
+ # name when the header is present so a partial parse cannot mis-number.
+ missing = [nm for nm, d, _ in _WANTED if nm not in d]
+ if _HDR_PRESENT and missing:
+ ksft.skip_all("uAPI header present but missing enum(s): %s"
+ % ", ".join(missing))
+ if not _HDR_PRESENT:
+ print("# port_cursor_abi: uAPI header absent, using id fallbacks")
+
+ if not _reload():
+ ksft.skip_all("could not load drm_fabric_sim")
+ # Primary cleanup is try/finally below; on_teardown() is the backup for hard
+ # exits, including the timeout killer's SIGTERM (which atexit would miss),
+ # so this suite's churned population never leaks into the next one.
+ L.on_teardown(_restore_default)
+
+ if not L.debugfs_available():
+ ksft.skip_all("fabricsim debugfs not present")
+
+ try:
+ fab = L.DrmFabric()
+ except (OSError, NlError) as exc:
+ ksft.skip_all("cannot open drm-fabric family: %s" % exc)
+
+ s = _open()
+ fam = _resolve_family(s, L.FAMILY.encode())
+ s.close()
+ if not fam:
+ ksft.skip_all("could not resolve %s family id" % L.FAMILY)
+
+ # Each case tops the multi-port population up to SCALE (so a dump spans many
+ # batches) and rebuilds the endpoint-id -> slot map before it runs.
+ try:
+ L.run_cases(ksft, Cfg(fam, fab), CASES)
+ finally:
+ _restore_default()
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/port_stats_cap_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/port_stats_cap_abi.py
new file mode 100755
index 000000000000..738ee76884f8
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/port_stats_cap_abi.py
@@ -0,0 +1,374 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+Heterogeneous per-port stats: a dump skips a port whose provider returns
+-EOPNOTSUPP and resumes; any other errno ends the dump. pyynl reassembles
+multipart dumps transparently, so this talks raw Generic Netlink.
+
+Needs drm_fabric + drm_fabric_sim (>= 3 ports on ep0), fabricsim debugfs
+(per-port stats_errno, bulk_add/bulk_del), and root.
+"""
+
+import glob
+import os
+import re
+import socket
+import struct
+import sys
+import time
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L # noqa: E402 - shared KTAP/module helpers
+
+EOPNOTSUPP = 95
+EIO = 5
+MID = 1 # not the first/last port, so skipping it proves resumption
+
+# Runtime endpoints added for the skip case. ep0 is enumerated first, so its
+# skipped port is necessarily behind a batch boundary from everything added
+# here. Overridable where record size or NLMSG_GOODSIZE change the batch
+# arithmetic enough to leave the dump single-batch.
+RESUME_SCALE = int(os.environ.get("STATS_RESUME_SCALE", "300"))
+
+# --- raw Generic Netlink (cf. nl_policy_probe.py, dump_intr_abi.py) -------
+
+FAMILY_NAME = b"drm-fabric"
+
+NETLINK_GENERIC = 16
+NLMSG_ERROR = 0x2
+NLMSG_DONE = 0x3
+NLM_F_REQUEST = 0x01
+NLM_F_DUMP = 0x300
+NLMSG_HDRLEN = 16
+GENL_HDRLEN = 4
+CTRL_ID = 0x10
+CTRL_CMD_GETFAMILY = 3
+CTRL_ATTR_FAMILY_ID = 1
+CTRL_ATTR_FAMILY_NAME = 2
+NLA_TYPE_MASK = 0x3FFF # strips NLA_F_NESTED / NLA_F_NET_BYTEORDER
+
+# Small reads keep dump batches small, making resume easier to exercise.
+BATCH_READ = 8192
+
+
+def _align4(n):
+ return (n + 3) & ~3
+
+
+def _nla(attr_type, payload):
+ length = 4 + len(payload)
+ pad = b"\x00" * (_align4(length) - length)
+ return struct.pack("=HH", length, attr_type) + payload + pad
+
+
+def _msg(family_id, cmd, seq, flags, payload=b""):
+ body = struct.pack("=BBH", cmd, 1, 0) + payload
+ total = NLMSG_HDRLEN + len(body)
+ return struct.pack("=IHHII", total, family_id, flags, seq, 0) + body
+
+
+def _open():
+ s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
+ s.bind((0, 0))
+ s.settimeout(5)
+ return s
+
+
+def _resolve_family(sock, name):
+ sock.send(_msg(CTRL_ID, CTRL_CMD_GETFAMILY, 1, NLM_F_REQUEST,
+ _nla(CTRL_ATTR_FAMILY_NAME, name + b"\x00")))
+ data = sock.recv(8192)
+ (_, mtype, _, _, _) = struct.unpack_from("=IHHII", data, 0)
+ if mtype == NLMSG_ERROR:
+ return None
+ for atype, payload in _iter_attrs(data[NLMSG_HDRLEN + GENL_HDRLEN:]):
+ if atype != CTRL_ATTR_FAMILY_ID:
+ continue
+ # CTRL_ATTR_FAMILY_ID is a u16; tolerate a u32 encoding too.
+ if len(payload) >= 4:
+ return struct.unpack_from("=I", payload)[0]
+ if len(payload) >= 2:
+ return struct.unpack_from("=H", payload)[0]
+ return None
+
+
+def _iter_attrs(blob):
+ off = 0
+ while off + 4 <= len(blob):
+ (alen, atype) = struct.unpack_from("=HH", blob, off)
+ if alen < 4:
+ break
+ yield atype & NLA_TYPE_MASK, blob[off + 4:off + alen]
+ off += _align4(alen)
+
+
+def _uapi_ids():
+ """Stats-dump ids from the generated uAPI header.
+
+ No fallback table: an unreadable header skips the suite rather than
+ decoding under a stale guess.
+ """
+ want = ("DRM_FABRIC_CMD_PORT_STATS_GET",
+ "DRM_FABRIC_A_PORT_STATS",
+ "DRM_FABRIC_A_PORT_STATS_ATTRS_ENDPOINT_ID",
+ "DRM_FABRIC_A_PORT_STATS_ATTRS_PORT_INDEX")
+ try:
+ text = open(L.UAPI_HEADER).read()
+ except OSError:
+ return None
+ # Commands are an anonymous enum and attributes are named ones, so merge
+ # every block flatly rather than parsing by enum name.
+ syms = {}
+ for body in re.findall(r"enum\s*(?:\w+\s*)?\{(.*?)\}", text, re.S):
+ nxt = 0
+ for raw in body.split(","):
+ item = raw.split("/*")[0].strip()
+ if not item:
+ continue
+ if "=" in item:
+ name, val = item.split("=", 1)
+ name = name.strip()
+ try:
+ nxt = int(val.strip(), 0)
+ except ValueError:
+ continue
+ else:
+ name = item
+ if name.isidentifier():
+ syms[name] = nxt
+ nxt += 1
+ if not all(w in syms for w in want):
+ return None
+ return {"cmd": syms[want[0]], "nest": syms[want[1]],
+ "ep": syms[want[2]], "port": syms[want[3]]}
+
+
+def _record(body, ids):
+ """(endpoint-id, port-index) carried by one stats entry, or None."""
+ for atype, payload in _iter_attrs(body):
+ if atype != ids["nest"]:
+ continue
+ ep = idx = None
+ for natype, npayload in _iter_attrs(payload):
+ if natype == ids["ep"] and len(npayload) >= 4:
+ ep = struct.unpack_from("=I", npayload)[0]
+ elif natype == ids["port"] and len(npayload) >= 4:
+ idx = struct.unpack_from("=I", npayload)[0]
+ if ep is not None and idx is not None:
+ return (ep, idx)
+ return None
+
+
+class Dump:
+ """Records in wire order, plus how the kernel delivered them."""
+
+ def __init__(self, batches, records, done, error, timed_out):
+ self.batches = batches
+ self.records = records
+ self.done = done
+ self.error = error
+ self.timed_out = timed_out
+
+ @property
+ def ok(self):
+ return self.done and not self.error and not self.timed_out
+
+ def __str__(self):
+ return ("batches=%d records=%d done=%s error=%s timeout=%s"
+ % (self.batches, len(self.records), self.done, self.error,
+ self.timed_out))
+
+
+def _dump_stats(ids):
+ """Run an unfiltered PORT_STATS_GET dump batch by batch."""
+ sock = _open()
+ try:
+ sock.send(_msg(ids["fam"], ids["cmd"], 2, NLM_F_REQUEST | NLM_F_DUMP))
+ batches, recs, done, err = 0, [], False, False
+ while not done:
+ try:
+ data = sock.recv(BATCH_READ)
+ except socket.timeout:
+ return Dump(batches, recs, done, err, True)
+ if not data:
+ break
+ batches += 1
+ off = 0
+ while off + NLMSG_HDRLEN <= len(data):
+ (mlen, mtype, _, _, _) = struct.unpack_from("=IHHII", data, off)
+ if mlen < NLMSG_HDRLEN:
+ break
+ if mtype == NLMSG_DONE:
+ done = True
+ elif mtype == NLMSG_ERROR:
+ err = True
+ else:
+ rec = _record(data[off + NLMSG_HDRLEN + GENL_HDRLEN:
+ off + mlen], ids)
+ if rec:
+ recs.append(rec)
+ off += _align4(mlen)
+ if batches > 10000: # runaway guard
+ break
+ return Dump(batches, recs, done, err, False)
+ finally:
+ sock.close()
+
+
+def _grow(ids):
+ """Grow until the dump spans several batches.
+
+ bulk_add is asynchronous, so read back until two dumps agree.
+ """
+ L.dbg_write("bulk_add", RESUME_SCALE)
+ last, deadline = -1, time.monotonic() + 10.0
+ while time.monotonic() < deadline:
+ now = len(_dump_stats(ids).records)
+ if now == last:
+ return now
+ last = now
+ time.sleep(0.05)
+ return last
+
+
+def _ep0(fab):
+ """endpoint-id of the first init endpoint (debugfs dir ep0, name sim-ep0)."""
+ for e in fab.dump("endpoint-get", {}):
+ ep = e["endpoint"]
+ if ep.get("name") == "sim-ep0":
+ return ep["endpoint-id"]
+ return None
+
+
+def _port_count(fab, ep_id):
+ return len(fab.dump("port-get", {"endpoint-id": ep_id}))
+
+
+def _knob(port, val):
+ # fabricsim's per-port debugfs control: forces @port's next stats
+ # callback to return -@val instead of real data.
+ L.dbg_write("ep0/port%d/stats_errno" % port, val)
+
+
+class Cfg:
+ def __init__(self, fab, ep_id, nports, nl_error, ids):
+ self.fab = fab
+ self.ep_id = ep_id
+ self.nports = nports
+ self.NlError = nl_error
+ self.ids = ids
+
+
+def test_dump_skips_unsupported_port(ksft, cfg):
+ """A mid-list -EOPNOTSUPP port is skipped across a real batch boundary.
+
+ Expectation is the unknobbed dump minus exactly that port, so a lost
+ neighbour, a re-emit after resume, or a duplicate all fail.
+ """
+ name = "stats-dump-skips-unsupported"
+ ids = cfg.ids
+ skipped = (cfg.ep_id, MID)
+ try:
+ _grow(ids)
+ base = _dump_stats(ids)
+ if not base.ok or not base.records:
+ ksft.not_ok(name, "unknobbed dump unusable: %s" % base)
+ return
+ _knob(MID, EOPNOTSUPP)
+ got = _dump_stats(ids)
+ if not got.ok:
+ ksft.not_ok(name, "dump did not complete: %s" % got)
+ return
+ lost = sorted(set(base.records) - set(got.records))
+ extra = sorted(set(got.records) - set(base.records))
+ dupes = len(got.records) != len(set(got.records))
+ ksft.check(got.batches >= 2 and lost == [skipped] and not extra
+ and not dupes,
+ name,
+ "%s lost=%s extra=%s dupes=%s (want lost=[%s], >=2 "
+ "batches -- raise STATS_RESUME_SCALE if single-batch)"
+ % (got, lost, extra, dupes, skipped))
+ finally:
+ _knob(MID, 0)
+ L.dbg_write("bulk_del", 0)
+
+
+def test_targeted_unsupported_port_eopnotsupp(ksft, cfg):
+ """A targeted request for the unsupported port still returns -EOPNOTSUPP."""
+ fab, NlError = cfg.fab, cfg.NlError
+ _knob(MID, EOPNOTSUPP)
+ try:
+ fab.do("port-stats-get", {"endpoint-id": cfg.ep_id, "port-index": MID})
+ ksft.not_ok("stats-targeted-unsupported-eopnotsupp", "request accepted")
+ except NlError as exc:
+ e = L.nl_errno(exc)
+ ksft.check(e == EOPNOTSUPP, "stats-targeted-unsupported-eopnotsupp",
+ "errno=%d (want EOPNOTSUPP=%d)" % (e, EOPNOTSUPP))
+ finally:
+ _knob(MID, 0)
+
+
+def test_dump_aborts_on_real_error(ksft, cfg):
+ """A non-capability provider error (EIO) ends the dump."""
+ fab, NlError = cfg.fab, cfg.NlError
+ _knob(MID, EIO)
+ try:
+ try:
+ fab.dump("port-stats-get", {"endpoint-id": cfg.ep_id})
+ ksft.not_ok("stats-dump-aborts-on-real-error",
+ "dump completed instead of aborting")
+ except NlError as exc:
+ e = L.nl_errno(exc)
+ ksft.check(e == EIO, "stats-dump-aborts-on-real-error",
+ "errno=%d (want EIO=%d)" % (e, EIO))
+ finally:
+ _knob(MID, 0)
+
+
+CASES = (
+ test_dump_skips_unsupported_port,
+ test_targeted_unsupported_port_eopnotsupp,
+ test_dump_aborts_on_real_error,
+)
+
+
+def main():
+ ksft = L.Ksft()
+ _, NlError = L.import_ynl()
+
+ with L.fabricsim(ksft, need_debugfs=True) as fab:
+ if not L.family_has_op(fab, "port-stats-get"):
+ ksft.skip_all("port-stats-get op absent")
+ ep_id = _ep0(fab)
+ if ep_id is None:
+ ksft.skip_all("sim-ep0 endpoint not present")
+ # The per-port stats_errno knob only exists on a recent simulator.
+ if not glob.glob(os.path.join(L.DEBUGFS, "ep0", "port*", "stats_errno")):
+ ksft.skip_all("fabricsim lacks per-port stats_errno knob (old module)")
+ # The skip case needs a population big enough to span dump batches.
+ if not os.path.exists(os.path.join(L.DEBUGFS, "bulk_add")):
+ ksft.skip_all("fabricsim lacks bulk_add (cannot reach a resume "
+ "boundary)")
+ nports = _port_count(fab, ep_id)
+ if nports < 3:
+ ksft.skip_all("need >= 3 ports on ep0 to place a mid-list skip "
+ "(have %d)" % nports)
+ ids = _uapi_ids()
+ if ids is None:
+ ksft.skip_all("could not resolve stats uAPI ids from %s"
+ % L.UAPI_HEADER)
+ sock = _open()
+ try:
+ fam = _resolve_family(sock, FAMILY_NAME)
+ finally:
+ sock.close()
+ if fam is None:
+ ksft.skip_all("drm-fabric generic netlink family not resolvable")
+ ids["fam"] = fam
+ L.run_cases(ksft, Cfg(fab, ep_id, nports, NlError, ids), CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/settings b/tools/testing/selftests/drivers/gpu/drm_fabric/settings
new file mode 100644
index 000000000000..694d70710ff0
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/drivers/gpu/drm_fabric/switch_abi.py b/tools/testing/selftests/drivers/gpu/drm_fabric/switch_abi.py
new file mode 100755
index 000000000000..775be4ac2160
--- /dev/null
+++ b/tools/testing/selftests/drivers/gpu/drm_fabric/switch_abi.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Intel Corporation
+"""
+fabricsim's "switch" shape links each leaf's first port to an opaque
+switch that is not a registered endpoint: asserts half-edge serialization
+and peer-id non-resolution, not leaf-switch-leaf reachability.
+
+--no-load is ignored (needs a fresh insmod). Run as root.
+"""
+
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import lib_drm_fabric as L
+
+
+def ports_of(fab, ep_id):
+ return [p["port"] for p in fab.dump("port-get", {"endpoint-id": ep_id})]
+
+
+def switch_peers(fab, sim_eps):
+ """All TYPE=switch peers across the sim leaves: [(ep, peer), ...]."""
+ out = []
+ for e in sim_eps:
+ for p in ports_of(fab, e["endpoint-id"]):
+ peer = p.get("peer")
+ if peer and peer.get("type") == "switch":
+ out.append((e, peer))
+ return out
+
+
+class Cfg:
+ def __init__(self, fab, sim_eps, sw_peers):
+ self.fab = fab
+ self.sim_eps = sim_eps
+ self.sw_peers = sw_peers
+ self.abort = False
+
+
+def test_every_leaf_has_switch_peer(ksft, cfg):
+ """Every leaf endpoint has a port with a TYPE=switch half-edge."""
+ ksft.check(len(cfg.sw_peers) == len(cfg.sim_eps),
+ "switch-every-leaf-has-switch-peer",
+ "switch-peers=%d leaves=%d"
+ % (len(cfg.sw_peers), len(cfg.sim_eps)))
+ if not cfg.sw_peers:
+ cfg.abort = True
+
+
+def test_half_edge_fully_serialized(ksft, cfg):
+ """Every switch half-edge carries all three peer fields."""
+ complete = all(
+ {"peer-id", "type", "port-index"} <= set(peer)
+ for _, peer in cfg.sw_peers)
+ ksft.check(complete, "switch-half-edge-fully-serialized",
+ "a switch peer is missing peer-id/type/port-index")
+
+
+def test_single_opaque_switch_id(ksft, cfg):
+ """All leaves name one opaque switch id, each via a distinct switch port."""
+ ids = {peer["peer-id"] for _, peer in cfg.sw_peers}
+ ports = [peer["port-index"] for _, peer in cfg.sw_peers]
+ ksft.check(len(ids) == 1, "switch-single-opaque-id",
+ "switch peer-ids=%s" % sorted(ids))
+ ksft.check(len(set(ports)) == len(ports), "switch-distinct-switch-ports",
+ "switch-side port-indexes=%s" % sorted(ports))
+
+
+def test_switch_id_does_not_resolve(ksft, cfg):
+ """The opaque switch id is not a registered endpoint (local adjacency)."""
+ ep_fepids = {e["fabric-ep-id"] for e in cfg.sim_eps}
+ sw_ids = {peer["peer-id"] for _, peer in cfg.sw_peers}
+ leaked = sw_ids & ep_fepids
+ ksft.check(not leaked, "switch-id-does-not-resolve-to-endpoint",
+ "switch id resolves to an endpoint fabric-ep-id: %s"
+ % sorted(leaked))
+
+
+CASES = (
+ test_every_leaf_has_switch_peer,
+ test_half_edge_fully_serialized,
+ test_single_opaque_switch_id,
+ test_switch_id_does_not_resolve,
+)
+
+
+def main():
+ ksft = L.Ksft()
+
+ # The switch shape is an insmod parameter, so fabricsim is always reloaded
+ # with topology=switch (ignoring --no-load) and restored to default on exit.
+ with L.fabricsim(ksft, topology="switch") as fab:
+ eps = [e["endpoint"] for e in fab.dump("endpoint-get", {})]
+ # Restrict to fabricsim's members (ignore anything a prior suite left).
+ sim_eps = [e for e in eps if e["name"].startswith("sim-ep")]
+ if len(sim_eps) < 2:
+ ksft.skip_all("switch topology needs >= 2 endpoints, got %d"
+ % len(sim_eps))
+
+ sw_peers = switch_peers(fab, sim_eps)
+ L.run_cases(ksft, Cfg(fab, sim_eps, sw_peers), CASES)
+ ksft.finish()
+
+
+if __name__ == "__main__":
+ main()
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 07/12] drm/fabric: add topology-provisioning core
2026-08-24 8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
` (5 preceding siblings ...)
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
2026-08-24 8:09 ` [RFC PATCH 08/12] drm/fabric: add provisioning netlink uAPI Konstantin Sinyuk
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
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
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH 08/12] drm/fabric: add provisioning netlink uAPI
2026-08-24 8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
` (6 preceding siblings ...)
2026-08-24 8:09 ` [RFC PATCH 07/12] drm/fabric: add topology-provisioning core Konstantin Sinyuk
@ 2026-08-24 8:09 ` Konstantin Sinyuk
7 siblings, 0 replies; 9+ messages in thread
From: Konstantin Sinyuk @ 2026-08-24 8:09 UTC (permalink / raw)
To: dri-devel
Cc: Maarten Lankhorst, Francois Dugast, David Airlie, Simona Vetter,
Maxime Ripard, Thomas Zimmermann, Jonathan Corbet, Shuah Khan,
Donald Hunter, Jakub Kicinski, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Ilia Levi, Rodrigo Vivi, linux-doc,
linux-kselftest, netdev, linux-kernel
From: Ilia Levi <ilia.levi@intel.com>
Extend the YNL specification with six privileged provisioning commands.
FABRIC_NEW and FABRIC_DEL create and delete empty userspace-owned
fabrics; FABRIC_NEW carries a fabric-new-params nest with the type, name
and instance ID. ENDPOINT_SET changes fabric membership and endpoint
administrative state, PORT_SET changes port administrative state, and
PORT_PEER_NEW and PORT_PEER_DEL manage peer adjacency. All six require
GENL_ADMIN_PERM.
A peer request carries its type-qualified identity and far-end port
index. The descriptor is stored without resolving it to a local object,
as documented in Documentation/gpu/drm-fabric.rst.
Add an endpoint-change notification and extend the existing port-change
and peer notifications to cover userspace-requested transitions.
Generate the headers from the tree root with:
tools/net/ynl/pyynl/ynl_gen_c.py --mode uapi --header \
--spec Documentation/netlink/specs/drm_fabric.yaml \
-o include/uapi/drm/drm_fabric.h
tools/net/ynl/pyynl/ynl_gen_c.py --mode kernel --header \
--spec Documentation/netlink/specs/drm_fabric.yaml \
-o drivers/gpu/drm/fabric/drm_fabric_nl.h
The generated source names the six doit handlers, so it lands with their
implementation in the next patch. Drop the temporary administrative-state
definitions now that the generated headers provide them.
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 | 117 ++++++++++++---
Documentation/netlink/specs/drm_fabric.yaml | 155 +++++++++++++++++++-
drivers/gpu/drm/fabric/drm_fabric_nl.h | 30 ++++
include/drm/drm_fabric.h | 9 +-
include/uapi/drm/drm_fabric.h | 28 ++++
5 files changed, 312 insertions(+), 27 deletions(-)
diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index 61e2c18bf0b5..4ee7cb7ef6ac 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -87,9 +87,11 @@ 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
-is not evidence the far end is still reachable.
+``port-peer-delete-ntf`` reports an explicitly retracted half-edge -- through
+the provider's own report or, on a userspace-managed port, a userspace
+``port-peer-del`` request -- and is never emitted when a peer merely becomes
+locally unresolvable, so its absence is not evidence the far end is still
+reachable.
Endpoint teardown removes the endpoint's owned half-edges without
generating a separate event for each port: the delete already describes
@@ -170,9 +172,9 @@ 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.
+A fabric records whether it was created by a provider
+(drm_fabric_register()) or by userspace (``fabric-new``); see
+`Provisioning`_ for the deletion rules.
Generic Netlink family
======================
@@ -216,8 +218,9 @@ User space enumerates topology with four read-only commands, each supporting a
single lookup (``do``) and a bulk dump (``dump``):
* ``fabric-get`` -- enumerate fabrics (``do`` by ``fabric-id``, ``dump`` for all).
-* ``endpoint-get`` -- enumerate endpoints, optionally filtered by ``fabric-id``,
- or resolve one by ``endpoint-id`` or backing ``dev-name``/``bus-name``.
+* ``endpoint-get`` -- enumerate endpoints, optionally filtered by ``fabric-id``
+ (0 selects orphans), or resolve one by ``endpoint-id`` or by its backing
+ ``dev-name`` with an optional ``bus-name``.
* ``port-get`` -- enumerate ports, filtered by ``endpoint-id``. Ports may report
the provider's maximum capability as ``max-lane-count`` and
``max-lane-signaling-rate-mbps`` (zero means unknown); these are maxima, not
@@ -243,10 +246,10 @@ a partial payload and are declared with ``event:``.
- Trigger
- Payload
* - ``fabric-create-ntf``
- - a fabric is registered by a provider
+ - a fabric is registered (provider or ``fabric-new``)
- reuses ``fabric-get``
* - ``fabric-delete-ntf``
- - a provider-owned fabric is unregistered
+ - a fabric is unregistered or removed via ``fabric-del``
- reuses ``fabric-get``
* - ``endpoint-create-ntf``
- an endpoint is registered
@@ -254,15 +257,19 @@ a partial payload and are declared with ``event:``.
* - ``endpoint-delete-ntf``
- an endpoint is unregistered
- reuses ``endpoint-get``
+ * - ``endpoint-change-ntf``
+ - an endpoint's fabric attachment or administrative state changes
+ - reuses ``endpoint-get``
* - ``port-change-ntf``
- - a port's operational state changes
+ - a port's operational or administrative state changes
- reuses ``port-get``
* - ``port-peer-create-ntf``
- - a port's peer descriptor is set by its provider
+ - a port's peer descriptor is set (provider or ``port-peer-new``)
- partial (``event:``)
* - ``port-peer-delete-ntf``
- - a port's peer descriptor is explicitly unset by its provider; never
- emitted for an implicit half-edge loss (see `Peer semantics`_)
+ - a port's peer descriptor is explicitly unset (provider or
+ ``port-peer-del``); never emitted for an implicit half-edge loss
+ (see `Peer semantics`_)
- partial (``event:``)
Notifications are best-effort. A listener that detects loss, restarts, or
@@ -274,9 +281,9 @@ Topology generation and dump consistency
The core keeps a nonzero generation counter and advances it whenever topology or
exposed state changes. It is surfaced as the ``topology-generation`` attribute on
``fabric-get``, ``endpoint-get`` and ``port-get`` replies and on the topology
-notifications. A provider-reported change advances the generation before its
-notification is serialized, so an event carries the post-change value that a
-later ``get``/``dump`` will also report.
+notifications. A committed change -- provider-reported or userspace mutation
+-- advances the generation before its notification is serialized, so an event
+carries the post-change value that a later ``get``/``dump`` will also report.
``topology-generation`` is a change token, not a timestamp, liveness counter or
event count: a changed value means topology changed, but the delta between two
@@ -308,8 +315,8 @@ Monitor notifications are likewise emitted only into ``init_net``, so a listener
that joins the multicast group from another network namespace never receives
them.
-Examples
-========
+Query examples
+--------------
Query the topology with the in-tree YNL tool, pointing it at the spec:
@@ -364,6 +371,78 @@ Query a single port:
The family name on the wire is ``drm-fabric``.
+Provisioning
+============
+
+The provisioning interface requires ``CAP_NET_ADMIN``. ``fabric-new`` and
+``fabric-del`` are core-owned; the rest are serviced by the provider and
+return ``-EOPNOTSUPP`` where it does not implement the matching callback:
+
+* ``fabric-new`` -- create an empty, userspace-owned fabric. The request
+ carries a ``fabric-new-params`` nest with the fabric type, an optional name
+ and a provider-defined instance ID.
+* ``fabric-del`` -- delete an empty, userspace-owned fabric. Refuses a
+ provider-owned fabric (``-EPERM``) and a non-empty one (``-EBUSY``).
+* ``endpoint-set`` -- attach an orphan endpoint, selected by ``endpoint-id`` or
+ by its backing ``dev-name`` with an optional ``bus-name``, to a fabric;
+ detach it with ``fabric-id`` 0; and/or set its administrative state.
+* ``port-set`` -- set a port's administrative state.
+* ``port-peer-new`` / ``port-peer-del`` -- set or unset a port's neighbor
+ half-edge. Valid only on a userspace-managed port; a provider-managed port
+ returns ``-EOPNOTSUPP`` (see `Peer management mode`_).
+
+Mutation commands carry ``GENL_ADMIN_PERM`` and are additionally confined to
+``init_net``. ``GENL_ADMIN_PERM`` only checks ``CAP_NET_ADMIN`` in the caller's
+user namespace, which a process in a nested user and network namespace may
+hold; restricting mutators to ``init_net`` prevents container-local privilege
+from reconfiguring host-global topology.
+
+A userspace-created fabric additionally pins this module for as long as it
+remains registered: ``fabric-new`` takes the pin, ``fabric-del`` releases it.
+
+Peer management mode
+--------------------
+
+A port uses one peer-management mode, fixed by the provider at registration.
+
+In provider-managed mode, the provider reports peer creation and removal
+(drm_fabric_port_set_peer() / drm_fabric_port_unset_peer()) and userspace peer
+provisioning is unsupported (``port-peer-new`` / ``port-peer-del`` return
+``-EOPNOTSUPP``).
+
+In userspace-managed mode, peer creation and removal are requested through the
+provisioning API and the provider does not independently replace or retract the
+peer (drm_fabric_port_set_peer() / drm_fabric_port_unset_peer() return
+``-EOPNOTSUPP``).
+
+One authority per descriptor prevents concurrent updates from both sources.
+Setting an occupied port returns ``-EEXIST``; unsetting an empty port returns
+``-ENOENT``.
+
+Provisioning examples
+---------------------
+
+Provision with the in-tree YNL tool (root, initial namespace):
+
+.. code-block:: bash
+
+ # Create an empty fabric
+ ./tools/net/ynl/pyynl/cli.py \
+ --spec Documentation/netlink/specs/drm_fabric.yaml \
+ --do fabric-new \
+ --json '{"fabric-new-params": {"type": "synthetic", "instance-id": 42}}'
+
+The command returns a core-assigned ``fabric-id``. Use that value in the
+following requests.
+
+.. code-block:: bash
+
+ # Attach an orphan endpoint to the returned fabric
+ ./tools/net/ynl/pyynl/cli.py \
+ --spec Documentation/netlink/specs/drm_fabric.yaml \
+ --do endpoint-set \
+ --json '{"endpoint-id": 5, "fabric-id": <returned-fabric-id>}'
+
Synthetic provider
==================
diff --git a/Documentation/netlink/specs/drm_fabric.yaml b/Documentation/netlink/specs/drm_fabric.yaml
index 778443d92a0d..0c9a56f4b387 100644
--- a/Documentation/netlink/specs/drm_fabric.yaml
+++ b/Documentation/netlink/specs/drm_fabric.yaml
@@ -18,6 +18,16 @@ doc: |
Replies and notifications carry a topology-generation token so an
interrupted or changed dump can be discarded and retried.
+ The provisioning surface (fabric-new, fabric-del, endpoint-set, port-set,
+ port-peer-new and port-peer-del) requires administrative permission and is
+ restricted to the initial network namespace. fabric-new and fabric-del are
+ implemented by the core and do not depend on a provider callback. The rest
+ are serviced by the provider and return -EOPNOTSUPP where it does not
+ implement the matching one.
+
+ The interface is extensible: new fabric types and attributes can be added
+ without breaking the uAPI.
+
definitions:
-
name: type
@@ -46,6 +56,13 @@ definitions:
doc: Kind of device directly adjacent at a port's far end.
entries: [accel, switch]
+ -
+ name: admin-state
+ type: enum
+ value-start: 1
+ doc: Administrative (requested) state of an endpoint or port.
+ entries: [down, up]
+
attribute-sets:
-
name: drm-fabric
@@ -108,6 +125,17 @@ attribute-sets:
or DUMP reply carries its snapshot's generation; a notification
carries the generation of its change. Statistics reads do not
advance it.
+ # Provisioning request parameters
+ -
+ name: admin-state
+ type: u32
+ enum: admin-state
+ doc: Requested administrative state.
+ -
+ name: fabric-new-params
+ type: nest
+ nested-attributes: fabric-new-params
+ doc: Creation parameters for ``fabric-new`` (full nest).
-
name: fabric
@@ -136,6 +164,30 @@ attribute-sets:
type: u64
doc: Vendor-unique fabric instance identifier (e.g. a hive ID).
+ -
+ name: fabric-new-params
+ name-prefix: drm-fabric-a-fabric-new-params-
+ enum-name: drm-fabric-a-fabric-new-params
+ doc: Creation parameters carried by a ``fabric-new`` request.
+ attributes:
+ -
+ name: type
+ type: u32
+ enum: type
+ doc: Fabric interconnect technology, see enum type.
+ -
+ name: name
+ type: string
+ checks:
+ max-len: 31
+ doc: >-
+ Human-readable fabric name. Bounded to the core's storage so an
+ over-long name is rejected rather than silently truncated.
+ -
+ name: instance-id
+ type: u64
+ doc: Vendor-unique fabric instance identifier (e.g. a hive ID).
+
-
name: endpoint
name-prefix: drm-fabric-a-endpoint-attrs-
@@ -152,7 +204,7 @@ attribute-sets:
-
name: fabric-id
type: u32
- doc: Identifier of the parent fabric.
+ doc: Identifier of the parent fabric, 0 while orphaned.
-
name: fabric-ep-id
type: u64
@@ -169,6 +221,11 @@ attribute-sets:
name: bus-name
type: string
doc: The bus of the backing device (e.g. pci).
+ -
+ name: admin-state
+ type: u32
+ enum: admin-state
+ doc: Endpoint's administrative state.
-
name: port
@@ -209,6 +266,11 @@ attribute-sets:
type: nest
nested-attributes: peer
doc: Peer info, absent if the port has no neighbor set.
+ -
+ name: admin-state
+ type: u32
+ enum: admin-state
+ doc: Port's administrative state.
-
name: peer
@@ -421,6 +483,97 @@ operations:
reply shape (full fabric nest).
notify: fabric-get
+ -
+ name: endpoint-change-ntf
+ doc: |
+ Endpoint state change notification.
+ Reuses the endpoint-get reply shape (full endpoint nest).
+ notify: endpoint-get
+
+ -
+ name: fabric-new
+ doc: Create a new empty fabric
+ attribute-set: drm-fabric
+ flags: [admin-perm]
+ do:
+ pre: drm-fabric-nl-pre-doit
+ post: drm-fabric-nl-post-doit
+ request:
+ attributes:
+ - fabric-new-params
+ reply:
+ attributes:
+ - fabric-id
+
+ -
+ name: fabric-del
+ doc: Delete an empty fabric
+ attribute-set: drm-fabric
+ flags: [admin-perm]
+ do:
+ pre: drm-fabric-nl-pre-doit
+ post: drm-fabric-nl-post-doit
+ request:
+ attributes:
+ - fabric-id
+
+ -
+ name: endpoint-set
+ doc: Attach/detach an endpoint to a fabric and/or set its admin state
+ attribute-set: drm-fabric
+ flags: [admin-perm]
+ do:
+ pre: drm-fabric-nl-endpoint-pre-doit
+ post: drm-fabric-nl-endpoint-post-doit
+ request:
+ attributes:
+ - endpoint-id
+ - dev-name
+ - bus-name
+ - fabric-id
+ - admin-state
+
+ -
+ name: port-set
+ doc: Set a port's administrative state
+ attribute-set: drm-fabric
+ flags: [admin-perm]
+ do:
+ pre: drm-fabric-nl-port-pre-doit
+ post: drm-fabric-nl-port-post-doit
+ request:
+ attributes:
+ - endpoint-id
+ - port-index
+ - admin-state
+
+ -
+ name: port-peer-new
+ doc: Set a port's neighbor
+ attribute-set: drm-fabric
+ flags: [admin-perm]
+ do:
+ pre: drm-fabric-nl-port-pre-doit
+ post: drm-fabric-nl-port-post-doit
+ request:
+ attributes:
+ - endpoint-id
+ - port-index
+ - peer
+
+ -
+ name: port-peer-del
+ doc: Unset a port's neighbor
+ attribute-set: drm-fabric
+ flags: [admin-perm]
+ do:
+ pre: drm-fabric-nl-port-pre-doit
+ post: drm-fabric-nl-port-post-doit
+ request:
+ attributes:
+ - endpoint-id
+ - port-index
+
mcast-groups:
list:
-
diff --git a/drivers/gpu/drm/fabric/drm_fabric_nl.h b/drivers/gpu/drm/fabric/drm_fabric_nl.h
index 8cedc004260f..03b3d22f180c 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_nl.h
+++ b/drivers/gpu/drm/fabric/drm_fabric_nl.h
@@ -12,6 +12,27 @@
#include <uapi/drm/drm_fabric.h>
+/* Common nested types */
+extern const struct nla_policy drm_fabric_fabric_new_params_nl_policy[DRM_FABRIC_A_FABRIC_NEW_PARAMS_INSTANCE_ID + 1];
+extern const struct nla_policy drm_fabric_peer_nl_policy[DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX + 1];
+
+int drm_fabric_nl_pre_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info);
+int drm_fabric_nl_endpoint_pre_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb,
+ struct genl_info *info);
+int drm_fabric_nl_port_pre_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info);
+void
+drm_fabric_nl_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
+ struct genl_info *info);
+void
+drm_fabric_nl_endpoint_post_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info);
+void
+drm_fabric_nl_port_post_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info);
+
int drm_fabric_nl_fabric_get_doit(struct sk_buff *skb, struct genl_info *info);
int drm_fabric_nl_fabric_get_dumpit(struct sk_buff *skb,
struct netlink_callback *cb);
@@ -26,6 +47,15 @@ int drm_fabric_nl_port_stats_get_doit(struct sk_buff *skb,
struct genl_info *info);
int drm_fabric_nl_port_stats_get_dumpit(struct sk_buff *skb,
struct netlink_callback *cb);
+int drm_fabric_nl_fabric_new_doit(struct sk_buff *skb, struct genl_info *info);
+int drm_fabric_nl_fabric_del_doit(struct sk_buff *skb, struct genl_info *info);
+int drm_fabric_nl_endpoint_set_doit(struct sk_buff *skb,
+ struct genl_info *info);
+int drm_fabric_nl_port_set_doit(struct sk_buff *skb, struct genl_info *info);
+int drm_fabric_nl_port_peer_new_doit(struct sk_buff *skb,
+ struct genl_info *info);
+int drm_fabric_nl_port_peer_del_doit(struct sk_buff *skb,
+ struct genl_info *info);
enum {
DRM_FABRIC_NLGRP_MONITOR,
diff --git a/include/drm/drm_fabric.h b/include/drm/drm_fabric.h
index 69e9099a1e57..0479b6435a0e 100644
--- a/include/drm/drm_fabric.h
+++ b/include/drm/drm_fabric.h
@@ -4,8 +4,8 @@
*/
/*
- * DRM Fabric driver API: common object model for GPU interconnect topology
- * (fabric, endpoint, port and peer relationships).
+ * Common object model for GPU interconnect topology: fabric, endpoint, port
+ * and peer relationships.
*/
#ifndef __DRM_FABRIC_H__
@@ -18,11 +18,6 @@
#include <uapi/drm/drm_fabric.h>
-enum drm_fabric_admin_state {
- DRM_FABRIC_ADMIN_STATE_DOWN = 1,
- DRM_FABRIC_ADMIN_STATE_UP,
-};
-
struct device;
/**
diff --git a/include/uapi/drm/drm_fabric.h b/include/uapi/drm/drm_fabric.h
index b6c7bd6e35f0..e004200423e8 100644
--- a/include/uapi/drm/drm_fabric.h
+++ b/include/uapi/drm/drm_fabric.h
@@ -36,6 +36,14 @@ enum drm_fabric_peer_type {
DRM_FABRIC_PEER_TYPE_SWITCH,
};
+/*
+ * Administrative (requested) state of an endpoint or port.
+ */
+enum drm_fabric_admin_state {
+ DRM_FABRIC_ADMIN_STATE_DOWN = 1,
+ DRM_FABRIC_ADMIN_STATE_UP,
+};
+
enum drm_fabric_a {
DRM_FABRIC_A_FABRIC = 1,
DRM_FABRIC_A_ENDPOINT,
@@ -48,6 +56,8 @@ enum drm_fabric_a {
DRM_FABRIC_A_BUS_NAME,
DRM_FABRIC_A_PEER,
DRM_FABRIC_A_TOPOLOGY_GENERATION,
+ DRM_FABRIC_A_ADMIN_STATE,
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS,
__DRM_FABRIC_A_MAX,
DRM_FABRIC_A_MAX = (__DRM_FABRIC_A_MAX - 1)
@@ -64,6 +74,15 @@ enum drm_fabric_a_fabric_attrs {
DRM_FABRIC_A_FABRIC_ATTRS_MAX = (__DRM_FABRIC_A_FABRIC_ATTRS_MAX - 1)
};
+enum drm_fabric_a_fabric_new_params {
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS_TYPE = 1,
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS_NAME,
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS_INSTANCE_ID,
+
+ __DRM_FABRIC_A_FABRIC_NEW_PARAMS_MAX,
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS_MAX = (__DRM_FABRIC_A_FABRIC_NEW_PARAMS_MAX - 1)
+};
+
enum drm_fabric_a_endpoint_attrs {
DRM_FABRIC_A_ENDPOINT_ATTRS_PAD = 1,
DRM_FABRIC_A_ENDPOINT_ATTRS_ENDPOINT_ID,
@@ -72,6 +91,7 @@ enum drm_fabric_a_endpoint_attrs {
DRM_FABRIC_A_ENDPOINT_ATTRS_NAME,
DRM_FABRIC_A_ENDPOINT_ATTRS_DEV_NAME,
DRM_FABRIC_A_ENDPOINT_ATTRS_BUS_NAME,
+ DRM_FABRIC_A_ENDPOINT_ATTRS_ADMIN_STATE,
__DRM_FABRIC_A_ENDPOINT_ATTRS_MAX,
DRM_FABRIC_A_ENDPOINT_ATTRS_MAX = (__DRM_FABRIC_A_ENDPOINT_ATTRS_MAX - 1)
@@ -84,6 +104,7 @@ enum drm_fabric_a_port_attrs {
DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_COUNT,
DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_SIGNALING_RATE_MBPS,
DRM_FABRIC_A_PORT_ATTRS_PEER,
+ DRM_FABRIC_A_PORT_ATTRS_ADMIN_STATE,
__DRM_FABRIC_A_PORT_ATTRS_MAX,
DRM_FABRIC_A_PORT_ATTRS_MAX = (__DRM_FABRIC_A_PORT_ATTRS_MAX - 1)
@@ -124,6 +145,13 @@ enum drm_fabric_cmd {
DRM_FABRIC_CMD_ENDPOINT_DELETE_NTF,
DRM_FABRIC_CMD_FABRIC_CREATE_NTF,
DRM_FABRIC_CMD_FABRIC_DELETE_NTF,
+ DRM_FABRIC_CMD_ENDPOINT_CHANGE_NTF,
+ DRM_FABRIC_CMD_FABRIC_NEW,
+ DRM_FABRIC_CMD_FABRIC_DEL,
+ DRM_FABRIC_CMD_ENDPOINT_SET,
+ DRM_FABRIC_CMD_PORT_SET,
+ DRM_FABRIC_CMD_PORT_PEER_NEW,
+ DRM_FABRIC_CMD_PORT_PEER_DEL,
__DRM_FABRIC_CMD_MAX,
DRM_FABRIC_CMD_MAX = (__DRM_FABRIC_CMD_MAX - 1)
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread