From: Ilia Levi 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 Co-developed-by: Konstantin Sinyuk Signed-off-by: Konstantin Sinyuk 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 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 +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