[RFC PATCH 04/12] drm/fabric: add read-only synthetic provider

Konstantin Sinyuk <[email protected]>
Newsgroups org.kernel.vger.linux-doc,org.freedesktop.lists.dri-devel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest,org.kernel.vger.netdev
Message-ID <e5c0fecf67dbe72f8dd6c49489833ebdf23c5a90.1787552412.git.ksinyuk@kernel.org>
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 <[email protected]>
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 <[email protected]>");
+MODULE_DESCRIPTION("DRM Fabric fabricsim synthetic driver");
+MODULE_LICENSE("Dual MIT/GPL");
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.