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 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** + * 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 "); +MODULE_DESCRIPTION("DRM Fabric fabricsim synthetic driver"); +MODULE_LICENSE("Dual MIT/GPL"); -- 2.43.0