[libcamera-devel] [PATCH 06/14] libcamera: converter: Add v4l2 m2m converter implementation
Xavier Roumegue (OSS)
xavier.roumegue at oss.nxp.com
Mon Sep 19 10:45:59 CEST 2022
Hi Jacopo,
On 9/15/22 11:46, Jacopo Mondi wrote:
> Hi Xavier
>
> On Thu, Sep 08, 2022 at 08:48:42PM +0200, Xavier Roumegue via libcamera-devel wrote:
>> Introduce a converter implementation relying on a v4l2 m2m device, mostly
>> based on the current simple pipeline converter implementation.
>>
>> The main change is the introduction of Mapping object which can be
>> loaded through a configuration file which define vertices remapping
>> coordinates. Those latters can be applied by any classes derived from
>> this base class which define the apply_mapping() method.
>>
>
> I'll skip questions on things not clear to me about the converter
> implementation as I understand this basically comes from the simple
> pipeline converter implementation [*]
>
> I would then focus more on two general questions:
> 1) Do we want to make a dir for converter/post-processors instead of
> placing them in src/libcamera
Might make sense, or could be done later - that's not my call.
I can move them to a dedicated folder on the next patch set version if there is
a willing to do so.
>
> 2) Is the format expected in the configuration file documented
> anywhere ?
Nope. I will do it on the next version, hopefully on an agreed configuration
file format.
>
> [*] Not true, I will ask a question anyway, see below :)
>
>> Signed-off-by: Xavier Roumegue <xavier.roumegue at oss.nxp.com>
>> ---
>> .../libcamera/internal/converter_v4l2_m2m.h | 120 +++++
>> include/libcamera/internal/meson.build | 1 +
>> src/libcamera/converter_v4l2_m2m.cpp | 504 ++++++++++++++++++
>> src/libcamera/meson.build | 1 +
>> 4 files changed, 626 insertions(+)
>> create mode 100644 include/libcamera/internal/converter_v4l2_m2m.h
>> create mode 100644 src/libcamera/converter_v4l2_m2m.cpp
>>
>> diff --git a/include/libcamera/internal/converter_v4l2_m2m.h b/include/libcamera/internal/converter_v4l2_m2m.h
>> new file mode 100644
>> index 00000000..3667b128
>> --- /dev/null
>> +++ b/include/libcamera/internal/converter_v4l2_m2m.h
>> @@ -0,0 +1,120 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright 2022 NXP
>> + *
>> + * converter_v4l2_m2m.h - V4l2 M2M Format converter interface
>> + */
>> +
>> +#pragma once
>> +
>> +#include <functional>
>> +#include <map>
>> +#include <memory>
>> +#include <string>
>> +#include <tuple>
>> +#include <vector>
>> +
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/signal.h>
>> +
>> +#include <libcamera/geometry.h>
>> +#include <libcamera/pixel_format.h>
>> +
>> +#include "libcamera/internal/converter.h"
>> +
>> +namespace libcamera {
>> +
>> +class FrameBuffer;
>> +class MediaDevice;
>> +class Size;
>> +class SizeRange;
>> +struct StreamConfiguration;
>> +class V4L2M2MDevice;
>> +class V4L2M2MConverter;
>> +class Converter;
>> +
>> +class V4L2M2MConverter : public Converter
>> +{
>> +protected:
>> + class Mapping
>> + {
>> + public:
>> + Mapping(const Size &input, const Size &output, const std::vector<uint32_t> &map)
>> + : input_(input), output_(output), map_(map) {}
>> + Size getInputSize() const { return input_; }
>> + Size getOutputSize() const { return output_; }
>> + std::size_t getLength() const { return map_.size(); }
>> + const uint32_t *getMapping() const { return map_.data(); }
>> +
>> + private:
>> + Size input_;
>> + Size output_;
>> + std::vector<uint32_t> map_;
>> + };
>> +
>> + class Stream : protected Loggable
>> + {
>> + public:
>> + Stream(V4L2M2MConverter *converter, unsigned int index);
>> +
>> + bool isValid() const { return m2m_ != nullptr; }
>> +
>> + int configure(const StreamConfiguration &inputCfg,
>> + const StreamConfiguration &outputCfg);
>> + int exportBuffers(unsigned int count,
>> + std::vector<std::unique_ptr<FrameBuffer>> *buffers);
>> +
>> + int start();
>> + void stop();
>> +
>> + int queueBuffers(FrameBuffer *input, FrameBuffer *output);
>> + std::unique_ptr<V4L2M2MDevice> m2m_;
>> +
>> + protected:
>> + std::string logPrefix() const override;
>> +
>> + private:
>> + void captureBufferReady(FrameBuffer *buffer);
>> + void outputBufferReady(FrameBuffer *buffer);
>> +
>> + V4L2M2MConverter *converter_;
>> + unsigned int index_;
>> +
>> + unsigned int inputBufferCount_;
>> + unsigned int outputBufferCount_;
>> + };
>> +
>> + std::unique_ptr<V4L2M2MDevice> m2m_;
>> +
>> + std::vector<Stream> streams_;
>> + std::vector<Mapping> mappings_;
>> + std::map<FrameBuffer *, unsigned int> queue_;
>> +
>> +public:
>> + V4L2M2MConverter(MediaDevice *media);
>> +
>> + int loadConfiguration(const std::string &filename) override;
>> +
>> + bool isValid() const { return m2m_ != nullptr; }
>> +
>> + std::vector<PixelFormat> formats(PixelFormat input);
>> + SizeRange sizes(const Size &input);
>> +
>> + std::tuple<unsigned int, unsigned int>
>> + strideAndFrameSize(const PixelFormat &pixelFormat, const Size &size);
>> +
>> + int configure(const StreamConfiguration &inputCfg,
>> + const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfg);
>> + int exportBuffers(unsigned int ouput, unsigned int count,
>> + std::vector<std::unique_ptr<FrameBuffer>> *buffers);
>> +
>> + int start();
>> + void stop();
>> +
>> + int queueBuffers(FrameBuffer *input,
>> + const std::map<unsigned int, FrameBuffer *> &outputs);
>> +
>> + virtual int applyMapping([[maybe_unused]] Stream *stream, [[maybe_unused]] Mapping &mapping) { return 0; };
>> +};
>> +
>> +} /* namespace libcamera */
>> diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
>> index 8f50d755..132de5ef 100644
>> --- a/include/libcamera/internal/meson.build
>> +++ b/include/libcamera/internal/meson.build
>> @@ -20,6 +20,7 @@ libcamera_internal_headers = files([
>> 'control_serializer.h',
>> 'control_validator.h',
>> 'converter.h',
>> + 'converter_v4l2_m2m.h',
>> 'delayed_controls.h',
>> 'device_enumerator.h',
>> 'device_enumerator_sysfs.h',
>> diff --git a/src/libcamera/converter_v4l2_m2m.cpp b/src/libcamera/converter_v4l2_m2m.cpp
>> new file mode 100644
>> index 00000000..942e6e6f
>> --- /dev/null
>> +++ b/src/libcamera/converter_v4l2_m2m.cpp
>> @@ -0,0 +1,504 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2020, Laurent Pinchart
>> + * Copyright 2022 NXP
>> + *
>> + * converter_v4l2_m2m.cpp - V4L2 M2M Format converter
>> + */
>> +
>> +#include <algorithm>
>> +#include <limits.h>
>> +
>> +#include <libcamera/base/file.h>
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/signal.h>
>> +#include <libcamera/base/utils.h>
>> +
>> +#include <libcamera/framebuffer.h>
>> +#include <libcamera/geometry.h>
>> +#include <libcamera/stream.h>
>> +
>> +#include "libcamera/internal/converter_v4l2_m2m.h"
>> +#include "libcamera/internal/media_device.h"
>> +#include "libcamera/internal/v4l2_videodevice.h"
>> +#include "libcamera/internal/yaml_parser.h"
>> +
>> +namespace libcamera {
>> +
>> +LOG_DECLARE_CATEGORY(Converter)
>> +
>> +/* -----------------------------------------------------------------------------
>> + * V4L2M2MConverter::Stream
>> + */
>> +
>> +V4L2M2MConverter::Stream::Stream(V4L2M2MConverter *converter, unsigned int index)
>> + : converter_(converter), index_(index)
>> +{
>> + m2m_ = std::make_unique<V4L2M2MDevice>(converter->deviceNode_);
>> +
>> + m2m_->output()->bufferReady.connect(this, &Stream::outputBufferReady);
>> + m2m_->capture()->bufferReady.connect(this, &Stream::captureBufferReady);
>> +
>> + int ret = m2m_->open();
>> + if (ret < 0)
>> + m2m_.reset();
>
> Why are we re-creating and opening the m2m_ device everytime we create
> a stream ? Isn't it the same created and open when the
> V4L2M2MConverter is created ?Would it be for proper reference counting ?
>
> Thanks
> j
>
>> +}
>> +
>> +int V4L2M2MConverter::Stream::configure(const StreamConfiguration &inputCfg,
>> + const StreamConfiguration &outputCfg)
>> +{
>> + V4L2PixelFormat videoFormat =
>> + m2m_->output()->toV4L2PixelFormat(inputCfg.pixelFormat);
>> +
>> + V4L2DeviceFormat format;
>> + format.fourcc = videoFormat;
>> + format.size = inputCfg.size;
>> + format.planesCount = 1;
>> + format.planes[0].bpl = inputCfg.stride;
>> +
>> + int ret = m2m_->output()->setFormat(&format);
>> + if (ret < 0) {
>> + LOG(Converter, Error)
>> + << "Failed to set input format: " << strerror(-ret);
>> + return ret;
>> + }
>> +
>> + if (format.fourcc != videoFormat || format.size != inputCfg.size ||
>> + format.planes[0].bpl != inputCfg.stride) {
>> + LOG(Converter, Error)
>> + << "Input format not supported (requested "
>> + << inputCfg.size << "-" << videoFormat
>> + << ", got " << format << ")";
>> + return -EINVAL;
>> + }
>> +
>> + /* Set the pixel format and size on the output. */
>> + videoFormat = m2m_->capture()->toV4L2PixelFormat(outputCfg.pixelFormat);
>> + format = {};
>> + format.fourcc = videoFormat;
>> + format.size = outputCfg.size;
>> +
>> + ret = m2m_->capture()->setFormat(&format);
>> + if (ret < 0) {
>> + LOG(Converter, Error)
>> + << "Failed to set output format: " << strerror(-ret);
>> + return ret;
>> + }
>> +
>> + if (format.fourcc != videoFormat || format.size != outputCfg.size) {
>> + LOG(Converter, Error)
>> + << "Output format not supported";
>> + return -EINVAL;
>> + }
>> +
>> + inputBufferCount_ = inputCfg.bufferCount;
>> + outputBufferCount_ = outputCfg.bufferCount;
>> +
>> + for (Mapping &mapping : converter_->mappings_) {
>> + ControlList ctrls;
>> + if (mapping.getInputSize() == inputCfg.size && mapping.getOutputSize() == outputCfg.size) {
>> + LOG(Converter, Debug)
>> + << "Got a configuration match "
>> + << inputCfg.size << " --> " << outputCfg.size;
>> + converter_->applyMapping(this, mapping);
>> + }
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +int V4L2M2MConverter::Stream::exportBuffers(unsigned int count,
>> + std::vector<std::unique_ptr<FrameBuffer>> *buffers)
>> +{
>> + return m2m_->capture()->exportBuffers(count, buffers);
>> +}
>> +
>> +int V4L2M2MConverter::Stream::start()
>> +{
>> + int ret = m2m_->output()->importBuffers(inputBufferCount_);
>> + if (ret < 0)
>> + return ret;
>> +
>> + ret = m2m_->capture()->importBuffers(outputBufferCount_);
>> + if (ret < 0) {
>> + stop();
>> + return ret;
>> + }
>> +
>> + ret = m2m_->output()->streamOn();
>> + if (ret < 0) {
>> + stop();
>> + return ret;
>> + }
>> +
>> + ret = m2m_->capture()->streamOn();
>> + if (ret < 0) {
>> + stop();
>> + return ret;
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +void V4L2M2MConverter::Stream::stop()
>> +{
>> + m2m_->capture()->streamOff();
>> + m2m_->output()->streamOff();
>> + m2m_->capture()->releaseBuffers();
>> + m2m_->output()->releaseBuffers();
>> +}
>> +
>> +int V4L2M2MConverter::Stream::queueBuffers(FrameBuffer *input, FrameBuffer *output)
>> +{
>> + int ret = m2m_->output()->queueBuffer(input);
>> + if (ret < 0)
>> + return ret;
>> +
>> + ret = m2m_->capture()->queueBuffer(output);
>> + if (ret < 0)
>> + return ret;
>> +
>> + return 0;
>> +}
>> +
>> +std::string V4L2M2MConverter::Stream::logPrefix() const
>> +{
>> + return "stream" + std::to_string(index_);
>> +}
>> +
>> +void V4L2M2MConverter::Stream::outputBufferReady(FrameBuffer *buffer)
>> +{
>> + auto it = converter_->queue_.find(buffer);
>> + if (it == converter_->queue_.end())
>> + return;
>> +
>> + if (!--it->second) {
>> + converter_->inputBufferReady.emit(buffer);
>> + converter_->queue_.erase(it);
>> + }
>> +}
>> +
>> +void V4L2M2MConverter::Stream::captureBufferReady(FrameBuffer *buffer)
>> +{
>> + converter_->outputBufferReady.emit(buffer);
>> +}
>> +
>> +/* -----------------------------------------------------------------------------
>> + * V4L2M2MConverter
>> + */
>> +
>> +V4L2M2MConverter::V4L2M2MConverter(MediaDevice *media)
>> + : Converter(media)
>> +{
>> + if (deviceNode_.empty())
>> + return;
>> +
>> + m2m_ = std::make_unique<V4L2M2MDevice>(deviceNode_);
>> + int ret = m2m_->open();
>> + if (ret < 0) {
>> + m2m_.reset();
>> + return;
>> + }
>> +}
>> +
>> +int V4L2M2MConverter::loadConfiguration(const std::string &filename)
>> +{
>> + LOG(Converter, Debug)
>> + << "Parsing configuration file " << filename;
>> +
>> + File file(filename);
>> +
>> + if (!file.open(File::OpenModeFlag::ReadOnly)) {
>> + int ret = file.error();
>> + LOG(Converter, Error)
>> + << "Failed to open configuration file "
>> + << filename << ": " << strerror(-ret);
>> + return ret;
>> + }
>> +
>> + std::unique_ptr<libcamera::YamlObject> data = YamlParser::parse(file);
>> + if (!data)
>> + return -EINVAL;
>> +
>> + if (!data->contains("mappings")) {
>> + LOG(Converter, Error)
>> + << "Vertex mapping key missing";
>> + return -EINVAL;
>> + }
>> +
>> + const YamlObject &mappings = (*data)["mappings"];
>> + if (!mappings.isList() || mappings.size() == 0) {
>> + LOG(Converter, Error)
>> + << "Invalid mappings entry";
>> + return -EINVAL;
>> + }
>> +
>> + LOG(Converter, Debug)
>> + << "Parsing " << mappings.size() << " mappings";
>> + mappings_.clear();
>> + mappings_.reserve(mappings.size());
>> +
>> + for (std::size_t i = 0; i < mappings.size(); i++) {
>> + const YamlObject &mapping = mappings[i];
>> + if (!mapping.isDictionary()) {
>> + LOG(Converter, Error)
>> + << "Mapping is not a dictionnary";
>> + return -EINVAL;
>> + }
>> +
>> + if (!mapping.contains("input-resolution")) {
>> + LOG(Converter, Error)
>> + << "Input resolution missing";
>> + return -EINVAL;
>> + }
>> +
>> + if (!mapping.contains("output-resolution")) {
>> + LOG(Converter, Error)
>> + << "Output resolution missing";
>> + return -EINVAL;
>> + }
>> +
>> + if (!mapping.contains("mapping")) {
>> + LOG(Converter, Error)
>> + << "Mapping table missing";
>> + return -EINVAL;
>> + }
>> +
>> + const YamlObject &input_res = mapping["input-resolution"];
>> + if (!input_res.isList() || input_res.size() != 2) {
>> + LOG(Converter, Error)
>> + << "Incorrect input resolution";
>> + return -EINVAL;
>> + }
>> +
>> + const YamlObject &output_res = mapping["output-resolution"];
>> + if (!output_res.isList() || output_res.size() != 2) {
>> + LOG(Converter, Error)
>> + << "Incorrect output resolution";
>> + return -EINVAL;
>> + }
>> +
>> + const YamlObject &map = mapping["mapping"];
>> + if (!map.isList() || map.size() == 0) {
>> + LOG(Converter, Error)
>> + << "Incorrect mapping entries";
>> + return -EINVAL;
>> + }
>> +
>> + Size input(input_res[0].get<uint32_t>(0), input_res[1].get<uint32_t>(0));
>> + Size output(output_res[0].get<uint32_t>(0), output_res[1].get<uint32_t>(0));
>> + const auto &mapVector = map.getList<uint32_t>().value_or(utils::defopt);
>> +
>> + LOG(Converter, Debug)
>> + << "Input/Output mapping resolution " << input << " ---> " << output;
>> + mappings_.emplace_back(Mapping(input, output, mapVector));
>> + }
>> +
>> + return mappings.size();
>> +}
>> +
>> +std::vector<PixelFormat> V4L2M2MConverter::formats(PixelFormat input)
>> +{
>> + if (!m2m_)
>> + return {};
>> +
>> + /*
>> + * Set the format on the input side (V4L2 output) of the converter to
>> + * enumerate the conversion capabilities on its output (V4L2 capture).
>> + */
>> + V4L2DeviceFormat v4l2Format;
>> + v4l2Format.fourcc = m2m_->output()->toV4L2PixelFormat(input);
>> + v4l2Format.size = { 1, 1 };
>> +
>> + int ret = m2m_->output()->setFormat(&v4l2Format);
>> + if (ret < 0) {
>> + LOG(Converter, Error)
>> + << "Failed to set format: " << strerror(-ret);
>> + return {};
>> + }
>> +
>> + if (v4l2Format.fourcc != m2m_->output()->toV4L2PixelFormat(input)) {
>> + LOG(Converter, Debug)
>> + << "Input format " << input << " not supported.";
>> + return {};
>> + }
>> +
>> + std::vector<PixelFormat> pixelFormats;
>> +
>> + for (const auto &format : m2m_->capture()->formats()) {
>> + PixelFormat pixelFormat = format.first.toPixelFormat();
>> + if (pixelFormat)
>> + pixelFormats.push_back(pixelFormat);
>> + }
>> +
>> + return pixelFormats;
>> +}
>> +
>> +SizeRange V4L2M2MConverter::sizes(const Size &input)
>> +{
>> + if (!m2m_)
>> + return {};
>> +
>> + /*
>> + * Set the size on the input side (V4L2 output) of the converter to
>> + * enumerate the scaling capabilities on its output (V4L2 capture).
>> + */
>> + V4L2DeviceFormat format;
>> + format.fourcc = V4L2PixelFormat();
>> + format.size = input;
>> +
>> + int ret = m2m_->output()->setFormat(&format);
>> + if (ret < 0) {
>> + LOG(Converter, Error)
>> + << "Failed to set format: " << strerror(-ret);
>> + return {};
>> + }
>> +
>> + SizeRange sizes;
>> +
>> + format.size = { 1, 1 };
>> + ret = m2m_->capture()->setFormat(&format);
>> + if (ret < 0) {
>> + LOG(Converter, Error)
>> + << "Failed to set format: " << strerror(-ret);
>> + return {};
>> + }
>> +
>> + sizes.min = format.size;
>> +
>> + format.size = { UINT_MAX, UINT_MAX };
>> + ret = m2m_->capture()->setFormat(&format);
>> + if (ret < 0) {
>> + LOG(Converter, Error)
>> + << "Failed to set format: " << strerror(-ret);
>> + return {};
>> + }
>> +
>> + sizes.max = format.size;
>> +
>> + return sizes;
>> +}
>> +
>> +std::tuple<unsigned int, unsigned int>
>> +V4L2M2MConverter::strideAndFrameSize(const PixelFormat &pixelFormat,
>> + const Size &size)
>> +{
>> + V4L2DeviceFormat format;
>> + format.fourcc = m2m_->capture()->toV4L2PixelFormat(pixelFormat);
>> + format.size = size;
>> +
>> + int ret = m2m_->capture()->tryFormat(&format);
>> + if (ret < 0)
>> + return std::make_tuple(0, 0);
>> +
>> + return std::make_tuple(format.planes[0].bpl, format.planes[0].size);
>> +}
>> +
>> +int V4L2M2MConverter::configure(const StreamConfiguration &inputCfg,
>> + const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs)
>> +{
>> + int ret = 0;
>> +
>> + streams_.clear();
>> + streams_.reserve(outputCfgs.size());
>> +
>> + for (unsigned int i = 0; i < outputCfgs.size(); ++i) {
>> + Stream &stream = streams_.emplace_back(this, i);
>> +
>> + if (!stream.isValid()) {
>> + LOG(Converter, Error)
>> + << "Failed to create stream " << i;
>> + ret = -EINVAL;
>> + break;
>> + }
>> +
>> + ret = stream.configure(inputCfg, outputCfgs[i]);
>> + if (ret < 0)
>> + break;
>> + }
>> +
>> + if (ret < 0) {
>> + streams_.clear();
>> + return ret;
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +int V4L2M2MConverter::exportBuffers(unsigned int output, unsigned int count,
>> + std::vector<std::unique_ptr<FrameBuffer>> *buffers)
>> +{
>> + if (output >= streams_.size())
>> + return -EINVAL;
>> +
>> + return streams_[output].exportBuffers(count, buffers);
>> +}
>> +
>> +int V4L2M2MConverter::start()
>> +{
>> + int ret;
>> +
>> + for (Stream &stream : streams_) {
>> + ret = stream.start();
>> + if (ret < 0) {
>> + stop();
>> + return ret;
>> + }
>> + }
>> +
>> + return 0;
>> +}
>> +
>> +void V4L2M2MConverter::stop()
>> +{
>> + for (Stream &stream : utils::reverse(streams_))
>> + stream.stop();
>> +}
>> +
>> +int V4L2M2MConverter::queueBuffers(FrameBuffer *input,
>> + const std::map<unsigned int, FrameBuffer *> &outputs)
>> +{
>> + unsigned int mask = 0;
>> + int ret;
>> +
>> + /*
>> + * Validate the outputs as a sanity check: at least one output is
>> + * required, all outputs must reference a valid stream and no two
>> + * outputs can reference the same stream.
>> + */
>> + if (outputs.empty())
>> + return -EINVAL;
>> +
>> + for (auto [index, buffer] : outputs) {
>> + if (!buffer)
>> + return -EINVAL;
>> + if (index >= streams_.size())
>> + return -EINVAL;
>> + if (mask & (1 << index))
>> + return -EINVAL;
>> +
>> + mask |= 1 << index;
>> + }
>> +
>> + /* Queue the input and output buffers to all the streams. */
>> + for (auto [index, buffer] : outputs) {
>> + ret = streams_[index].queueBuffers(input, buffer);
>> + if (ret < 0)
>> + return ret;
>> + }
>> +
>> + /*
>> + * Add the input buffer to the queue, with the number of streams as a
>> + * reference count. Completion of the input buffer will be signalled by
>> + * the stream that releases the last reference.
>> + */
>> + queue_.emplace(std::piecewise_construct,
>> + std::forward_as_tuple(input),
>> + std::forward_as_tuple(outputs.size()));
>> +
>> + return 0;
>> +}
>> +
>> +REGISTER_CONVERTER("v4l2_m2m", V4L2M2MConverter, "pxp")
>> +
>> +} /* namespace libcamera */
>> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
>> index a261d4b4..b12c8401 100644
>> --- a/src/libcamera/meson.build
>> +++ b/src/libcamera/meson.build
>> @@ -14,6 +14,7 @@ libcamera_sources = files([
>> 'control_serializer.cpp',
>> 'control_validator.cpp',
>> 'converter.cpp',
>> + 'converter_v4l2_m2m.cpp',
>> 'delayed_controls.cpp',
>> 'device_enumerator.cpp',
>> 'device_enumerator_sysfs.cpp',
>> --
>> 2.37.3
>>
More information about the libcamera-devel
mailing list