[libcamera-devel,v3,10/11] libcamera: pipeline: simple: Add simple format converter

Message ID 20200320014839.14683-11-laurent.pinchart@ideasonboard.com
State Superseded
Headers show
Series
  • Simple pipeline handler
Related show

Commit Message

Laurent Pinchart March 20, 2020, 1:48 a.m. UTC
The simple format converter supports V4L2 M2M devices that convert pixel
formats.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
Changes since v2:

- Rebase on top of V4L2PixelFormat
---
 src/libcamera/pipeline/simple/converter.cpp | 209 ++++++++++++++++++++
 src/libcamera/pipeline/simple/converter.h   |  60 ++++++
 src/libcamera/pipeline/simple/meson.build   |   1 +
 3 files changed, 270 insertions(+)
 create mode 100644 src/libcamera/pipeline/simple/converter.cpp
 create mode 100644 src/libcamera/pipeline/simple/converter.h

Comments

Kieran Bingham March 31, 2020, 12:39 p.m. UTC | #1
Hi Laurent,

On 20/03/2020 01:48, Laurent Pinchart wrote:
> The simple format converter supports V4L2 M2M devices that convert pixel
> formats.
> 
> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> ---
> Changes since v2:
> 
> - Rebase on top of V4L2PixelFormat

Not much I can spot in this one, and I'm pleased to see the V4L2 M2M
device abstraction being useful/used more ;-)

Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>

> ---
>  src/libcamera/pipeline/simple/converter.cpp | 209 ++++++++++++++++++++
>  src/libcamera/pipeline/simple/converter.h   |  60 ++++++
>  src/libcamera/pipeline/simple/meson.build   |   1 +
>  3 files changed, 270 insertions(+)
>  create mode 100644 src/libcamera/pipeline/simple/converter.cpp
>  create mode 100644 src/libcamera/pipeline/simple/converter.h
> 
> diff --git a/src/libcamera/pipeline/simple/converter.cpp b/src/libcamera/pipeline/simple/converter.cpp
> new file mode 100644
> index 000000000000..3025c3dea809
> --- /dev/null
> +++ b/src/libcamera/pipeline/simple/converter.cpp
> @@ -0,0 +1,209 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2020, Laurent Pinchart
> + *
> + * converter.cpp - Format converter for simple pipeline handler
> + */
> +
> +#include "converter.h"
> +
> +#include <algorithm>
> +
> +#include <libcamera/buffer.h>
> +#include <libcamera/geometry.h>
> +#include <libcamera/signal.h>
> +
> +#include "log.h"
> +#include "media_device.h"
> +#include "v4l2_videodevice.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(SimplePipeline);
> +
> +SimpleConverter::SimpleConverter(MediaDevice *media)
> +	: m2m_(nullptr)
> +{
> +	/*
> +	 * Locate the video node. There's no need to validate the pipeline
> +	 * further, the caller guarantees that this is a V4L2 mem2mem device.
> +	 */
> +	const std::vector<MediaEntity *> &entities = media->entities();
> +	auto it = std::find_if(entities.begin(), entities.end(),
> +			       [](MediaEntity *entity) {
> +				       return entity->function() == MEDIA_ENT_F_IO_V4L;
> +			       });
> +	if (it == entities.end())
> +		return;
> +
> +	m2m_ = new V4L2M2MDevice((*it)->deviceNode());
> +
> +	m2m_->output()->bufferReady.connect(this, &SimpleConverter::outputBufferReady);
> +	m2m_->capture()->bufferReady.connect(this, &SimpleConverter::captureBufferReady);
> +}
> +
> +SimpleConverter::~SimpleConverter()
> +{
> +	delete m2m_;
> +}
> +
> +int SimpleConverter::open()
> +{
> +	if (!m2m_)
> +		return -ENODEV;
> +
> +	return m2m_->open();
> +}
> +
> +void SimpleConverter::close()
> +{
> +	if (m2m_)
> +		m2m_->close();
> +}
> +
> +std::vector<PixelFormat> SimpleConverter::formats(PixelFormat input)
> +{
> +	if (!m2m_)
> +		return {};
> +

/* Set the given PixelFormat on the output queue, to determine the
conversion capabilities at the capture queue. */

(where /queue/{node,device,something else}/)

> +	V4L2DeviceFormat format;
> +	format.fourcc = m2m_->output()->toV4L2PixelFormat(input);
> +	format.size = { 1, 1 };
> +
> +	int ret = m2m_->output()->setFormat(&format);
> +	if (ret < 0) {
> +		LOG(SimplePipeline, Error)
> +			<< "Failed to set format: " << strerror(-ret);
> +		return {};
> +	}
> +
> +	std::vector<PixelFormat> pixelFormats;
> +
> +	for (const auto &format : m2m_->capture()->formats()) {
> +		PixelFormat pixelFormat = m2m_->capture()->toPixelFormat(format.first);
> +		if (pixelFormat)
> +			pixelFormats.push_back(pixelFormat);
> +	}
> +
> +	return pixelFormats;
> +}
> +
> +int SimpleConverter::configure(PixelFormat inputFormat,
> +			       PixelFormat outputFormat, const Size &size)
> +{
> +	V4L2DeviceFormat format;
> +	int ret;
> +
> +	V4L2PixelFormat videoFormat = m2m_->output()->toV4L2PixelFormat(inputFormat);
> +	format.fourcc = videoFormat;
> +	format.size = size;
> +
> +	ret = m2m_->output()->setFormat(&format);
> +	if (ret < 0) {
> +		LOG(SimplePipeline, Error)
> +			<< "Failed to set input format: " << strerror(-ret);
> +		return ret;
> +	}
> +
> +	if (format.fourcc != videoFormat || format.size != size) {
> +		LOG(SimplePipeline, Error)
> +			<< "Input format not supported";
> +		return -EINVAL;
> +	}
> +
> +	videoFormat = m2m_->capture()->toV4L2PixelFormat(outputFormat);
> +	format.fourcc = videoFormat;
> +
> +	ret = m2m_->capture()->setFormat(&format);
> +	if (ret < 0) {
> +		LOG(SimplePipeline, Error)
> +			<< "Failed to set output format: " << strerror(-ret);
> +		return ret;
> +	}
> +
> +	if (format.fourcc != videoFormat || format.size != size) {
> +		LOG(SimplePipeline, Error)
> +			<< "Output format not supported";
> +		return -EINVAL;
> +	}
> +
> +	return 0;
> +}
> +
> +int SimpleConverter::exportBuffers(unsigned int count,
> +				   std::vector<std::unique_ptr<FrameBuffer>> *buffers)
> +{
> +	return m2m_->capture()->exportBuffers(count, buffers);
> +}
> +
> +int SimpleConverter::start(unsigned int count)
> +{
> +	int ret = m2m_->output()->importBuffers(count);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = m2m_->capture()->importBuffers(count);
> +	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 SimpleConverter::stop()
> +{
> +	m2m_->capture()->streamOff();
> +	m2m_->output()->streamOff();
> +	m2m_->capture()->releaseBuffers();
> +	m2m_->output()->releaseBuffers();
> +}
> +
> +int SimpleConverter::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;
> +}
> +
> +void SimpleConverter::captureBufferReady(FrameBuffer *buffer)
> +{

Do we need any kind of locking between these two BufferReady slots which
essentially interact with each other? or is the queue 'safe' enough
 (or perhaps are we guaranteed to process both in the same thread?)

> +	if (!outputDoneQueue_.empty()) {
> +		FrameBuffer *other = outputDoneQueue_.front();
> +		outputDoneQueue_.pop();
> +		bufferReady.emit(other, buffer);
> +	} else {
> +		captureDoneQueue_.push(buffer);
> +	}
> +}
> +
> +void SimpleConverter::outputBufferReady(FrameBuffer *buffer)
> +{
> +	if (!captureDoneQueue_.empty()) {
> +		FrameBuffer *other = captureDoneQueue_.front();
> +		captureDoneQueue_.pop();
> +		bufferReady.emit(buffer, other);
> +	} else {
> +		outputDoneQueue_.push(buffer);
> +	}
> +}
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/pipeline/simple/converter.h b/src/libcamera/pipeline/simple/converter.h
> new file mode 100644
> index 000000000000..a33071fa8578
> --- /dev/null
> +++ b/src/libcamera/pipeline/simple/converter.h
> @@ -0,0 +1,60 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2020, Laurent Pinchart
> + *
> + * converter.h - Format converter for simple pipeline handler
> + */
> +
> +#ifndef __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__
> +#define __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__
> +
> +#include <memory>
> +#include <queue>
> +#include <vector>
> +
> +#include <libcamera/pixelformats.h>
> +#include <libcamera/signal.h>
> +
> +namespace libcamera {
> +
> +class FrameBuffer;
> +class MediaDevice;
> +struct Size;
> +class V4L2M2MDevice;
> +
> +class SimpleConverter
> +{
> +public:
> +	SimpleConverter(MediaDevice *media);
> +	~SimpleConverter();
> +
> +	int open();
> +	void close();
> +
> +	std::vector<PixelFormat> formats(PixelFormat input);
> +
> +	int configure(PixelFormat inputFormat, PixelFormat outputFormat,
> +		      const Size &size);
> +	int exportBuffers(unsigned int count,
> +			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
> +
> +	int start(unsigned int count);
> +	void stop();
> +
> +	int queueBuffers(FrameBuffer *input, FrameBuffer *output);
> +
> +	Signal<FrameBuffer *, FrameBuffer *> bufferReady;
> +
> +private:
> +	void captureBufferReady(FrameBuffer *buffer);
> +	void outputBufferReady(FrameBuffer *buffer);
> +
> +	V4L2M2MDevice *m2m_;
> +
> +	std::queue<FrameBuffer *> captureDoneQueue_;
> +	std::queue<FrameBuffer *> outputDoneQueue_;
> +};
> +
> +} /* namespace libcamera */
> +
> +#endif /* __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__ */
> diff --git a/src/libcamera/pipeline/simple/meson.build b/src/libcamera/pipeline/simple/meson.build
> index 4945a3e173cf..8372f24e3788 100644
> --- a/src/libcamera/pipeline/simple/meson.build
> +++ b/src/libcamera/pipeline/simple/meson.build
> @@ -1,3 +1,4 @@
>  libcamera_sources += files([
> +    'converter.cpp',
>      'simple.cpp',
>  ])
>
Laurent Pinchart April 4, 2020, 12:24 a.m. UTC | #2
Hi Kieran,

On Tue, Mar 31, 2020 at 01:39:37PM +0100, Kieran Bingham wrote:
> On 20/03/2020 01:48, Laurent Pinchart wrote:
> > The simple format converter supports V4L2 M2M devices that convert pixel
> > formats.
> > 
> > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> > ---
> > Changes since v2:
> > 
> > - Rebase on top of V4L2PixelFormat
> 
> Not much I can spot in this one, and I'm pleased to see the V4L2 M2M
> device abstraction being useful/used more ;-)
> 
> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> 
> > ---
> >  src/libcamera/pipeline/simple/converter.cpp | 209 ++++++++++++++++++++
> >  src/libcamera/pipeline/simple/converter.h   |  60 ++++++
> >  src/libcamera/pipeline/simple/meson.build   |   1 +
> >  3 files changed, 270 insertions(+)
> >  create mode 100644 src/libcamera/pipeline/simple/converter.cpp
> >  create mode 100644 src/libcamera/pipeline/simple/converter.h
> > 
> > diff --git a/src/libcamera/pipeline/simple/converter.cpp b/src/libcamera/pipeline/simple/converter.cpp
> > new file mode 100644
> > index 000000000000..3025c3dea809
> > --- /dev/null
> > +++ b/src/libcamera/pipeline/simple/converter.cpp
> > @@ -0,0 +1,209 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2020, Laurent Pinchart
> > + *
> > + * converter.cpp - Format converter for simple pipeline handler
> > + */
> > +
> > +#include "converter.h"
> > +
> > +#include <algorithm>
> > +
> > +#include <libcamera/buffer.h>
> > +#include <libcamera/geometry.h>
> > +#include <libcamera/signal.h>
> > +
> > +#include "log.h"
> > +#include "media_device.h"
> > +#include "v4l2_videodevice.h"
> > +
> > +namespace libcamera {
> > +
> > +LOG_DECLARE_CATEGORY(SimplePipeline);
> > +
> > +SimpleConverter::SimpleConverter(MediaDevice *media)
> > +	: m2m_(nullptr)
> > +{
> > +	/*
> > +	 * Locate the video node. There's no need to validate the pipeline
> > +	 * further, the caller guarantees that this is a V4L2 mem2mem device.
> > +	 */
> > +	const std::vector<MediaEntity *> &entities = media->entities();
> > +	auto it = std::find_if(entities.begin(), entities.end(),
> > +			       [](MediaEntity *entity) {
> > +				       return entity->function() == MEDIA_ENT_F_IO_V4L;
> > +			       });
> > +	if (it == entities.end())
> > +		return;
> > +
> > +	m2m_ = new V4L2M2MDevice((*it)->deviceNode());
> > +
> > +	m2m_->output()->bufferReady.connect(this, &SimpleConverter::outputBufferReady);
> > +	m2m_->capture()->bufferReady.connect(this, &SimpleConverter::captureBufferReady);
> > +}
> > +
> > +SimpleConverter::~SimpleConverter()
> > +{
> > +	delete m2m_;
> > +}
> > +
> > +int SimpleConverter::open()
> > +{
> > +	if (!m2m_)
> > +		return -ENODEV;
> > +
> > +	return m2m_->open();
> > +}
> > +
> > +void SimpleConverter::close()
> > +{
> > +	if (m2m_)
> > +		m2m_->close();
> > +}
> > +
> > +std::vector<PixelFormat> SimpleConverter::formats(PixelFormat input)
> > +{
> > +	if (!m2m_)
> > +		return {};
> > +
> 
> /* Set the given PixelFormat on the output queue, to determine the
> conversion capabilities at the capture queue. */
> 
> (where /queue/{node,device,something else}/)

        /*
         * Set the format on the input side (V4L2 output) of the converter to
         * enumerate the conversion capabilities on its output (V4L2 capture).
         */

> > +	V4L2DeviceFormat format;
> > +	format.fourcc = m2m_->output()->toV4L2PixelFormat(input);
> > +	format.size = { 1, 1 };
> > +
> > +	int ret = m2m_->output()->setFormat(&format);
> > +	if (ret < 0) {
> > +		LOG(SimplePipeline, Error)
> > +			<< "Failed to set format: " << strerror(-ret);
> > +		return {};
> > +	}
> > +
> > +	std::vector<PixelFormat> pixelFormats;
> > +
> > +	for (const auto &format : m2m_->capture()->formats()) {
> > +		PixelFormat pixelFormat = m2m_->capture()->toPixelFormat(format.first);
> > +		if (pixelFormat)
> > +			pixelFormats.push_back(pixelFormat);
> > +	}
> > +
> > +	return pixelFormats;
> > +}
> > +
> > +int SimpleConverter::configure(PixelFormat inputFormat,
> > +			       PixelFormat outputFormat, const Size &size)
> > +{
> > +	V4L2DeviceFormat format;
> > +	int ret;
> > +
> > +	V4L2PixelFormat videoFormat = m2m_->output()->toV4L2PixelFormat(inputFormat);
> > +	format.fourcc = videoFormat;
> > +	format.size = size;
> > +
> > +	ret = m2m_->output()->setFormat(&format);
> > +	if (ret < 0) {
> > +		LOG(SimplePipeline, Error)
> > +			<< "Failed to set input format: " << strerror(-ret);
> > +		return ret;
> > +	}
> > +
> > +	if (format.fourcc != videoFormat || format.size != size) {
> > +		LOG(SimplePipeline, Error)
> > +			<< "Input format not supported";
> > +		return -EINVAL;
> > +	}
> > +
> > +	videoFormat = m2m_->capture()->toV4L2PixelFormat(outputFormat);
> > +	format.fourcc = videoFormat;
> > +
> > +	ret = m2m_->capture()->setFormat(&format);
> > +	if (ret < 0) {
> > +		LOG(SimplePipeline, Error)
> > +			<< "Failed to set output format: " << strerror(-ret);
> > +		return ret;
> > +	}
> > +
> > +	if (format.fourcc != videoFormat || format.size != size) {
> > +		LOG(SimplePipeline, Error)
> > +			<< "Output format not supported";
> > +		return -EINVAL;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +int SimpleConverter::exportBuffers(unsigned int count,
> > +				   std::vector<std::unique_ptr<FrameBuffer>> *buffers)
> > +{
> > +	return m2m_->capture()->exportBuffers(count, buffers);
> > +}
> > +
> > +int SimpleConverter::start(unsigned int count)
> > +{
> > +	int ret = m2m_->output()->importBuffers(count);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ret = m2m_->capture()->importBuffers(count);
> > +	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 SimpleConverter::stop()
> > +{
> > +	m2m_->capture()->streamOff();
> > +	m2m_->output()->streamOff();
> > +	m2m_->capture()->releaseBuffers();
> > +	m2m_->output()->releaseBuffers();
> > +}
> > +
> > +int SimpleConverter::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;
> > +}
> > +
> > +void SimpleConverter::captureBufferReady(FrameBuffer *buffer)
> > +{
> 
> Do we need any kind of locking between these two BufferReady slots which
> essentially interact with each other? or is the queue 'safe' enough
>  (or perhaps are we guaranteed to process both in the same thread?)

It's all single-threaded code, so no locking is needed.

> > +	if (!outputDoneQueue_.empty()) {
> > +		FrameBuffer *other = outputDoneQueue_.front();
> > +		outputDoneQueue_.pop();
> > +		bufferReady.emit(other, buffer);
> > +	} else {
> > +		captureDoneQueue_.push(buffer);
> > +	}
> > +}
> > +
> > +void SimpleConverter::outputBufferReady(FrameBuffer *buffer)
> > +{
> > +	if (!captureDoneQueue_.empty()) {
> > +		FrameBuffer *other = captureDoneQueue_.front();
> > +		captureDoneQueue_.pop();
> > +		bufferReady.emit(buffer, other);
> > +	} else {
> > +		outputDoneQueue_.push(buffer);
> > +	}
> > +}
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/libcamera/pipeline/simple/converter.h b/src/libcamera/pipeline/simple/converter.h
> > new file mode 100644
> > index 000000000000..a33071fa8578
> > --- /dev/null
> > +++ b/src/libcamera/pipeline/simple/converter.h
> > @@ -0,0 +1,60 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2020, Laurent Pinchart
> > + *
> > + * converter.h - Format converter for simple pipeline handler
> > + */
> > +
> > +#ifndef __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__
> > +#define __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__
> > +
> > +#include <memory>
> > +#include <queue>
> > +#include <vector>
> > +
> > +#include <libcamera/pixelformats.h>
> > +#include <libcamera/signal.h>
> > +
> > +namespace libcamera {
> > +
> > +class FrameBuffer;
> > +class MediaDevice;
> > +struct Size;
> > +class V4L2M2MDevice;
> > +
> > +class SimpleConverter
> > +{
> > +public:
> > +	SimpleConverter(MediaDevice *media);
> > +	~SimpleConverter();
> > +
> > +	int open();
> > +	void close();
> > +
> > +	std::vector<PixelFormat> formats(PixelFormat input);
> > +
> > +	int configure(PixelFormat inputFormat, PixelFormat outputFormat,
> > +		      const Size &size);
> > +	int exportBuffers(unsigned int count,
> > +			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
> > +
> > +	int start(unsigned int count);
> > +	void stop();
> > +
> > +	int queueBuffers(FrameBuffer *input, FrameBuffer *output);
> > +
> > +	Signal<FrameBuffer *, FrameBuffer *> bufferReady;
> > +
> > +private:
> > +	void captureBufferReady(FrameBuffer *buffer);
> > +	void outputBufferReady(FrameBuffer *buffer);
> > +
> > +	V4L2M2MDevice *m2m_;
> > +
> > +	std::queue<FrameBuffer *> captureDoneQueue_;
> > +	std::queue<FrameBuffer *> outputDoneQueue_;
> > +};
> > +
> > +} /* namespace libcamera */
> > +
> > +#endif /* __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__ */
> > diff --git a/src/libcamera/pipeline/simple/meson.build b/src/libcamera/pipeline/simple/meson.build
> > index 4945a3e173cf..8372f24e3788 100644
> > --- a/src/libcamera/pipeline/simple/meson.build
> > +++ b/src/libcamera/pipeline/simple/meson.build
> > @@ -1,3 +1,4 @@
> >  libcamera_sources += files([
> > +    'converter.cpp',
> >      'simple.cpp',
> >  ])
> >

Patch

diff --git a/src/libcamera/pipeline/simple/converter.cpp b/src/libcamera/pipeline/simple/converter.cpp
new file mode 100644
index 000000000000..3025c3dea809
--- /dev/null
+++ b/src/libcamera/pipeline/simple/converter.cpp
@@ -0,0 +1,209 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Laurent Pinchart
+ *
+ * converter.cpp - Format converter for simple pipeline handler
+ */
+
+#include "converter.h"
+
+#include <algorithm>
+
+#include <libcamera/buffer.h>
+#include <libcamera/geometry.h>
+#include <libcamera/signal.h>
+
+#include "log.h"
+#include "media_device.h"
+#include "v4l2_videodevice.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(SimplePipeline);
+
+SimpleConverter::SimpleConverter(MediaDevice *media)
+	: m2m_(nullptr)
+{
+	/*
+	 * Locate the video node. There's no need to validate the pipeline
+	 * further, the caller guarantees that this is a V4L2 mem2mem device.
+	 */
+	const std::vector<MediaEntity *> &entities = media->entities();
+	auto it = std::find_if(entities.begin(), entities.end(),
+			       [](MediaEntity *entity) {
+				       return entity->function() == MEDIA_ENT_F_IO_V4L;
+			       });
+	if (it == entities.end())
+		return;
+
+	m2m_ = new V4L2M2MDevice((*it)->deviceNode());
+
+	m2m_->output()->bufferReady.connect(this, &SimpleConverter::outputBufferReady);
+	m2m_->capture()->bufferReady.connect(this, &SimpleConverter::captureBufferReady);
+}
+
+SimpleConverter::~SimpleConverter()
+{
+	delete m2m_;
+}
+
+int SimpleConverter::open()
+{
+	if (!m2m_)
+		return -ENODEV;
+
+	return m2m_->open();
+}
+
+void SimpleConverter::close()
+{
+	if (m2m_)
+		m2m_->close();
+}
+
+std::vector<PixelFormat> SimpleConverter::formats(PixelFormat input)
+{
+	if (!m2m_)
+		return {};
+
+	V4L2DeviceFormat format;
+	format.fourcc = m2m_->output()->toV4L2PixelFormat(input);
+	format.size = { 1, 1 };
+
+	int ret = m2m_->output()->setFormat(&format);
+	if (ret < 0) {
+		LOG(SimplePipeline, Error)
+			<< "Failed to set format: " << strerror(-ret);
+		return {};
+	}
+
+	std::vector<PixelFormat> pixelFormats;
+
+	for (const auto &format : m2m_->capture()->formats()) {
+		PixelFormat pixelFormat = m2m_->capture()->toPixelFormat(format.first);
+		if (pixelFormat)
+			pixelFormats.push_back(pixelFormat);
+	}
+
+	return pixelFormats;
+}
+
+int SimpleConverter::configure(PixelFormat inputFormat,
+			       PixelFormat outputFormat, const Size &size)
+{
+	V4L2DeviceFormat format;
+	int ret;
+
+	V4L2PixelFormat videoFormat = m2m_->output()->toV4L2PixelFormat(inputFormat);
+	format.fourcc = videoFormat;
+	format.size = size;
+
+	ret = m2m_->output()->setFormat(&format);
+	if (ret < 0) {
+		LOG(SimplePipeline, Error)
+			<< "Failed to set input format: " << strerror(-ret);
+		return ret;
+	}
+
+	if (format.fourcc != videoFormat || format.size != size) {
+		LOG(SimplePipeline, Error)
+			<< "Input format not supported";
+		return -EINVAL;
+	}
+
+	videoFormat = m2m_->capture()->toV4L2PixelFormat(outputFormat);
+	format.fourcc = videoFormat;
+
+	ret = m2m_->capture()->setFormat(&format);
+	if (ret < 0) {
+		LOG(SimplePipeline, Error)
+			<< "Failed to set output format: " << strerror(-ret);
+		return ret;
+	}
+
+	if (format.fourcc != videoFormat || format.size != size) {
+		LOG(SimplePipeline, Error)
+			<< "Output format not supported";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int SimpleConverter::exportBuffers(unsigned int count,
+				   std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	return m2m_->capture()->exportBuffers(count, buffers);
+}
+
+int SimpleConverter::start(unsigned int count)
+{
+	int ret = m2m_->output()->importBuffers(count);
+	if (ret < 0)
+		return ret;
+
+	ret = m2m_->capture()->importBuffers(count);
+	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 SimpleConverter::stop()
+{
+	m2m_->capture()->streamOff();
+	m2m_->output()->streamOff();
+	m2m_->capture()->releaseBuffers();
+	m2m_->output()->releaseBuffers();
+}
+
+int SimpleConverter::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;
+}
+
+void SimpleConverter::captureBufferReady(FrameBuffer *buffer)
+{
+	if (!outputDoneQueue_.empty()) {
+		FrameBuffer *other = outputDoneQueue_.front();
+		outputDoneQueue_.pop();
+		bufferReady.emit(other, buffer);
+	} else {
+		captureDoneQueue_.push(buffer);
+	}
+}
+
+void SimpleConverter::outputBufferReady(FrameBuffer *buffer)
+{
+	if (!captureDoneQueue_.empty()) {
+		FrameBuffer *other = captureDoneQueue_.front();
+		captureDoneQueue_.pop();
+		bufferReady.emit(buffer, other);
+	} else {
+		outputDoneQueue_.push(buffer);
+	}
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/simple/converter.h b/src/libcamera/pipeline/simple/converter.h
new file mode 100644
index 000000000000..a33071fa8578
--- /dev/null
+++ b/src/libcamera/pipeline/simple/converter.h
@@ -0,0 +1,60 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Laurent Pinchart
+ *
+ * converter.h - Format converter for simple pipeline handler
+ */
+
+#ifndef __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__
+#define __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__
+
+#include <memory>
+#include <queue>
+#include <vector>
+
+#include <libcamera/pixelformats.h>
+#include <libcamera/signal.h>
+
+namespace libcamera {
+
+class FrameBuffer;
+class MediaDevice;
+struct Size;
+class V4L2M2MDevice;
+
+class SimpleConverter
+{
+public:
+	SimpleConverter(MediaDevice *media);
+	~SimpleConverter();
+
+	int open();
+	void close();
+
+	std::vector<PixelFormat> formats(PixelFormat input);
+
+	int configure(PixelFormat inputFormat, PixelFormat outputFormat,
+		      const Size &size);
+	int exportBuffers(unsigned int count,
+			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
+
+	int start(unsigned int count);
+	void stop();
+
+	int queueBuffers(FrameBuffer *input, FrameBuffer *output);
+
+	Signal<FrameBuffer *, FrameBuffer *> bufferReady;
+
+private:
+	void captureBufferReady(FrameBuffer *buffer);
+	void outputBufferReady(FrameBuffer *buffer);
+
+	V4L2M2MDevice *m2m_;
+
+	std::queue<FrameBuffer *> captureDoneQueue_;
+	std::queue<FrameBuffer *> outputDoneQueue_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_PIPELINE_SIMPLE_CONVERTER_H__ */
diff --git a/src/libcamera/pipeline/simple/meson.build b/src/libcamera/pipeline/simple/meson.build
index 4945a3e173cf..8372f24e3788 100644
--- a/src/libcamera/pipeline/simple/meson.build
+++ b/src/libcamera/pipeline/simple/meson.build
@@ -1,3 +1,4 @@ 
 libcamera_sources += files([
+    'converter.cpp',
     'simple.cpp',
 ])