/* SPDX-License-Identifier: LGPL-2.1-or-later */ /* * Copyright (C) 2018, Google Inc. * * media_device.cpp - Media device handler */ #include "libcamera/internal/media_device.h" #include #include #include #include #include #include #include #include #include #include /** * \file media_device.h * \brief Provide a representation of a Linux kernel Media Controller device * that exposes the full graph topology. */ namespace libcamera { LOG_DEFINE_CATEGORY(MediaDevice) /** * \class MediaDevice * \brief The MediaDevice represents a Media Controller device with its full * graph of connected objects. * * A MediaDevice instance is associated with a media controller device node when * created, and that association is kept for the lifetime of the MediaDevice * instance. * * The instance is created with an empty media graph. Before performing any * other operation, it must be populate by calling populate(). Instances of * MediaEntity, MediaPad and MediaLink are created to model the media graph, * and stored in a map indexed by object id. * * The graph is valid once successfully populated, as reported by the isValid() * function. It can be queried to list all entities(), or entities can be * looked up by name with getEntityByName(). The graph can be traversed from * entity to entity through pads and links as exposed by the corresponding * classes. * * Media devices can be claimed for exclusive use with acquire(), released with * release() and tested with busy(). This mechanism is aimed at pipeline * managers to claim media devices they support during enumeration. */ /** * \brief Construct a MediaDevice * \param[in] deviceNode The media device node path * * Once constructed the media device is invalid, and must be populated with * populate() before the media graph can be queried. */ MediaDevice::MediaDevice(const std::string &deviceNode) : deviceNode_(deviceNode), fd_(-1), valid_(false), acquired_(false), lockOwner_(false) { } MediaDevice::~MediaDevice() { if (fd_ != -1) ::close(fd_); clear(); } std::string MediaDevice::logPrefix() const { return deviceNode() + "[" + driver() + "]"; } /** * \brief Claim a device for exclusive use * * The device claiming mechanism offers simple media device access arbitration * between multiple users. When the media device is created, it is available to * all users. Users can query the media graph to determine whether they can * support the device and, if they do, claim the device for exclusive use. Other * users are then expected to skip over media devices in use as reported by the * busy() function. * * Once claimed the device shall be released by its user when not needed anymore * by calling the release() function. Acquiring the media device opens a file * descriptor to the device which is kept open until release() is called. * * Exclusive access is only guaranteed if all users of the media device abide by * the device claiming mechanism, as it isn't enforced by the media device * itself. * * \return true if the device was successfully claimed, or false if it was * already in use * \sa release(), busy() */ bool MediaDevice::acquire() { if (acquired_) return false; if (open()) return false; acquired_ = true; return true; } /** * \brief Release a device previously claimed for exclusive use * \sa acquire(), busy() */ void MediaDevice::release() { close(); acquired_ = false; } /** * \brief Lock the device to prevent it from being used by other instances of * libcamera * * Multiple instances of libcamera might be running on the same system, at the * same time. To allow the different instances to coexist, system resources in * the form of media devices must be accessible for enumerating the cameras * they provide at all times, while still allowing an instance to lock a * resource while it prepares to actively use a camera from the resource. * * This method shall not be called from a pipeline handler implementation * directly, as the base PipelineHandler implementation handles this on the * behalf of the specified implementation. * * \return True if the device could be locked, false otherwise * \sa unlock() */ bool MediaDevice::lock() { if (fd_ == -1) return false; /* Do not allow nested locking in the same libcamera instance. */ if (lockOwner_) return false; if (lockf(fd_, F_TLOCK, 0)) return false; lockOwner_ = true; return true; } /** * \brief Unlock the device and free it for use for libcamera instances * * This method shall not be called from a pipeline handler implementation * directly, as the base PipelineHandler implementation handles this on the * behalf of the specified implementation. * * \sa lock() */ void MediaDevice::unlock() { if (fd_ == -1) return; if (!lockOwner_) return; lockOwner_ = false; lockf(fd_, F_ULOCK, 0); } /** * \fn MediaDevice::busy() * \brief Check if a device is in use * \return true if the device has been claimed for exclusive use, or false if it * is available * \sa acquire(), release() */ /** * \brief Populate the MediaDevice with device information and media objects * * This function retrieves the media device information and enumerates all * media objects in the media device graph and creates their MediaObject * representations. All entities, pads and links are stored as MediaEntity, * MediaPad and MediaLink respectively, with cross-references between objects. * Interfaces are not processed. * * Entities are stored in a separate list in the MediaDevice to ease lookup, * while pads are accessible from the entity they belong to and links from the * pads they connect. * * \return 0 on success or a negative error code otherwise */ int MediaDevice::populate() { struct media_v2_topology topology = {}; struct media_v2_entity *ents = nullptr; struct media_v2_interface *interfaces = nullptr; struct media_v2_link *links = nullptr; struct media_v2_pad *pads = nullptr; __u64 version = -1; int ret; clear(); ret = open(); if (ret) return ret; struct media_device_info info = {}; ret = ioctl(fd_, MEDIA_IOC_DEVICE_INFO, &info); if (ret) { ret = -errno; LOG(MediaDevice, Error) << "Failed to get media device info " << strerror(-ret); goto done; } driver_ = info.driver; model_ = info.model; version_ = info.media_version; hwRevision_ = info.hw_revision; /* * Keep calling G_TOPOLOGY until the version number stays stable. */ while (true) { topology.topology_version = 0; topology.ptr_entities = reinterpret_cast(ents); topology.ptr_interfaces = reinterpret_cast(interfaces); topology.ptr_links = reinterpret_cast(links); topology.ptr_pads = reinterpret_cast(pads); ret = ioctl(fd_, MEDIA_IOC_G_TOPOLOGY, &topology); if (ret < 0) { ret = -errno; LOG(MediaDevice, Error) << "Failed to enumerate topology: " << strerror(-ret); goto done; } if (version == topology.topology_version) break; delete[] ents; delete[] interfaces; delete[] pads; delete[] links; ents = new struct media_v2_entity[topology.num_entities](); interfaces = new struct media_v2_interface[topology.num_interfaces](); links = new struct media_v2_link[topology.num_links](); pads = new struct media_v2_pad[topology.num_pads](); version = topology.topology_version; } /* Populate entities, pads and links. */ if (populateEntities(topology) && populatePads(topology) && populateLinks(topology)) valid_ = true; ret = 0; done: close(); delete[] ents; delete[] interfaces; delete[] pads; delete[] links; if (!valid_) { clear(); return -EINVAL; } return ret; } /** * \fn MediaDevice::isValid() * \brief Query whether the media graph has been populated and is valid * \return true if the media graph is valid, false otherwise */ /** * \fn MediaDevice::driver() * \brief Retrieve the media device driver name * \return The name of the kernel driver that handles the MediaDevice */ /** * \fn MediaDevice::deviceNode() * \brief Retrieve the media device node path * \return The MediaDevice deviceNode path */ /** * \fn MediaDevice::model() * \brief Retrieve the media device model name * \return The MediaDevice model name */ /** * \fn MediaDevice::version() * \brief Retrieve the media device API version * * The version is formatted with the KERNEL_VERSION() macro. * * \return The MediaDevice API version */ /** * \fn MediaDevice::hwRevision() * \brief Retrieve the media device hardware revision * * The hardware revision is in a driver-specific format. * * \return The MediaDevice hardware revision */ /** * \fn MediaDevice::entities() * \brief Retrieve the list of entities in the media graph * \return The list of MediaEntities registered in the MediaDevice */ /** * \brief Return the MediaEntity with name \a name * \param[in] name The entity name * \return The entity with \a name, or nullptr if no such entity is found */ MediaEntity *MediaDevice::getEntityByName(const std::string &name) const { for (MediaEntity *e : entities_) if (e->name() == name) return e; return nullptr; } /** * \brief Retrieve the MediaLink connecting two pads, identified by entity * names and pad indexes * \param[in] sourceName The source entity name * \param[in] sourceIdx The index of the source pad * \param[in] sinkName The sink entity name * \param[in] sinkIdx The index of the sink pad * * Find the link that connects the pads at index \a sourceIdx of the source * entity with name \a sourceName, to the pad at index \a sinkIdx of the * sink entity with name \a sinkName, if any. * * \sa MediaDevice::link(const MediaEntity *source, unsigned int sourceIdx, const MediaEntity *sink, unsigned int sinkIdx) const * \sa MediaDevice::link(const MediaPad *source, const MediaPad *sink) const * * \return The link that connects the two pads, or nullptr if no such a link * exists */ MediaLink *MediaDevice::link(const std::string &sourceName, unsigned int sourceIdx, const std::string &sinkName, unsigned int sinkIdx) { const MediaEntity *source = getEntityByName(sourceName); const MediaEntity *sink = getEntityByName(sinkName); if (!source || !sink) return nullptr; return link(source, sourceIdx, sink, sinkIdx); } /** * \brief Retrieve the MediaLink connecting two pads, identified by the * entities they belong to and pad indexes * \param[in] source The source entity * \param[in] sourceIdx The index of the source pad * \param[in] sink The sink entity * \param[in] sinkIdx The index of the sink pad * * Find the link that connects the pads at index \a sourceIdx of the source * entity \a source, to the pad at index \a sinkIdx of the sink entity \a * sink, if any. * * \sa MediaDevice::link(const std::string &sourceName, unsigned int sourceIdx, const std::string &sinkName, unsigned int sinkIdx) const * \sa MediaDevice::link(const MediaPad *source, const MediaPad *sink) const * * \return The link that connects the two pads, or nullptr if no such a link * exists */ MediaLink *MediaDevice::link(const MediaEntity *source, unsigned int sourceIdx, const MediaEntity *sink, unsigned int sinkIdx) { const MediaPad *sourcePad = source->getPadByIndex(sourceIdx); const MediaPad *sinkPad = sink->getPadByIndex(sinkIdx); if (!sourcePad || !sinkPad) return nullptr; return link(sourcePad, sinkPad); } /** * \brief Retrieve the MediaLink that connects two pads * \param[in] source The source pad * \param[in] sink The sink pad * * \sa MediaDevice::link(const std::string &sourceName, unsigned int sourceIdx, const std::string &sinkName, unsigned int sinkIdx) const * \sa MediaDevice::link(const MediaEntity *source, unsigned int sourceIdx, const MediaEntity *sink, unsigned int sinkIdx) const * * \return The link that connects the two pads, or nullptr if no such a link * exists */ MediaLink *MediaDevice::link(const MediaPad *source, const MediaPad *sink) { for (MediaLink *link : source->links()) { if (link->sink()->id() == sink->id()) return link; } return nullptr; } /** * \brief Disable all links in the media device * * Disable all the media device links, clearing the MEDIA_LNK_FL_ENABLED flag * on links which are not flagged as IMMUTABLE. * * \return 0 on success or a negative error code otherwise */ int MediaDevice::disableLinks() { for (MediaEntity *entity : entities_) { for (MediaPad *pad : entity->pads()) { if (!(pad->flags() & MEDIA_PAD_FL_SOURCE)) continue; for (MediaLink *link : pad->links()) { if (link->flags() & MEDIA_LNK_FL_IMMUTABLE) continue; int ret = link->setEnabled(false); if (ret) return ret; } } } return 0; } /** * \var MediaDevice::disconnected * \brief Signal emitted when the media device is disconnected from the system * * This signal is emitted when the device enumerator detects that the media * device has been removed from the system. For hot-pluggable devices this is * usually caused by physical device disconnection, but can also result from * driver unloading for most devices. The media device is passed as a parameter. */ /** * \brief Open the media device * * \return 0 on success or a negative error code otherwise * \retval -EBUSY Media device already open * \sa close() */ int MediaDevice::open() { if (fd_ != -1) { LOG(MediaDevice, Error) << "MediaDevice already open"; return -EBUSY; } int ret = ::open(deviceNode_.c_str(), O_RDWR); if (ret < 0) { ret = -errno; LOG(MediaDevice, Error) << "Failed to open media device at " << deviceNode_ << ": " << strerror(-ret); return ret; } fd_ = ret; return 0; } /** * \brief Close the media device * * This function closes the media device node. It does not invalidate the media * graph and all cached media objects remain valid and can be accessed normally. * Once closed no operation interacting with the media device node can be * performed until the device is opened again. * * Closing an already closed device is allowed and will not perform any * operation. * * \sa open() */ void MediaDevice::close() { if (fd_ == -1) return; ::close(fd_); fd_ = -1; } /** * \var MediaDevice::objects_ * \brief Global map of media objects (entities, pads, links) keyed by their * object id. */ /** * \brief Retrieve the media graph object specified by \a id * \return The graph object, or nullptr if no object with \a id is found */ MediaObject *MediaDevice::object(unsigned int id) { auto it = objects_.find(id); return (it == objects_.end()) ? nullptr : it->second; } /** * \brief Add a media object to the media graph * * If the \a object has a unique id it is added to the media graph, and its * lifetime will be managed by the media device. Otherwise the object isn't * added to the graph and the caller must delete it. * * \return true if the object was successfully added to the graph and false * otherwise */ bool MediaDevice::addObject(MediaObject *object) { if (objects_.find(object->id()) != objects_.end()) { LOG(MediaDevice, Error) << "Element with id " << object->id() << " already enumerated."; return false; } objects_[object->id()] = object; return true; } /** * \brief Delete all graph objects in the media device * * Clear the media graph and delete all the objects it contains. After this * function returns any previously obtained pointer to a media graph object * becomes invalid. * * The media device graph state is reset to invalid when the graph is cleared. * * \sa isValid() */ void MediaDevice::clear() { for (auto const &o : objects_) delete o.second; objects_.clear(); entities_.clear(); valid_ = false; } /** * \var MediaDevice::entities_ * \brief Global list of media entities in the media graph */ /** * \brief Find the interface associated with an entity * \param[in] topology The media topology as returned by MEDIA_IOC_G_TOPOLOGY * \param[in] entityId The entity id * \return A pointer to the interface if found, or nullptr otherwise */ struct media_v2_interface *MediaDevice::findInterface(const struct media_v2_topology &topology, unsigned int entityId) { struct media_v2_link *links = reinterpret_cast (topology.ptr_links); unsigned int ifaceId = 0; unsigned int i; for (i = 0; i < topology.num_links; ++i) { /* Search for the interface to entity link. */ if (links[i].sink_id != entityId) continue; if ((links[i].flags & MEDIA_LNK_FL_LINK_TYPE) != MEDIA_LNK_FL_INTERFACE_LINK) continue; ifaceId = links[i].source_id; break; } if (i == topology.num_links) return nullptr; struct media_v2_interface *ifaces = reinterpret_cast (topology.ptr_interfaces); for (i = 0; i < topology.num_interfaces; ++i) { if (ifaces[i].id == ifaceId) return &ifaces[i]; } return nullptr; } /* * For each entity in the media graph create a MediaEntity and store a * reference in the media device objects map and entities list. */ bool MediaDevice::populateEntities(const struct media_v2_topology &topology) { struct media_v2_entity *mediaEntities = reinterpret_cast (topology.ptr_entities); for (unsigned int i = 0; i < topology.num_entities; ++i) { struct media_v2_entity *ent = &mediaEntities[i]; /* * The media_v2_entity structure was missing the flag field before * v4.19. */ if (!MEDIA_V2_ENTITY_HAS_FLAGS(version_)) fixupEntityFlags(ent); /* * Find the interface linked to this entity to get the device * node major and minor numbers. */ struct media_v2_interface *iface = findInterface(topology, ent->id); MediaEntity *entity; if (iface) entity = new MediaEntity(this, ent, iface->devnode.major, iface->devnode.minor); else entity = new MediaEntity(this, ent); if (!addObject(entity)) { delete entity; return false; } entities_.push_back(entity); } return true; } bool MediaDevice::populatePads(const struct media_v2_topology &topology) { struct media_v2_pad *mediaPads = reinterpret_cast (topology.ptr_pads); for (unsigned int i = 0; i < topology.num_pads; ++i) { unsigned int entity_id = mediaPads[i].entity_id; /* Store a reference to this MediaPad in entity. */ MediaEntity *mediaEntity = dynamic_cast (object(entity_id)); if (!mediaEntity) { LOG(MediaDevice, Error) << "Failed to find entity with id: " << entity_id; return false; } MediaPad *pad = new MediaPad(&mediaPads[i], mediaEntity); if (!addObject(pad)) { delete pad; return false; } mediaEntity->addPad(pad); } return true; } bool MediaDevice::populateLinks(const struct media_v2_topology &topology) { struct media_v2_link *mediaLinks = reinterpret_cast (topology.ptr_links); for (unsigned int i = 0; i < topology.num_links; ++i) { /* * Skip links between entities and interfaces: we only care * about pad-2-pad links here. */ if ((mediaLinks[i].flags & MEDIA_LNK_FL_LINK_TYPE) == MEDIA_LNK_FL_INTERFACE_LINK) continue; /* Store references to source and sink pads in the link. */ unsigned int source_id = mediaLinks[i].source_id; MediaPad *source = dynamic_cast (object(source_id)); if (!source) { LOG(MediaDevice, Error) << "Failed to find pad with id: " << source_id; return false; } unsigned int sink_id = mediaLinks[i].sink_id; MediaPad *sink = dynamic_cast (object(sink_id)); if (!sink) { LOG(MediaDevice, Error) << "Failed to find pad with id: " << sink_id; return false; } MediaLink *link = new MediaLink(&mediaLinks[i], source, sink); if (!addObject(link)) { delete link; return false; } source->addLink(link); sink->addLink(link); } return true; } /** * \brief Fixup entity flags using the legacy API * \param[in] entity The entity * * This function is used as a fallback to query entity flags using the legacy * MEDIA_IOC_ENUM_ENTITIES ioctl when running on a kernel version that doesn't * provide them through the MEDIA_IOC_G_TOPOLOGY ioctl. */ void MediaDevice::fixupEntityFlags(struct media_v2_entity *entity) { struct media_entity_desc desc = {}; desc.id = entity->id; int ret = ioctl(fd_, MEDIA_IOC_ENUM_ENTITIES, &desc); if (ret < 0) { ret = -errno; LOG(MediaDevice, Debug) << "Failed to retrieve information for entity " << entity->id << ": " << strerror(-ret); return; } entity->flags = desc.flags; } /** * \brief Apply \a flags to a link between two pads * \param[in] link The link to apply flags to * \param[in] flags The flags to apply to the link * * This function applies the link \a flags (as defined by the MEDIA_LNK_FL_* * macros from the Media Controller API) to the given \a link. It implements * low-level link setup as it performs no checks on the validity of the \a * flags, and assumes that the supplied \a flags are valid for the link (e.g. * immutable links cannot be disabled). * * \sa MediaLink::setEnabled(bool enable) * * \return 0 on success or a negative error code otherwise */ int MediaDevice::setupLink(const MediaLink *link, unsigned int flags) { struct media_link_desc linkDesc = {}; MediaPad *source = link->source(); MediaPad *sink = link->sink(); linkDesc.source.entity = source->entity()->id(); linkDesc.source.index = source->index(); linkDesc.source.flags = MEDIA_PAD_FL_SOURCE; linkDesc.sink.entity = sink->entity()->id(); linkDesc.sink.index = sink->index(); linkDesc.sink.flags = MEDIA_PAD_FL_SINK; linkDesc.flags = flags; int ret = ioctl(fd_, MEDIA_IOC_SETUP_LINK, &linkDesc); if (ret) { ret = -errno; LOG(MediaDevice, Error) << "Failed to setup link: " << strerror(-ret); return ret; } LOG(MediaDevice, Debug) << source->entity()->name() << "[" << source->index() << "] -> " << sink->entity()->name() << "[" << sink->index() << "]: " << flags; return 0; } } /* namespace libcamera */