[1/7] libcamera: mali-c55: Add RZG2LCRU class
diff mbox series

Message ID 20251205-mali-cru-v1-1-d81bb5ffe73a@ideasonboard.com
State Superseded
Headers show
Series
  • libcamera: mali-c55: Add support for memory-to-memory
Related show

Commit Message

Jacopo Mondi Dec. 5, 2025, 2:52 p.m. UTC
From: Daniel Scally <dan.scally@ideasonboard.com>

Add a class allowing us to control the RZ/G2L Camera Receiver Unit.

The class is named after the media device it handles, which is called
"rzg2l_cru" but the CRU is actually found in other SoCs than the RZ/G2L
one, such as the RZ/V2H(P).

The RZG2LCRU class models the CSI-2 receiver found on those SoCs and
allows to add support for memory-to-memory operations on the RZ/V2H(P)
SoC which integrates a Mali-C55 ISP and a specialized DMA engine named
IVC that feeds images from memory, where the CRU has saved them.

Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/libcamera/pipeline/mali-c55/meson.build   |   3 +-
 src/libcamera/pipeline/mali-c55/rzg2l-cru.cpp | 261 ++++++++++++++++++++++++++
 src/libcamera/pipeline/mali-c55/rzg2l-cru.h   |  72 +++++++
 3 files changed, 335 insertions(+), 1 deletion(-)

Comments

Barnabás Pőcze Dec. 8, 2025, 9:48 a.m. UTC | #1
Hi

2025. 12. 05. 15:52 keltezéssel, Jacopo Mondi írta:
> From: Daniel Scally <dan.scally@ideasonboard.com>
> 
> Add a class allowing us to control the RZ/G2L Camera Receiver Unit.
> 
> The class is named after the media device it handles, which is called
> "rzg2l_cru" but the CRU is actually found in other SoCs than the RZ/G2L
> one, such as the RZ/V2H(P).
> 
> The RZG2LCRU class models the CSI-2 receiver found on those SoCs and
> allows to add support for memory-to-memory operations on the RZ/V2H(P)
> SoC which integrates a Mali-C55 ISP and a specialized DMA engine named
> IVC that feeds images from memory, where the CRU has saved them.
> 
> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>   src/libcamera/pipeline/mali-c55/meson.build   |   3 +-
>   src/libcamera/pipeline/mali-c55/rzg2l-cru.cpp | 261 ++++++++++++++++++++++++++
>   src/libcamera/pipeline/mali-c55/rzg2l-cru.h   |  72 +++++++
>   3 files changed, 335 insertions(+), 1 deletion(-)
> 
> [...]
> +
> +void RZG2LCRU::cruReturnBuffer(FrameBuffer *buffer)
> +{
> +	for (const std::unique_ptr<FrameBuffer> &buf : buffers_) {
> +		if (buf.get() == buffer) {
> +			availableBuffers_.push(buffer);
> +			break;
> +		}
> +	}

I am bit confused by this. Is it expected that sometimes `buffer` will
not be in `buffers_`? Is that not an error case?

It would be a bit more clear to me like this:

   auto it = std::find_if(buffers_.begin(), buffers_.end(), [&](const auto& b) { return b.get() == buffer; }); // or similar

   // ASSERT(it != buffers_.end())
   // if (it == buffers_.end()) return;
   // etc.

   availableBuffers_.push(buffer);


> +}
> [...]
> +void RZG2LCRU::initCRUSizes()
> +{
> +	Size maxCSI2Size;
> +
> +	/*
> +	 * Get the maximum supported size on the CSI-2 receiver. We need to
> +	 * query the kernel interface as the size limits differ from RZ/G2L
> +	 * (2800x4095) and RZ/V2H (4096x4096).
> +	 */
> +	V4L2Subdevice::Formats csi2Formats = csi2_->formats(0);
> +	if (csi2Formats.empty())
> +		return;
> +
> +	for (const auto &format : csi2Formats) {
> +		for (const auto &range : format.second) {
> +			if (range.max > maxCSI2Size)
> +				maxCSI2Size = range.max;
> +		}
> +	}
> +
> +	/*
> +	 * Enumerate the sensor supported resolutiond and filter out the ones

resolutiond -> resolutions


> +	 * largest than the maximum supported CSI-2 receiver input size.

largest -> larger


> +	 */
> +	V4L2Subdevice::Formats formats = sensor_->device()->formats(0);
> +	if (formats.empty())
> +		return;
> +
> +	for (const auto &format : formats) {
> +		for (const auto &range : format.second) {
> +			const Size &max = range.max;
> +
> +			if (max.width > maxCSI2Size.width ||
> +			    max.height > maxCSI2Size.height)
> +				continue;
> +
> +			csi2Sizes_.push_back(max);
> +		}
> +	}
> +
> +	/* Sort in increasing order and remove duplicates. */
> +	std::sort(csi2Sizes_.begin(), csi2Sizes_.end());
> +	auto last = std::unique(csi2Sizes_.begin(), csi2Sizes_.end());
> +	csi2Sizes_.erase(last, csi2Sizes_.end());
> +
> +	csi2Resolution_ = csi2Sizes_.back();
> +}
> +
> +int RZG2LCRU::init(const MediaDevice *media)
> +{
> +	int ret;
> +
> +	csi2_ = V4L2Subdevice::fromEntityName(media,
> +					      std::regex("csi-[0-9a-f]{8}.csi2"));

Maybe have `static const std::regex csi2Regex("...");` somewhere?


> +	if (!csi2_)
> +		return -ENODEV;
> +
> +	ret = csi2_->open();
> +	if (ret)
> +		return ret;
> +
> +	const std::vector<MediaPad *> &pads = csi2_->entity()->pads();
> +	if (pads.empty())
> +		return -ENODEV;
> +
> +	/* The receiver has a single sink pad at index 0 */
> +	MediaPad *sink = pads[0];
> +	const std::vector<MediaLink *> &links = sink->links();
> +	if (links.empty())
> +		return -ENODEV;
> +
> +	MediaLink *link = links[0];
> +	sensor_ = CameraSensorFactoryBase::create(link->source()->entity());
> +	if (!sensor_)
> +		return -ENODEV;
> +
> +	cru_ = V4L2Subdevice::fromEntityName(media,
> +					     std::regex("cru-ip-[0-9a-f]{8}.cru[0-9]"));

Same here.


> +	ret = cru_->open();
> +	if (ret)
> +		return ret;
> +
> +	output_ = V4L2VideoDevice::fromEntityName(media, "CRU output");
> +	ret = output_->open();
> +	if (ret)
> +		return ret;
> +
> +	initCRUSizes();
> +
> +	return 0;
> +}
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/pipeline/mali-c55/rzg2l-cru.h b/src/libcamera/pipeline/mali-c55/rzg2l-cru.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..9944e71cda82a494182619f01766f32f57a8b516
> --- /dev/null
> +++ b/src/libcamera/pipeline/mali-c55/rzg2l-cru.h
> @@ -0,0 +1,72 @@
> [...]
> +class RZG2LCRU
> +{
> +public:
> +	static constexpr unsigned int kBufferCount = 4;
> +
> +	RZG2LCRU() = default;
> +
> +	const std::vector<Size> &sizes() const
> +	{
> +		return csi2Sizes_;
> +	}
> +
> +	int init(const MediaDevice *media);
> +	const Size &resolution() const
> +	{
> +		return csi2Resolution_;
> +	}
> +
> +	CameraSensor *sensor() const { return sensor_.get(); }
> +
> +	int configure(V4L2SubdeviceFormat *subdevFormat, V4L2DeviceFormat *inputFormat);
> +	FrameBuffer *queueBuffer(Request *request);
> +	void cruReturnBuffer(FrameBuffer *buffer);
> +	V4L2VideoDevice *output() { return output_.get(); }
> +	int start();
> +	int stop();

An empty line is missing for me here.


Regards,
Barnabás Pőcze


> +private:
> +	void freeBuffers();
> +
> [...]
Jacopo Mondi Dec. 10, 2025, 8:28 a.m. UTC | #2
Hi Barnabás

On Mon, Dec 08, 2025 at 10:48:40AM +0100, Barnabás Pőcze wrote:
> Hi
>
> 2025. 12. 05. 15:52 keltezéssel, Jacopo Mondi írta:
> > From: Daniel Scally <dan.scally@ideasonboard.com>
> >
> > Add a class allowing us to control the RZ/G2L Camera Receiver Unit.
> >
> > The class is named after the media device it handles, which is called
> > "rzg2l_cru" but the CRU is actually found in other SoCs than the RZ/G2L
> > one, such as the RZ/V2H(P).
> >
> > The RZG2LCRU class models the CSI-2 receiver found on those SoCs and
> > allows to add support for memory-to-memory operations on the RZ/V2H(P)
> > SoC which integrates a Mali-C55 ISP and a specialized DMA engine named
> > IVC that feeds images from memory, where the CRU has saved them.
> >
> > Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
> > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > ---
> >   src/libcamera/pipeline/mali-c55/meson.build   |   3 +-
> >   src/libcamera/pipeline/mali-c55/rzg2l-cru.cpp | 261 ++++++++++++++++++++++++++
> >   src/libcamera/pipeline/mali-c55/rzg2l-cru.h   |  72 +++++++
> >   3 files changed, 335 insertions(+), 1 deletion(-)
> >
> > [...]
> > +
> > +void RZG2LCRU::cruReturnBuffer(FrameBuffer *buffer)
> > +{
> > +	for (const std::unique_ptr<FrameBuffer> &buf : buffers_) {
> > +		if (buf.get() == buffer) {
> > +			availableBuffers_.push(buffer);
> > +			break;
> > +		}
> > +	}
>
> I am bit confused by this. Is it expected that sometimes `buffer` will
> not be in `buffers_`? Is that not an error case?

I don't see how it can happen, and if it happens is a programming
mistake that's worth an assertion imho

>
> It would be a bit more clear to me like this:
>
>   auto it = std::find_if(buffers_.begin(), buffers_.end(), [&](const auto& b) { return b.get() == buffer; }); // or similar
>
>   // ASSERT(it != buffers_.end())

yes!

>   // if (it == buffers_.end()) return;
>   // etc.
>
>   availableBuffers_.push(buffer);
>
>
> > +}
> > [...]
> > +void RZG2LCRU::initCRUSizes()
> > +{
> > +	Size maxCSI2Size;
> > +
> > +	/*
> > +	 * Get the maximum supported size on the CSI-2 receiver. We need to
> > +	 * query the kernel interface as the size limits differ from RZ/G2L
> > +	 * (2800x4095) and RZ/V2H (4096x4096).
> > +	 */
> > +	V4L2Subdevice::Formats csi2Formats = csi2_->formats(0);
> > +	if (csi2Formats.empty())
> > +		return;
> > +
> > +	for (const auto &format : csi2Formats) {
> > +		for (const auto &range : format.second) {
> > +			if (range.max > maxCSI2Size)
> > +				maxCSI2Size = range.max;
> > +		}
> > +	}
> > +
> > +	/*
> > +	 * Enumerate the sensor supported resolutiond and filter out the ones
>
> resolutiond -> resolutions
>
>
> > +	 * largest than the maximum supported CSI-2 receiver input size.
>
> largest -> larger
>

Thanks

>
> > +	 */
> > +	V4L2Subdevice::Formats formats = sensor_->device()->formats(0);
> > +	if (formats.empty())
> > +		return;
> > +
> > +	for (const auto &format : formats) {
> > +		for (const auto &range : format.second) {
> > +			const Size &max = range.max;
> > +
> > +			if (max.width > maxCSI2Size.width ||
> > +			    max.height > maxCSI2Size.height)
> > +				continue;
> > +
> > +			csi2Sizes_.push_back(max);
> > +		}
> > +	}
> > +
> > +	/* Sort in increasing order and remove duplicates. */
> > +	std::sort(csi2Sizes_.begin(), csi2Sizes_.end());
> > +	auto last = std::unique(csi2Sizes_.begin(), csi2Sizes_.end());
> > +	csi2Sizes_.erase(last, csi2Sizes_.end());
> > +
> > +	csi2Resolution_ = csi2Sizes_.back();
> > +}
> > +
> > +int RZG2LCRU::init(const MediaDevice *media)
> > +{
> > +	int ret;
> > +
> > +	csi2_ = V4L2Subdevice::fromEntityName(media,
> > +					      std::regex("csi-[0-9a-f]{8}.csi2"));
>
> Maybe have `static const std::regex csi2Regex("...");` somewhere?
>

ok
>
> > +	if (!csi2_)
> > +		return -ENODEV;
> > +
> > +	ret = csi2_->open();
> > +	if (ret)
> > +		return ret;
> > +
> > +	const std::vector<MediaPad *> &pads = csi2_->entity()->pads();
> > +	if (pads.empty())
> > +		return -ENODEV;
> > +
> > +	/* The receiver has a single sink pad at index 0 */
> > +	MediaPad *sink = pads[0];
> > +	const std::vector<MediaLink *> &links = sink->links();
> > +	if (links.empty())
> > +		return -ENODEV;
> > +
> > +	MediaLink *link = links[0];
> > +	sensor_ = CameraSensorFactoryBase::create(link->source()->entity());
> > +	if (!sensor_)
> > +		return -ENODEV;
> > +
> > +	cru_ = V4L2Subdevice::fromEntityName(media,
> > +					     std::regex("cru-ip-[0-9a-f]{8}.cru[0-9]"));
>
> Same here.
>
>
> > +	ret = cru_->open();
> > +	if (ret)
> > +		return ret;
> > +
> > +	output_ = V4L2VideoDevice::fromEntityName(media, "CRU output");
> > +	ret = output_->open();
> > +	if (ret)
> > +		return ret;
> > +
> > +	initCRUSizes();
> > +
> > +	return 0;
> > +}
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/libcamera/pipeline/mali-c55/rzg2l-cru.h b/src/libcamera/pipeline/mali-c55/rzg2l-cru.h
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..9944e71cda82a494182619f01766f32f57a8b516
> > --- /dev/null
> > +++ b/src/libcamera/pipeline/mali-c55/rzg2l-cru.h
> > @@ -0,0 +1,72 @@
> > [...]
> > +class RZG2LCRU
> > +{
> > +public:
> > +	static constexpr unsigned int kBufferCount = 4;
> > +
> > +	RZG2LCRU() = default;
> > +
> > +	const std::vector<Size> &sizes() const
> > +	{
> > +		return csi2Sizes_;
> > +	}
> > +
> > +	int init(const MediaDevice *media);
> > +	const Size &resolution() const
> > +	{
> > +		return csi2Resolution_;
> > +	}
> > +
> > +	CameraSensor *sensor() const { return sensor_.get(); }
> > +
> > +	int configure(V4L2SubdeviceFormat *subdevFormat, V4L2DeviceFormat *inputFormat);
> > +	FrameBuffer *queueBuffer(Request *request);
> > +	void cruReturnBuffer(FrameBuffer *buffer);
> > +	V4L2VideoDevice *output() { return output_.get(); }
> > +	int start();
> > +	int stop();
>
> An empty line is missing for me here.

yes it is! thanks

>
>
> Regards,
> Barnabás Pőcze
>
>
> > +private:
> > +	void freeBuffers();
> > +
> > [...]
>

Patch
diff mbox series

diff --git a/src/libcamera/pipeline/mali-c55/meson.build b/src/libcamera/pipeline/mali-c55/meson.build
index eba8e5a3905469297ade6ed5bf523473448cb9de..4e768242602e563612dca7ccbd0d11f5e5ab92a5 100644
--- a/src/libcamera/pipeline/mali-c55/meson.build
+++ b/src/libcamera/pipeline/mali-c55/meson.build
@@ -1,5 +1,6 @@ 
 # SPDX-License-Identifier: CC0-1.0
 
 libcamera_internal_sources += files([
-    'mali-c55.cpp'
+    'mali-c55.cpp',
+    'rzg2l-cru.cpp',
 ])
diff --git a/src/libcamera/pipeline/mali-c55/rzg2l-cru.cpp b/src/libcamera/pipeline/mali-c55/rzg2l-cru.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ac540ba2e29d25191e87a6208d33094e4ef2b969
--- /dev/null
+++ b/src/libcamera/pipeline/mali-c55/rzg2l-cru.cpp
@@ -0,0 +1,261 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas on Board Oy
+ *
+ * Pipine handler element for the Renesas RZ/G2L Camera Receiver Unit
+ */
+
+#include "rzg2l-cru.h"
+
+#include <algorithm>
+#include <map>
+
+#include <linux/videodev2.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/regex.h>
+
+#include <libcamera/formats.h>
+#include <libcamera/pixel_format.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/media_device.h"
+#include "libcamera/internal/request.h"
+
+namespace libcamera {
+
+static const std::map<uint8_t, V4L2PixelFormat> bitDepthToFmt{
+	{ 10, V4L2PixelFormat(V4L2_PIX_FMT_RAW_CRU10) },
+	{ 12, V4L2PixelFormat(V4L2_PIX_FMT_RAW_CRU12) },
+	{ 14, V4L2PixelFormat(V4L2_PIX_FMT_RAW_CRU14) },
+};
+
+LOG_DEFINE_CATEGORY(RZG2LCRU)
+
+FrameBuffer *RZG2LCRU::queueBuffer(Request *request)
+{
+	FrameBuffer *buffer;
+
+	if (availableBuffers_.empty()) {
+		LOG(RZG2LCRU, Debug) << "CRU buffer underrun";
+		return nullptr;
+	}
+
+	buffer = availableBuffers_.front();
+
+	int ret = output_->queueBuffer(buffer);
+	if (ret) {
+		LOG(RZG2LCRU, Error) << "Failed to queue buffer to CRU";
+		return nullptr;
+	}
+
+	availableBuffers_.pop();
+	buffer->_d()->setRequest(request);
+
+	return buffer;
+}
+
+void RZG2LCRU::cruReturnBuffer(FrameBuffer *buffer)
+{
+	for (const std::unique_ptr<FrameBuffer> &buf : buffers_) {
+		if (buf.get() == buffer) {
+			availableBuffers_.push(buffer);
+			break;
+		}
+	}
+}
+
+int RZG2LCRU::start()
+{
+	int ret = output_->exportBuffers(kBufferCount, &buffers_);
+	if (ret < 0)
+		return ret;
+
+	ret = output_->importBuffers(kBufferCount);
+	if (ret)
+		return ret;
+
+	for (std::unique_ptr<FrameBuffer> &buffer : buffers_)
+		availableBuffers_.push(buffer.get());
+
+	ret = output_->streamOn();
+	if (ret) {
+		freeBuffers();
+		return ret;
+	}
+
+	return 0;
+}
+
+int RZG2LCRU::stop()
+{
+	int ret;
+
+	csi2_->setFrameStartEnabled(false);
+
+	ret = output_->streamOff();
+
+	freeBuffers();
+
+	return ret;
+}
+
+void RZG2LCRU::freeBuffers()
+{
+	availableBuffers_ = {};
+	buffers_.clear();
+
+	if (output_->releaseBuffers())
+		LOG(RZG2LCRU, Error) << "Failed to release CRU buffers";
+}
+
+int RZG2LCRU::configure(V4L2SubdeviceFormat *subdevFormat, V4L2DeviceFormat *inputFormat)
+{
+	/*
+	 * Set format on the sensor and propagate it up to the CRU video
+	 * device.
+	 */
+
+	int ret = sensor_->setFormat(subdevFormat);
+	if (ret)
+		return ret;
+
+	ret = csi2_->setFormat(0, subdevFormat);
+	if (ret)
+		return ret;
+
+	ret = csi2_->getFormat(1, subdevFormat);
+	if (ret)
+		return ret;
+
+	ret = cru_->setFormat(0, subdevFormat);
+	if (ret)
+		return ret;
+
+	ret = cru_->getFormat(1, subdevFormat);
+	if (ret)
+		return ret;
+
+	/*
+	 * The capture device needs to be set with a format that can be produced
+	 * from the mbus code of the subdevFormat. The CRU and IVC use bayer
+	 * order agnostic pixel formats, so all we need to do is find the right
+	 * bitdepth and select the appropriate format.
+	 */
+	BayerFormat bayerFormat = BayerFormat::fromMbusCode(subdevFormat->code);
+	if (!bayerFormat.isValid())
+		return -EINVAL;
+
+	V4L2DeviceFormat captureFormat;
+	captureFormat.fourcc = bitDepthToFmt.at(bayerFormat.bitDepth);
+	captureFormat.size = subdevFormat->size;
+
+	ret = output_->setFormat(&captureFormat);
+	if (ret)
+		return ret;
+
+	/*
+	 * We return the format that we set against the output device, as the
+	 * same format will also need to be set against the Input Video Control
+	 * Block device.
+	 */
+	*inputFormat = captureFormat;
+
+	return 0;
+}
+
+void RZG2LCRU::initCRUSizes()
+{
+	Size maxCSI2Size;
+
+	/*
+	 * Get the maximum supported size on the CSI-2 receiver. We need to
+	 * query the kernel interface as the size limits differ from RZ/G2L
+	 * (2800x4095) and RZ/V2H (4096x4096).
+	 */
+	V4L2Subdevice::Formats csi2Formats = csi2_->formats(0);
+	if (csi2Formats.empty())
+		return;
+
+	for (const auto &format : csi2Formats) {
+		for (const auto &range : format.second) {
+			if (range.max > maxCSI2Size)
+				maxCSI2Size = range.max;
+		}
+	}
+
+	/*
+	 * Enumerate the sensor supported resolutiond and filter out the ones
+	 * largest than the maximum supported CSI-2 receiver input size.
+	 */
+	V4L2Subdevice::Formats formats = sensor_->device()->formats(0);
+	if (formats.empty())
+		return;
+
+	for (const auto &format : formats) {
+		for (const auto &range : format.second) {
+			const Size &max = range.max;
+
+			if (max.width > maxCSI2Size.width ||
+			    max.height > maxCSI2Size.height)
+				continue;
+
+			csi2Sizes_.push_back(max);
+		}
+	}
+
+	/* Sort in increasing order and remove duplicates. */
+	std::sort(csi2Sizes_.begin(), csi2Sizes_.end());
+	auto last = std::unique(csi2Sizes_.begin(), csi2Sizes_.end());
+	csi2Sizes_.erase(last, csi2Sizes_.end());
+
+	csi2Resolution_ = csi2Sizes_.back();
+}
+
+int RZG2LCRU::init(const MediaDevice *media)
+{
+	int ret;
+
+	csi2_ = V4L2Subdevice::fromEntityName(media,
+					      std::regex("csi-[0-9a-f]{8}.csi2"));
+	if (!csi2_)
+		return -ENODEV;
+
+	ret = csi2_->open();
+	if (ret)
+		return ret;
+
+	const std::vector<MediaPad *> &pads = csi2_->entity()->pads();
+	if (pads.empty())
+		return -ENODEV;
+
+	/* The receiver has a single sink pad at index 0 */
+	MediaPad *sink = pads[0];
+	const std::vector<MediaLink *> &links = sink->links();
+	if (links.empty())
+		return -ENODEV;
+
+	MediaLink *link = links[0];
+	sensor_ = CameraSensorFactoryBase::create(link->source()->entity());
+	if (!sensor_)
+		return -ENODEV;
+
+	cru_ = V4L2Subdevice::fromEntityName(media,
+					     std::regex("cru-ip-[0-9a-f]{8}.cru[0-9]"));
+	ret = cru_->open();
+	if (ret)
+		return ret;
+
+	output_ = V4L2VideoDevice::fromEntityName(media, "CRU output");
+	ret = output_->open();
+	if (ret)
+		return ret;
+
+	initCRUSizes();
+
+	return 0;
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/mali-c55/rzg2l-cru.h b/src/libcamera/pipeline/mali-c55/rzg2l-cru.h
new file mode 100644
index 0000000000000000000000000000000000000000..9944e71cda82a494182619f01766f32f57a8b516
--- /dev/null
+++ b/src/libcamera/pipeline/mali-c55/rzg2l-cru.h
@@ -0,0 +1,72 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas on Board Oy
+ *
+ * Pipine handler element for the Renesas RZ/G2L Camera Receiver Unit
+ */
+
+#pragma once
+
+#include <memory>
+#include <queue>
+#include <vector>
+
+#include <libcamera/base/signal.h>
+
+#include "libcamera/internal/v4l2_subdevice.h"
+#include "libcamera/internal/v4l2_videodevice.h"
+
+namespace libcamera {
+
+class CameraSensor;
+class FrameBuffer;
+class MediaDevice;
+class Request;
+class Size;
+
+class RZG2LCRU
+{
+public:
+	static constexpr unsigned int kBufferCount = 4;
+
+	RZG2LCRU() = default;
+
+	const std::vector<Size> &sizes() const
+	{
+		return csi2Sizes_;
+	}
+
+	int init(const MediaDevice *media);
+	const Size &resolution() const
+	{
+		return csi2Resolution_;
+	}
+
+	CameraSensor *sensor() const { return sensor_.get(); }
+
+	int configure(V4L2SubdeviceFormat *subdevFormat, V4L2DeviceFormat *inputFormat);
+	FrameBuffer *queueBuffer(Request *request);
+	void cruReturnBuffer(FrameBuffer *buffer);
+	V4L2VideoDevice *output() { return output_.get(); }
+	int start();
+	int stop();
+private:
+	void freeBuffers();
+
+	void cruBufferReady(FrameBuffer *buffer);
+
+	void initCRUSizes();
+
+	std::unique_ptr<CameraSensor> sensor_;
+	std::unique_ptr<V4L2Subdevice> csi2_;
+	std::unique_ptr<V4L2Subdevice> cru_;
+	std::unique_ptr<V4L2VideoDevice> output_;
+
+	std::vector<std::unique_ptr<FrameBuffer>> buffers_;
+	std::queue<FrameBuffer *> availableBuffers_;
+
+	std::vector<Size> csi2Sizes_;
+	Size csi2Resolution_;
+};
+
+} /* namespace libcamera */