[RFC,05/19] ipa: rkisp2: Add rkisp2 ipa
diff mbox series

Message ID 20260703122543.1991189-6-paul.elder@ideasonboard.com
State New
Headers show
Series
  • Add support for rkisp2
Related show

Commit Message

Paul Elder July 3, 2026, 12:25 p.m. UTC
Implement the rkisp2 IPA. It is based on the rkisp1 IPA. No control
algorithms are implemented yet, but this lays the foundation and
plumbing for the IPA to function. The control algorithms will be added
in later patches.

Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>

---
This is also split from other patches to make them all easier to review.
I put this patch before the pipeline handler patch so that the pipeline
handler doesn't error out when it fails to find an IPA.
---
 meson_options.txt                     |   4 +-
 src/ipa/rkisp2/algorithms/meson.build |   5 +
 src/ipa/rkisp2/algorithms/module.h    |  28 ++
 src/ipa/rkisp2/ipa_context.h          | 201 ++++++++++++
 src/ipa/rkisp2/meson.build            |  29 ++
 src/ipa/rkisp2/rkisp2.cpp             | 429 ++++++++++++++++++++++++++
 6 files changed, 694 insertions(+), 2 deletions(-)
 create mode 100644 src/ipa/rkisp2/algorithms/meson.build
 create mode 100644 src/ipa/rkisp2/algorithms/module.h
 create mode 100644 src/ipa/rkisp2/ipa_context.h
 create mode 100644 src/ipa/rkisp2/meson.build
 create mode 100644 src/ipa/rkisp2/rkisp2.cpp

Patch
diff mbox series

diff --git a/meson_options.txt b/meson_options.txt
index 20baacc4fc65..2638c77cb8b0 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -48,8 +48,8 @@  option('gstreamer',
 
 option('ipas',
         type : 'array',
-        choices : ['ipu3', 'mali-c55', 'rkisp1', 'rpi/pisp', 'rpi/vc4', 'simple',
-                   'vimc'],
+        choices : ['ipu3', 'mali-c55', 'rkisp1', 'rkisp2', 'rpi/pisp',
+                   'rpi/vc4', 'simple', 'vimc'],
         description : 'Select which IPA modules to build')
 
 option('lc-compliance',
diff --git a/src/ipa/rkisp2/algorithms/meson.build b/src/ipa/rkisp2/algorithms/meson.build
new file mode 100644
index 000000000000..4bb81e48eb01
--- /dev/null
+++ b/src/ipa/rkisp2/algorithms/meson.build
@@ -0,0 +1,5 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+rkisp2_ipa_algorithms = files([
+])
+
diff --git a/src/ipa/rkisp2/algorithms/module.h b/src/ipa/rkisp2/algorithms/module.h
new file mode 100644
index 000000000000..23507e1db81f
--- /dev/null
+++ b/src/ipa/rkisp2/algorithms/module.h
@@ -0,0 +1,28 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas on Board Oy.
+ *
+ * RkISP2 IPA Module
+ */
+
+#pragma once
+
+#include <linux/rkisp2-config.h>
+
+#include <libcamera/ipa/rkisp2_ipa_interface.h>
+
+#include <libipa/module.h>
+
+#include "ipa_context.h"
+#include "params.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp2 {
+
+using Module = ipa::Module<IPAContext, IPAFrameContext, IPACameraSensorInfo,
+			   RkISP2Params, rkisp2_stats_buffer>;
+
+} /* namespace ipa::rkisp2 */
+
+} /* namespace libcamera*/
diff --git a/src/ipa/rkisp2/ipa_context.h b/src/ipa/rkisp2/ipa_context.h
new file mode 100644
index 000000000000..c806f1fe1bfa
--- /dev/null
+++ b/src/ipa/rkisp2/ipa_context.h
@@ -0,0 +1,201 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas on Board Oy.
+ *
+ * RkISP2 IPA Context
+ *
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <linux/rkisp2-config.h>
+
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/geometry.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "libcamera/internal/debug_controls.h"
+#include "libcamera/internal/matrix.h"
+#include "libcamera/internal/vector.h"
+
+#include "libipa/agc_mean_luminance.h"
+#include "libipa/awb.h"
+#include "libipa/camera_sensor_helper.h"
+#include "libipa/ccm.h"
+#include "libipa/fc_queue.h"
+#include "libipa/fixedpoint.h"
+#include "libipa/lsc.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp2 {
+
+struct IPAHwSettings {
+	unsigned int numAeCells;
+	unsigned int numHistogramBins;
+	unsigned int numHistogramWeights;
+	unsigned int numGammaOutSamples;
+	uint32_t supportedBlocks;
+	bool compand;
+};
+
+struct RKISP2AwbSession {
+	struct rkisp2_isp_window measureWindow;
+	bool enabled;
+};
+
+struct IPASessionConfiguration {
+	struct {
+		struct rkisp2_isp_window measureWindow;
+		struct rkisp2_isp_window measureWindow15;
+	} agc;
+
+	struct RKISP2AwbSession awb;
+
+	struct {
+		utils::Duration minExposureTime;
+		utils::Duration maxExposureTime;
+		double minAnalogueGain;
+		double maxAnalogueGain;
+
+		int32_t defVBlank;
+		utils::Duration lineDuration;
+		Size size;
+	} sensor;
+
+	struct {
+		int32_t colorSpaceEncoding;
+		int32_t colorSpaceRange;
+	} csm;
+
+	bool raw;
+};
+
+struct IPAActiveState {
+	struct {
+		struct {
+			uint32_t exposure;
+			double gain;
+		} manual;
+		struct {
+			uint32_t exposure;
+			double gain;
+			double quantizationGain;
+			double yTarget;
+		} automatic;
+
+		bool autoExposureEnabled;
+		bool autoGainEnabled;
+		double exposureValue;
+		controls::AeConstraintModeEnum constraintMode;
+		controls::AeExposureModeEnum exposureMode;
+		controls::AeMeteringModeEnum meteringMode;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
+	} agc;
+
+	ipa::awb::ActiveState awb;
+
+	struct {
+		double gamma;
+	} goc;
+
+	ipa::ccm::ActiveState ccm;
+
+	struct {
+		double lux;
+	} lux;
+
+	struct {
+		controls::WdrModeEnum mode;
+		AgcMeanLuminance::AgcConstraint constraint;
+		double gain;
+		double strength;
+	} wdr;
+
+	ipa::lsc::ActiveState lsc;
+
+	struct {
+		Matrix<uint16_t, 3, 3> csm;
+		bool update;
+	} csm;
+};
+
+struct IPAFrameContext : public FrameContext {
+	struct {
+		uint32_t exposure;
+		double gain;
+		double exposureValue;
+		double quantizationGain;
+		uint32_t vblank;
+		double yTarget;
+		bool autoExposureEnabled;
+		bool autoGainEnabled;
+		controls::AeConstraintModeEnum constraintMode;
+		controls::AeExposureModeEnum exposureMode;
+		controls::AeMeteringModeEnum meteringMode;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
+		utils::Duration frameDuration;
+		bool updateMetering;
+		bool autoExposureModeChange;
+		bool autoGainModeChange;
+	} agc;
+
+	ipa::awb::FrameContext awb;
+
+	struct {
+		double gamma;
+		bool update;
+	} goc;
+
+	struct {
+		uint32_t exposure;
+		double gain;
+	} sensor;
+
+	ipa::ccm::FrameContext ccm;
+
+	struct {
+		double lux;
+	} lux;
+
+	struct {
+		controls::WdrModeEnum mode;
+		double strength;
+		double gain;
+	} wdr;
+
+	ipa::lsc::FrameContext lsc;
+};
+
+struct IPAContext {
+	IPAContext(unsigned int frameContextSize)
+		: frameContexts(frameContextSize)
+	{
+	}
+
+	IPAHwSettings hw;
+	IPACameraSensorInfo sensorInfo;
+	IPASessionConfiguration configuration;
+	IPAActiveState activeState;
+
+	FCQueue<IPAFrameContext> frameContexts;
+
+	ControlInfoMap::Map ctrlMap;
+
+	DebugMetadata debugMetadata;
+
+	/* Interface to the Camera Helper */
+	std::unique_ptr<CameraSensorHelper> camHelper;
+};
+
+} /* namespace ipa::rkisp2 */
+
+} /* namespace libcamera*/
diff --git a/src/ipa/rkisp2/meson.build b/src/ipa/rkisp2/meson.build
new file mode 100644
index 000000000000..f2f435c2d66f
--- /dev/null
+++ b/src/ipa/rkisp2/meson.build
@@ -0,0 +1,29 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+subdir('algorithms')
+
+ipa_name = 'ipa_rkisp2'
+
+rkisp2_ipa_sources = files([
+    'rkisp2.cpp',
+])
+
+rkisp2_ipa_sources += rkisp2_ipa_algorithms
+
+mod = shared_module(ipa_name, rkisp2_ipa_sources,
+                    name_prefix : '',
+                    include_directories : [ipa_includes],
+                    dependencies : [libcamera_private, libipa_dep],
+                    install : true,
+                    install_dir : ipa_install_dir)
+
+if ipa_sign_module
+    custom_target(ipa_name + '.so.sign',
+                  input : mod,
+                  output : ipa_name + '.so.sign',
+                  command : [ipa_sign, ipa_priv_key, '@INPUT@', '@OUTPUT@'],
+                  install : false,
+                  build_by_default : true)
+endif
+
+ipa_names += ipa_name
diff --git a/src/ipa/rkisp2/rkisp2.cpp b/src/ipa/rkisp2/rkisp2.cpp
new file mode 100644
index 000000000000..18053c7d802a
--- /dev/null
+++ b/src/ipa/rkisp2/rkisp2.cpp
@@ -0,0 +1,429 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas on Board Oy.
+ *
+ * RkISP2 Image Processing Algorithms
+ */
+
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <functional>
+#include <stdint.h>
+#include <string.h>
+
+#include <linux/rkisp2-config.h>
+#include <linux/v4l2-controls.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/request.h>
+
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+#include <libcamera/ipa/rkisp2_ipa_interface.h>
+
+#include "libcamera/internal/formats.h"
+#include "libcamera/internal/mapped_framebuffer.h"
+#include "libcamera/internal/yaml_parser.h"
+
+#include "algorithms/algorithm.h"
+
+#include "ipa_context.h"
+#include "params.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(IPARkISP2)
+
+using namespace std::literals::chrono_literals;
+
+namespace ipa::rkisp2 {
+
+/* Maximum number of frame contexts to be held */
+static constexpr uint32_t kMaxFrameContexts = 16;
+
+class IPARkISP2 : public IPARkISP2Interface, public Module
+{
+public:
+	IPARkISP2();
+
+	int init(const IPASettings &settings,
+		 const IPACameraSensorInfo &sensorInfo,
+		 const ControlInfoMap &sensorControls,
+		 ControlInfoMap *ipaControls) override;
+	int start() override;
+	void stop() override;
+
+	int configure(const IPAConfigInfo &ipaConfig,
+		      ControlInfoMap *ipaControls) override;
+	void mapBuffers(const std::vector<IPABuffer> &buffers) override;
+	void unmapBuffers(const std::vector<unsigned int> &ids) override;
+
+	void queueRequest(const uint32_t frame, const ControlList &controls) override;
+	void computeParams(const uint32_t frame, const uint32_t bufferId) override;
+	void initializeFrameContext(IPAFrameContext &frameContext,
+				    const ControlList &controls);
+	void processStats(const uint32_t frame, const uint32_t bufferId,
+			  const ControlList &sensorControls) override;
+
+protected:
+	std::string logPrefix() const override;
+
+private:
+	void updateControls(const IPACameraSensorInfo &sensorInfo,
+			    const ControlInfoMap &sensorControls,
+			    ControlInfoMap *ipaControls);
+
+	void setControls(unsigned int frame, const IPAFrameContext &frameContext);
+
+	std::map<unsigned int, FrameBuffer> buffers_;
+	std::map<unsigned int, MappedFrameBuffer> mappedBuffers_;
+
+	ControlInfoMap sensorControls_;
+
+	/* Local parameter storage */
+	struct IPAContext context_;
+};
+
+namespace {
+
+/* List of controls handled by the RkISP2 IPA */
+const ControlInfoMap::Map rkisp2Controls{
+	{ &controls::DebugMetadataEnable, ControlInfo(false, true, false) },
+};
+
+} /* namespace */
+
+IPARkISP2::IPARkISP2()
+	: context_(kMaxFrameContexts)
+{
+	context_.frameContexts.setInitCallback(
+		[this](IPAFrameContext &fc, const ControlList &c) {
+			this->initializeFrameContext(fc, c);
+		});
+}
+
+std::string IPARkISP2::logPrefix() const
+{
+	return "rkisp2";
+}
+
+int IPARkISP2::init(const IPASettings &settings,
+		    const IPACameraSensorInfo &sensorInfo,
+		    const ControlInfoMap &sensorControls,
+		    ControlInfoMap *ipaControls)
+{
+	context_.sensorInfo = sensorInfo;
+
+	context_.camHelper = CameraSensorHelperFactoryBase::create(settings.sensorModel);
+	if (!context_.camHelper) {
+		LOG(IPARkISP2, Error)
+			<< "Failed to create camera sensor helper for "
+			<< settings.sensorModel;
+		return -ENODEV;
+	}
+
+	context_.configuration.sensor.lineDuration =
+		sensorInfo.minLineLength * 1.0s / sensorInfo.pixelRate;
+
+	/* Load the tuning data file. */
+	File file(settings.configurationFile);
+	if (!file.open(File::OpenModeFlag::ReadOnly)) {
+		int ret = file.error();
+		LOG(IPARkISP2, Error)
+			<< "Failed to open configuration file "
+			<< settings.configurationFile << ": " << strerror(-ret);
+		return ret;
+	}
+
+	std::unique_ptr<libcamera::ValueNode> data = YamlParser::parse(file);
+	if (!data)
+		return -EINVAL;
+
+	unsigned int version = (*data)["version"].get<uint32_t>(0);
+	if (version != 1) {
+		LOG(IPARkISP2, Error)
+			<< "Invalid tuning file version " << version;
+		return -EINVAL;
+	}
+
+	if (!data->contains("algorithms")) {
+		LOG(IPARkISP2, Error)
+			<< "Tuning file doesn't contain any algorithm";
+		return -EINVAL;
+	}
+
+	int ret = createAlgorithms(context_, (*data)["algorithms"]);
+	if (ret)
+		return ret;
+
+	/* Initialize controls. */
+	updateControls(sensorInfo, sensorControls, ipaControls);
+
+	return 0;
+}
+
+int IPARkISP2::start()
+{
+	/* \todo Properly handle startup controls. */
+	return 0;
+}
+
+void IPARkISP2::stop()
+{
+	context_.frameContexts.clear();
+}
+
+int IPARkISP2::configure(const IPAConfigInfo &ipaConfig,
+			 ControlInfoMap *ipaControls)
+{
+	sensorControls_ = ipaConfig.sensorControls;
+
+	const auto itExp = sensorControls_.find(V4L2_CID_EXPOSURE);
+	int32_t minExposure = itExp->second.min().get<int32_t>();
+	int32_t maxExposure = itExp->second.max().get<int32_t>();
+
+	const auto itGain = sensorControls_.find(V4L2_CID_ANALOGUE_GAIN);
+	int32_t minGain = itGain->second.min().get<int32_t>();
+	int32_t maxGain = itGain->second.max().get<int32_t>();
+
+	LOG(IPARkISP2, Debug)
+		<< "Exposure: [" << minExposure << ", " << maxExposure
+		<< "], gain: [" << minGain << ", " << maxGain << "]";
+
+	/* Clear the IPA context before the streaming session. */
+	context_.configuration = {};
+	context_.activeState = {};
+	context_.frameContexts.clear();
+
+	const IPACameraSensorInfo &info = ipaConfig.sensorInfo;
+	const ControlInfo vBlank = sensorControls_.find(V4L2_CID_VBLANK)->second;
+	context_.configuration.sensor.defVBlank = vBlank.def().get<int32_t>();
+	context_.configuration.sensor.size = info.outputSize;
+	context_.configuration.sensor.lineDuration = info.minLineLength * 1.0s / info.pixelRate;
+
+	/* Update the camera controls using the new sensor settings. */
+	updateControls(info, sensorControls_, ipaControls);
+
+	/*
+	 * When the AGC computes the new exposure values for a frame, it needs
+	 * to know the limits for exposure time and analogue gain. As it depends
+	 * on the sensor, update it with the controls.
+	 *
+	 * \todo take VBLANK into account for maximum exposure time
+	 */
+	context_.configuration.sensor.minExposureTime =
+		minExposure * context_.configuration.sensor.lineDuration;
+	context_.configuration.sensor.maxExposureTime =
+		maxExposure * context_.configuration.sensor.lineDuration;
+	context_.configuration.sensor.minAnalogueGain =
+		context_.camHelper->gain(minGain);
+	context_.configuration.sensor.maxAnalogueGain =
+		context_.camHelper->gain(maxGain);
+
+	context_.configuration.csm.colorSpaceEncoding = ipaConfig.colorSpaceEncoding;
+	context_.configuration.csm.colorSpaceRange = ipaConfig.colorSpaceRange;
+
+	for (const auto &a : algorithms()) {
+		Algorithm *algo = static_cast<Algorithm *>(a.get());
+
+		int ret = algo->configure(context_, info);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+void IPARkISP2::mapBuffers(const std::vector<IPABuffer> &buffers)
+{
+	for (const IPABuffer &buffer : buffers) {
+		auto elem = buffers_.emplace(std::piecewise_construct,
+					     std::forward_as_tuple(buffer.id),
+					     std::forward_as_tuple(buffer.planes));
+		const FrameBuffer &fb = elem.first->second;
+
+		MappedFrameBuffer mappedBuffer(&fb, MappedFrameBuffer::MapFlag::ReadWrite);
+		if (!mappedBuffer.isValid()) {
+			LOG(IPARkISP2, Fatal) << "Failed to mmap buffer: "
+					      << strerror(mappedBuffer.error());
+		}
+
+		mappedBuffers_.emplace(buffer.id, std::move(mappedBuffer));
+	}
+}
+
+void IPARkISP2::unmapBuffers(const std::vector<unsigned int> &ids)
+{
+	for (unsigned int id : ids) {
+		const auto fb = buffers_.find(id);
+		if (fb == buffers_.end())
+			continue;
+
+		mappedBuffers_.erase(id);
+		buffers_.erase(id);
+	}
+}
+
+void IPARkISP2::queueRequest(const uint32_t frame, const ControlList &controls)
+{
+	context_.debugMetadata.enableByControl(controls);
+
+	context_.frameContexts.getOrInitContext(frame, controls);
+}
+
+void IPARkISP2::initializeFrameContext(IPAFrameContext &frameContext,
+				       const ControlList &controls)
+{
+	for (const auto &a : algorithms()) {
+		Algorithm *algo = static_cast<Algorithm *>(a.get());
+		algo->queueRequest(context_, frameContext.frame(), frameContext, controls);
+	}
+}
+
+void IPARkISP2::computeParams(const uint32_t frame, const uint32_t bufferId)
+{
+	IPAFrameContext &frameContext = context_.frameContexts.getOrInitContext(frame);
+
+	RkISP2Params params(mappedBuffers_.at(bufferId).planes()[0]);
+
+	for (const auto &algo : algorithms())
+		algo->prepare(context_, frame, frameContext, &params);
+
+	paramsComputed.emit(frame, bufferId, params.bytesused());
+}
+
+void IPARkISP2::processStats(const uint32_t frame, const uint32_t bufferId,
+			     const ControlList &sensorControls)
+{
+	IPAFrameContext &frameContext = context_.frameContexts.getOrInitContext(frame);
+
+	const rkisp2_stats_buffer *stats = reinterpret_cast<rkisp2_stats_buffer *>(
+		mappedBuffers_.at(bufferId).planes()[0].data());
+
+	frameContext.sensor.exposure =
+		sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();
+	frameContext.sensor.gain =
+		context_.camHelper->gain(sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>());
+
+	ControlList metadata(controls::controls);
+
+	for (const auto &algo : algorithms())
+		algo->process(context_, frame, frameContext, stats, metadata);
+
+	setControls(frame, frameContext);
+
+	metadataReady.emit(metadata);
+}
+
+void IPARkISP2::setControls(unsigned int frame, const IPAFrameContext &frameContext)
+{
+	/*
+	 * \todo The frame number is most likely wrong here, we need to take
+	 * internal sensor delays and other timing parameters into account.
+	 */
+
+	uint32_t exposure = frameContext.agc.exposure;
+	uint32_t gain = context_.camHelper->gainCode(frameContext.agc.gain);
+	uint32_t vblank = frameContext.agc.vblank;
+
+	LOG(IPARkISP2, Debug)
+		<< "Set controls for frame " << frame << ": exposure " << exposure
+		<< ", gain " << frameContext.agc.gain << ", vblank " << vblank;
+
+	ControlList ctrls(sensorControls_);
+	if (frameContext.agc.exposure * frameContext.agc.gain > 0) {
+		ctrls.set(V4L2_CID_EXPOSURE, static_cast<int32_t>(exposure));
+		ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(gain));
+	}
+	ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank));
+
+	setSensorControls.emit(frame, ctrls);
+}
+
+void IPARkISP2::updateControls(const IPACameraSensorInfo &sensorInfo,
+			       const ControlInfoMap &sensorControls,
+			       ControlInfoMap *ipaControls)
+{
+	ControlInfoMap::Map ctrlMap = rkisp2Controls;
+
+	/*
+	 * Compute exposure time limits from the V4L2_CID_EXPOSURE control
+	 * limits and the line duration.
+	 */
+	double lineDuration = context_.configuration.sensor.lineDuration.get<std::micro>();
+	const ControlInfo &v4l2Exposure = sensorControls.find(V4L2_CID_EXPOSURE)->second;
+	int32_t minExposure = v4l2Exposure.min().get<int32_t>() * lineDuration;
+	int32_t maxExposure = v4l2Exposure.max().get<int32_t>() * lineDuration;
+	int32_t defExposure = v4l2Exposure.def().get<int32_t>() * lineDuration;
+	ctrlMap.emplace(std::piecewise_construct,
+			std::forward_as_tuple(&controls::ExposureTime),
+			std::forward_as_tuple(minExposure, maxExposure, defExposure));
+
+	/* Compute the analogue gain limits. */
+	const ControlInfo &v4l2Gain = sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;
+	float minGain = context_.camHelper->gain(v4l2Gain.min().get<int32_t>());
+	float maxGain = context_.camHelper->gain(v4l2Gain.max().get<int32_t>());
+	float defGain = context_.camHelper->gain(v4l2Gain.def().get<int32_t>());
+	ctrlMap.emplace(std::piecewise_construct,
+			std::forward_as_tuple(&controls::AnalogueGain),
+			std::forward_as_tuple(minGain, maxGain, defGain));
+
+	/*
+	 * Compute the frame duration limits.
+	 *
+	 * The frame length is computed assuming a fixed line length combined
+	 * with the vertical frame sizes.
+	 */
+	const ControlInfo &v4l2HBlank = sensorControls.find(V4L2_CID_HBLANK)->second;
+	uint32_t hblank = v4l2HBlank.def().get<int32_t>();
+	uint32_t lineLength = sensorInfo.outputSize.width + hblank;
+
+	const ControlInfo &v4l2VBlank = sensorControls.find(V4L2_CID_VBLANK)->second;
+	std::array<uint32_t, 3> frameHeights{
+		v4l2VBlank.min().get<int32_t>() + sensorInfo.outputSize.height,
+		v4l2VBlank.max().get<int32_t>() + sensorInfo.outputSize.height,
+		v4l2VBlank.def().get<int32_t>() + sensorInfo.outputSize.height,
+	};
+
+	std::array<int64_t, 3> frameDurations;
+	for (unsigned int i = 0; i < frameHeights.size(); ++i) {
+		uint64_t frameSize = lineLength * frameHeights[i];
+		frameDurations[i] = frameSize / (sensorInfo.pixelRate / 1000000U);
+	}
+
+	/* \todo Move this (and other agc-related controls) to agc */
+	context_.ctrlMap[&controls::FrameDurationLimits] =
+		ControlInfo(frameDurations[0], frameDurations[1],
+			    ControlValue(Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } }));
+
+	ctrlMap.insert(context_.ctrlMap.begin(), context_.ctrlMap.end());
+	*ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls);
+}
+
+} /* namespace ipa::rkisp2 */
+
+/*
+ * External IPA module interface
+ */
+
+extern "C" {
+const struct IPAModuleInfo ipaModuleInfo = {
+	IPA_MODULE_API_VERSION,
+	1,
+	"rkisp2",
+	"rkisp2",
+};
+
+IPAInterface *ipaCreate()
+{
+	return new ipa::rkisp2::IPARkISP2();
+}
+}
+
+} /* namespace libcamera */