[RFC,v1,15/17] ipa: libipa: Add `AgcAlgorithm`
diff mbox series

Message ID 20260703153819.1088752-16-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • ipa: libipa: agc rework
Related show

Commit Message

Barnabás Pőcze July 3, 2026, 3:38 p.m. UTC
Add a class that encompasses the core logic of an agc algorithm with
some calculations and control handling. This can be used to simplify
the implementation of specific algorithms.

Also port `AgcMeanLuminanceAlgorithm` to use it.

Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
---
 src/ipa/ipu3/algorithms/agc.cpp       |   4 +-
 src/ipa/libipa/agc.cpp                | 488 ++++++++++++++++++++++++++
 src/ipa/libipa/agc.h                  | 100 ++++++
 src/ipa/libipa/agc_mean_luminance.cpp | 429 ++--------------------
 src/ipa/libipa/agc_mean_luminance.h   |  61 +---
 src/ipa/libipa/meson.build            |   2 +
 src/ipa/mali-c55/algorithms/agc.cpp   |   4 +-
 src/ipa/rkisp1/algorithms/agc.cpp     |   4 +-
 src/ipa/rkisp1/algorithms/wdr.cpp     |   2 +-
 9 files changed, 627 insertions(+), 467 deletions(-)
 create mode 100644 src/ipa/libipa/agc.cpp
 create mode 100644 src/ipa/libipa/agc.h

Patch
diff mbox series

diff --git a/src/ipa/ipu3/algorithms/agc.cpp b/src/ipa/ipu3/algorithms/agc.cpp
index 0ca02390bd..fbf887c328 100644
--- a/src/ipa/ipu3/algorithms/agc.cpp
+++ b/src/ipa/ipu3/algorithms/agc.cpp
@@ -74,7 +74,7 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 		return ret;
 
 	ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
-		.sensor = *context.camHelper,
+		.sensor = context.camHelper.get(),
 		.sensorInfo = context.sensorInfo,
 		.sensorControls = context.sensorControls,
 		.ctrlMap = context.ctrlMap,
@@ -99,7 +99,7 @@  int Agc::configure(IPAContext &context,
 	bdsGrid_ = context.configuration.grid.bdsGrid;
 
 	return agc_.configure(context.configuration.agc, context.activeState.agc, {
-		.sensor = *context.camHelper,
+		.sensor = context.camHelper.get(),
 		.sensorInfo = context.sensorInfo,
 		.sensorControls = context.sensorControls,
 		.ctrlMap = context.ctrlMap,
diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
new file mode 100644
index 0000000000..7fce952171
--- /dev/null
+++ b/src/ipa/libipa/agc.cpp
@@ -0,0 +1,488 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ */
+
+#include "agc.h"
+
+#include <chrono>
+
+#include <linux/v4l2-controls.h>
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/control_ids.h>
+
+namespace libcamera {
+
+namespace ipa {
+
+using namespace std::chrono_literals;
+
+LOG_DEFINE_CATEGORY(Agc)
+
+/**
+ * \class AgcAlgorithm
+ * \brief Commong handling of AGC related controls and calculation
+ *
+ * \todo DigitalGain, DigitalGainMode
+ */
+
+/**
+ * \struct AgcAlgorithm::Session
+ * \brief Session configuration for AgcAlgorithm
+ *
+ * \var AgcAlgorithm::Session::minExposureTime
+ * \brief Minimum exposure time supported with the configured sensor
+ *
+ * \var AgcAlgorithm::Session::maxExposureTime
+ * \brief Maximum exposure time supported with the configured sensor
+ *
+ * \var AgcAlgorithm::Session::minAnalogueGain
+ * \brief Minimum analogue gain supported with the configured sensor
+ *
+ * \var AgcAlgorithm::Session::maxAnalogueGain
+ * \brief Maximum analogue gain supported with the configured sensor
+ *
+ * \var AgcAlgorithm::Session::minFrameDuration
+ * \brief Minimum frame duration supported with the configured sensor
+ *
+ * \var AgcAlgorithm::Session::maxFrameDuration
+ * \brief Maximum frame duration supported with the configured sensor
+ *
+ * \var AgcAlgorithm::Session::lineDuration
+ * \brief Line duration with the configured sensor and output size
+ *
+ * \var AgcAlgorithm::Session::sensor
+ * \brief Details of the sensor configuration
+ *
+ * \var AgcAlgorithm::Session::sensor.outputSize
+ * \brief Configured output size of the sensor
+ *
+ * \var AgcAlgorithm::Session::autoAllowed
+ * \brief Whether automatic controls are allowed
+ */
+
+/**
+ * \struct AgcAlgorithm::ActiveState
+ * \brief Active state for AgcAlgorithm
+ *
+ * The \a automatic variables track the latest values computed by algorithm
+ * based on the latest processed statistics. All other variables track the
+ * consolidated controls requested in queued requests.
+ *
+ * \var AgcAlgorithm::ActiveState::manual
+ * \brief Manual exposure time and analog gain (set through requests)
+ *
+ * \var AgcAlgorithm::ActiveState::manual.exposure
+ * \brief Manual exposure time expressed as a number of lines as set by the
+ * ExposureTime control
+ *
+ * \var AgcAlgorithm::ActiveState::manual.gain
+ * \brief Manual analogue gain as set by the AnalogueGain control
+ *
+ * \var AgcAlgorithm::ActiveState::automatic
+ * \brief Automatic exposure time and analog gain (computed by the algorithm)
+ *
+ * \var AgcAlgorithm::ActiveState::automatic.exposure
+ * \brief Automatic exposure time expressed as a number of lines
+ *
+ * \var AgcAlgorithm::ActiveState::automatic.gain
+ * \brief Automatic analogue gain multiplier
+ *
+ * \var AgcAlgorithm::ActiveState::autoExposureEnabled
+ * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
+ *
+ * \var AgcAlgorithm::ActiveState::autoGainEnabled
+ * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
+ *
+ * \var AgcAlgorithm::ActiveState::minFrameDuration
+ * \brief Minimum frame duration as set by the FrameDurationLimits control
+ *
+ * \var AgcAlgorithm::ActiveState::maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
+ */
+
+/**
+ * \struct AgcAlgorithm::FrameContext
+ * \brief Per-frame context for AgcAlgorithm
+ *
+ * \var AgcAlgorithm::FrameContext::exposure
+ * \brief Exposure time expressed as a number of lines computed by the algorithm
+ *
+ * \var AgcAlgorithm::FrameContext::gain
+ * \brief Analogue gain multiplier computed by the algorithm
+ *
+ * The gain should be adapted to the sensor specific gain code before applying.
+ *
+ * \var AgcAlgorithm::FrameContext::vblank
+ * \brief Vertical blanking parameter computed by the algorithm
+ *
+ * \var AgcAlgorithm::FrameContext::autoExposureEnabled
+ * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
+ *
+ * \var AgcAlgorithm::FrameContext::autoGainEnabled
+ * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
+ *
+ * \var AgcAlgorithm::FrameContext::minFrameDuration
+ * \brief Minimum frame duration as set by the FrameDurationLimits control
+ *
+ * \var AgcAlgorithm::FrameContext::maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
+ *
+ * \var AgcAlgorithm::FrameContext::frameDuration
+ * \brief The actual FrameDuration used by the algorithm for the frame
+ *
+ * \var AgcAlgorithm::FrameContext::autoExposureModeChange
+ * \brief Indicate if autoExposureEnabled has changed from true in the previous
+ * frame to false in the current frame, and no manual exposure value has been
+ * supplied in the current frame.
+ *
+ * \var AgcAlgorithm::FrameContext::autoGainModeChange
+ * \brief Indicate if autoGainEnabled has changed from true in the previous
+ * frame to false in the current frame, and no manual gain value has been
+ * supplied in the current frame.
+ */
+
+/**
+ * \struct AgcAlgorithm::ConfigurationParams
+ * \brief Parameters for AgcAlgorithm::configure()
+ *
+ * \var AgcAlgorithm::ConfigurationParams::sensor
+ * \brief CameraSensorHelper for the sensor
+ *
+ * \var AgcAlgorithm::ConfigurationParams::sensorInfo
+ * \brief Details of the sensor
+ *
+ * \var AgcAlgorithm::ConfigurationParams::sensorControls
+ * \brief ControlInfoMap of the sensor
+ *
+ * \var AgcAlgorithm::ConfigurationParams::ctrlMap
+ * \brief ControlMap to update with controls
+ *
+ * \var AgcAlgorithm::ConfigurationParams::autoAllowed
+ * \brief Whether to enable auto controls
+ */
+
+/**
+ * \struct AgcAlgorithm::Limits
+ * \brief Limits of AGC parameters
+ *
+ * This structure contains the limits to consider for the actual
+ * AGC implementation.
+ *
+ * \var AgcAlgorithm::Limits::exposure
+ * \brief Limits of exposure time
+ *
+ * \var AgcAlgorithm::Limits::gain
+ * \brief Limits of analogue gain
+ */
+
+/**
+ * \brief Initialize the session configuration and active state
+ */
+int AgcAlgorithm::configure(Session &session, ActiveState &state, const ConfigurationParams &config)
+{
+	session = {};
+	session.lineDuration = config.sensorInfo.minLineLength * 1.0s
+		/ config.sensorInfo.pixelRate;
+	session.sensor.outputSize = config.sensorInfo.outputSize;
+	session.autoAllowed = config.autoAllowed;
+
+	const double lineDurationUs = session.lineDuration.get<std::micro>();
+
+	/*
+	 * Compute exposure time limits from the V4L2_CID_EXPOSURE control
+	 * limits and the line duration.
+	 */
+
+	const ControlInfo &v4l2Exposure = config.sensorControls.find(V4L2_CID_EXPOSURE)->second;
+	int32_t minExposure = v4l2Exposure.min().get<int32_t>();
+	int32_t maxExposure = v4l2Exposure.max().get<int32_t>();
+	int32_t defExposure = v4l2Exposure.def().get<int32_t>();
+	config.ctrlMap[&controls::ExposureTime] = ControlInfo{
+		static_cast<int32_t>(minExposure * lineDurationUs),
+		static_cast<int32_t>(maxExposure * lineDurationUs),
+		static_cast<int32_t>(defExposure * lineDurationUs),
+	};
+
+	/* Compute the analogue gain limits. */
+	const auto mapGain = [&](const ControlValue &v) {
+		auto code = v.get<int32_t>();
+		return config.sensor ? config.sensor->gain(code) : code;
+	};
+
+	const ControlInfo &v4l2Gain = config.sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;
+	float minGain = mapGain(v4l2Gain.min());
+	float maxGain = mapGain(v4l2Gain.max());
+	float defGain = mapGain(v4l2Gain.def());
+	config.ctrlMap[&controls::AnalogueGain] = ControlInfo{
+		minGain,
+		maxGain,
+		defGain,
+	};
+
+	LOG(Agc, Debug)
+		<< "Exposure: [" << minExposure << ", " << maxExposure
+		<< "], gain: [" << minGain << ", " << maxGain << "]";
+
+	/*
+	* Compute the frame duration limits.
+	*
+	* The frame length is computed assuming a fixed line length combined
+	* with the vertical frame sizes.
+	*/
+	const ControlInfo &v4l2HBlank = config.sensorControls.find(V4L2_CID_HBLANK)->second;
+	uint32_t hblank = v4l2HBlank.def().get<int32_t>();
+	uint32_t lineLength = config.sensorInfo.outputSize.width + hblank;
+
+	const ControlInfo &v4l2VBlank = config.sensorControls.find(V4L2_CID_VBLANK)->second;
+	std::array<uint32_t, 3> frameHeights{
+		v4l2VBlank.min().get<int32_t>() + config.sensorInfo.outputSize.height,
+		v4l2VBlank.max().get<int32_t>() + config.sensorInfo.outputSize.height,
+		v4l2VBlank.def().get<int32_t>() + config.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 / (config.sensorInfo.pixelRate / 1000000U);
+	}
+
+	config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
+		frameDurations[0],
+		frameDurations[1],
+		Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },
+	};
+
+	session.minFrameDuration = std::chrono::microseconds(frameDurations[0]);
+	session.maxFrameDuration = std::chrono::microseconds(frameDurations[1]);
+
+	/*
+	 * 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
+	 */
+	session.minExposureTime = minExposure * session.lineDuration;
+	session.maxExposureTime = maxExposure * session.lineDuration;
+	session.minAnalogueGain = minGain;
+	session.maxAnalogueGain = maxGain;
+
+	/* Configure the default exposure and gain. */
+	state = {};
+	state.automatic.gain = session.minAnalogueGain;
+	state.automatic.exposure = 10ms / session.lineDuration;
+	state.manual.gain = state.automatic.gain;
+	state.manual.exposure = state.automatic.exposure;
+	state.autoExposureEnabled = session.autoAllowed;
+	state.autoGainEnabled = session.autoAllowed;
+	state.minFrameDuration = session.minFrameDuration;
+	state.maxFrameDuration = session.maxFrameDuration;
+
+	const auto add = [&](const ControlId &cid, const auto &automatic, const auto &manual) {
+		std::array<ControlValue, 2> values;
+		size_t count = 0;
+
+		if (session.autoAllowed)
+			values[count++] = ControlValue(automatic);
+
+		values[count++] = ControlValue(manual);
+
+		config.ctrlMap[&cid] = ControlInfo{
+			{ values.data(), count },
+			ControlValue(session.autoAllowed ? automatic : manual),
+		};
+	};
+
+	add(controls::ExposureTimeMode, controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual);
+	add(controls::AnalogueGainMode, controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual);
+
+	/* \todo Move this to the `Camera` class. */
+	config.ctrlMap[&controls::AeEnable] = ControlInfo{
+		false,
+		session.autoAllowed,
+		session.autoAllowed,
+	};
+
+	return 0;
+}
+
+/**
+ * \brief Handle a \a queueRequest operation
+ */
+void AgcAlgorithm::queueRequest(const Session &session, ActiveState &state,
+				FrameContext &frameContext, const ControlList &controls)
+{
+	if (session.autoAllowed) {
+		const auto &aeEnable = controls.get(controls::ExposureTimeMode);
+		if (aeEnable &&
+		    (*aeEnable == controls::ExposureTimeModeAuto) != state.autoExposureEnabled) {
+			state.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto);
+
+			LOG(Agc, Debug)
+				<< (state.autoExposureEnabled ? "Enabling" : "Disabling")
+				<< " AGC (exposure)";
+
+			/*
+			 * If we go from auto -> manual with no manual control
+			 * set, use the last computed value, which we don't
+			 * know until prepare() so save this information.
+			 *
+			 * \todo Check the previous frame at prepare() time
+			 * instead of saving a flag here
+			 */
+			if (!state.autoExposureEnabled && !controls.get(controls::ExposureTime))
+				frameContext.autoExposureModeChange = true;
+		}
+
+		const auto &agEnable = controls.get(controls::AnalogueGainMode);
+		if (agEnable &&
+		    (*agEnable == controls::AnalogueGainModeAuto) != state.autoGainEnabled) {
+			state.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto);
+
+			LOG(Agc, Debug)
+				<< (state.autoGainEnabled ? "Enabling" : "Disabling")
+				<< " AGC (gain)";
+			/*
+			 * If we go from auto -> manual with no manual control
+			 * set, use the last computed value, which we don't
+			 * know until prepare() so save this information.
+			 */
+			if (!state.autoGainEnabled && !controls.get(controls::AnalogueGain))
+				frameContext.autoGainModeChange = true;
+		}
+	}
+
+	const auto &exposure = controls.get(controls::ExposureTime);
+	if (exposure && !state.autoExposureEnabled) {
+		state.manual.exposure = *exposure * 1.0us / session.lineDuration;
+
+		LOG(Agc, Debug)
+			<< "Set exposure to " << state.manual.exposure;
+	}
+
+	const auto &gain = controls.get(controls::AnalogueGain);
+	if (gain && !state.autoGainEnabled) {
+		state.manual.gain = *gain;
+
+		LOG(Agc, Debug) << "Set gain to " << state.manual.gain;
+	}
+
+	frameContext.autoExposureEnabled = state.autoExposureEnabled;
+	frameContext.autoGainEnabled = state.autoGainEnabled;
+
+	if (!frameContext.autoExposureEnabled)
+		frameContext.exposure = state.manual.exposure;
+	if (!frameContext.autoGainEnabled)
+		frameContext.gain = state.manual.gain;
+
+	const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
+	if (frameDurationLimits) {
+		/* Limit the control value to the limits in ControlInfo */
+		state.minFrameDuration = std::clamp<utils::Duration>(
+			std::chrono::microseconds((*frameDurationLimits).front()),
+			session.minFrameDuration,
+			session.maxFrameDuration
+		);
+
+		state.maxFrameDuration = std::clamp<utils::Duration>(
+			std::chrono::microseconds((*frameDurationLimits).back()),
+			session.minFrameDuration,
+			session.maxFrameDuration
+		);
+	}
+	frameContext.minFrameDuration = state.minFrameDuration;
+	frameContext.maxFrameDuration = state.maxFrameDuration;
+}
+
+/**
+ * \brief Handle a \a prepare operation
+ */
+void AgcAlgorithm::prepare(ActiveState &state, FrameContext &frameContext)
+{
+	uint32_t activeAutoExposure = state.automatic.exposure;
+	double activeAutoGain = state.automatic.gain;
+
+	/* Populate exposure and gain in auto mode */
+	if (frameContext.autoExposureEnabled)
+		frameContext.exposure = activeAutoExposure;
+	if (frameContext.autoGainEnabled)
+		frameContext.gain = activeAutoGain;
+
+	/*
+	 * Populate manual exposure and gain from the active auto values when
+	 * transitioning from auto to manual
+	 */
+	if (!frameContext.autoExposureEnabled && frameContext.autoExposureModeChange) {
+		state.manual.exposure = activeAutoExposure;
+		frameContext.exposure = activeAutoExposure;
+	}
+	if (!frameContext.autoGainEnabled && frameContext.autoGainModeChange) {
+		state.manual.gain = activeAutoGain;
+		frameContext.gain = activeAutoGain;
+	}
+}
+
+/**
+ * \brief Calculate the AGC limits for the given frame
+ */
+AgcAlgorithm::Limits AgcAlgorithm::calculateLimits(const Session &session, const FrameContext &frameContext)
+{
+	/*
+	 * Set the AGC limits using the fixed exposure time and/or gain in
+	 * manual mode, or the sensor limits in auto mode.
+	 */
+	Limits result;
+
+	if (frameContext.autoExposureEnabled) {
+		result.exposure = {
+			session.minExposureTime,
+			std::clamp(frameContext.maxFrameDuration, session.minExposureTime, session.maxExposureTime),
+		};
+	} else {
+		result.exposure.first = session.lineDuration * frameContext.exposure;
+		result.exposure.second = result.exposure.first;
+	}
+
+	if (frameContext.autoGainEnabled)
+		result.gain = { session.minAnalogueGain, session.maxAnalogueGain };
+	else
+		result.gain = { frameContext.gain, frameContext.gain };
+
+	return result;
+}
+
+/**
+ * \brief Handle a \a process operation
+ */
+void AgcAlgorithm::process(const Session& session, FrameContext& frameContext,
+			   utils::Duration newExposureTime, ControlList &metadata)
+{
+	/*
+	 * Expand the target frame duration so that we do not run faster than
+	 * the minimum frame duration when we have short exposures.
+	 */
+	const auto frameDuration = std::max(frameContext.minFrameDuration, newExposureTime);
+	frameContext.vblank = (frameDuration / session.lineDuration) - session.sensor.outputSize.height;
+
+	/* Update frame duration accounting for line length quantization. */
+	frameContext.frameDuration = (session.sensor.outputSize.height + frameContext.vblank) * session.lineDuration;
+
+	metadata.set(controls::AnalogueGain, frameContext.gain);
+	metadata.set(controls::ExposureTime, utils::Duration(session.lineDuration * frameContext.exposure).get<std::micro>());
+	metadata.set(controls::FrameDuration, frameContext.frameDuration.get<std::micro>());
+	metadata.set(controls::ExposureTimeMode,
+		     frameContext.autoExposureEnabled
+		     ? controls::ExposureTimeModeAuto
+		     : controls::ExposureTimeModeManual);
+	metadata.set(controls::AnalogueGainMode,
+		     frameContext.autoGainEnabled
+		     ? controls::AnalogueGainModeAuto
+		     : controls::AnalogueGainModeManual);
+}
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h
new file mode 100644
index 0000000000..35d321a0a1
--- /dev/null
+++ b/src/ipa/libipa/agc.h
@@ -0,0 +1,100 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ */
+
+#pragma once
+
+
+#include <libcamera/base/utils.h>
+
+#include <libcamera/controls.h>
+#include <libcamera/geometry.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "camera_sensor_helper.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+class AgcAlgorithm
+{
+public:
+	struct Session {
+		utils::Duration minExposureTime;
+		utils::Duration maxExposureTime;
+		double minAnalogueGain;
+		double maxAnalogueGain;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
+
+		utils::Duration lineDuration;
+
+		struct {
+			Size outputSize;
+		} sensor;
+
+		bool autoAllowed;
+	};
+
+	struct ActiveState {
+		struct {
+			uint32_t exposure;
+			double gain;
+		} manual;
+		struct {
+			uint32_t exposure;
+			double gain;
+		} automatic;
+
+		bool autoExposureEnabled;
+		bool autoGainEnabled;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
+	};
+
+	struct FrameContext {
+		uint32_t exposure;
+		double gain;
+		uint32_t vblank;
+		bool autoExposureEnabled;
+		bool autoGainEnabled;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
+		utils::Duration frameDuration;
+		bool autoExposureModeChange;
+		bool autoGainModeChange;
+	};
+
+	struct ConfigurationParams {
+		const CameraSensorHelper *sensor;
+		const IPACameraSensorInfo &sensorInfo;
+		const ControlInfoMap &sensorControls;
+		ControlInfoMap::Map &ctrlMap;
+		bool autoAllowed = true;
+	};
+
+protected:
+	int configure(Session &session, ActiveState &state, const ConfigurationParams &config);
+
+	void queueRequest(const Session &session, ActiveState &state,
+			  FrameContext &frameContext, const ControlList &controls);
+
+	void prepare(ActiveState &state, FrameContext &frameContext);
+
+	struct Limits {
+		std::pair<utils::Duration, utils::Duration> exposure;
+		std::pair<double, double> gain;
+	};
+
+	[[nodiscard]] Limits calculateLimits(const Session &session, const FrameContext &frameContext);
+
+	void process(const Session& session, FrameContext& frameContext,
+		     utils::Duration newExposureTime, ControlList &metadata);
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
index 951a4b0e02..0755c7ccac 100644
--- a/src/ipa/libipa/agc_mean_luminance.cpp
+++ b/src/ipa/libipa/agc_mean_luminance.cpp
@@ -8,11 +8,8 @@ 
 #include "agc_mean_luminance.h"
 
 #include <algorithm>
-#include <chrono>
 #include <cmath>
 
-#include <linux/v4l2-controls.h>
-
 #include <libcamera/base/log.h>
 #include <libcamera/base/utils.h>
 
@@ -748,80 +745,16 @@  AgcMeanLuminance::calculateNewEv(uint32_t constraintModeIndex,
  * \todo DigitalGain, DigitalGainMode
  */
 
-/**
- * \struct AgcMeanLuminanceAlgorithm::Session
- * \brief Session configuration for AgcMeanLuminanceAlgorithm
- *
- * \var AgcMeanLuminanceAlgorithm::Session::minExposureTime
- * \brief Minimum exposure time supported with the configured sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::maxExposureTime
- * \brief Maximum exposure time supported with the configured sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::minAnalogueGain
- * \brief Minimum analogue gain supported with the configured sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::maxAnalogueGain
- * \brief Maximum analogue gain supported with the configured sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::minFrameDuration
- * \brief Minimum frame duration supported with the configured sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::maxFrameDuration
- * \brief Maximum frame duration supported with the configured sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::lineDuration
- * \brief Line duration with the configured sensor and output size
- *
- * \var AgcMeanLuminanceAlgorithm::Session::sensor
- * \brief Details of the sensor configuration
- *
- * \var AgcMeanLuminanceAlgorithm::Session::sensor.outputSize
- * \brief Configured output size of the sensor
- *
- * \var AgcMeanLuminanceAlgorithm::Session::autoAllowed
- * \brief Whether automatic controls are allowed
- */
-
 /**
  * \struct AgcMeanLuminanceAlgorithm::ActiveState
  * \brief Active state for AgcMeanLuminanceAlgorithm
  *
- * The \a automatic variables track the latest values computed by algorithm
- * based on the latest processed statistics. All other variables track the
- * consolidated controls requested in queued requests.
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::manual
- * \brief Manual exposure time and analog gain (set through requests)
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::manual.exposure
- * \brief Manual exposure time expressed as a number of lines as set by the
- * ExposureTime control
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::manual.gain
- * \brief Manual analogue gain as set by the AnalogueGain control
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::automatic
- * \brief Automatic exposure time and analog gain (computed by the algorithm)
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::automatic.exposure
- * \brief Automatic exposure time expressed as a number of lines
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::automatic.gain
- * \brief Automatic analogue gain multiplier
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::automatic.quantizationGain
+ * \var AgcMeanLuminanceAlgorithm::ActiveState::quantizationGain
  * \brief Automatic quantization gain multiplier
  *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::automatic.yTarget
+ * \var AgcMeanLuminanceAlgorithm::ActiveState::yTarget
  * \brief Automatically determined luminance target
  *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::autoExposureEnabled
- * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::autoGainEnabled
- * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
- *
  * \var AgcMeanLuminanceAlgorithm::ActiveState::exposureValue
  * \brief Exposure value as set by the ExposureValue control
  *
@@ -830,26 +763,12 @@  AgcMeanLuminance::calculateNewEv(uint32_t constraintModeIndex,
  *
  * \var AgcMeanLuminanceAlgorithm::ActiveState::exposureMode
  * \brief Exposure mode as set by the AeExposureMode control
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::minFrameDuration
- * \brief Minimum frame duration as set by the FrameDurationLimits control
- *
- * \var AgcMeanLuminanceAlgorithm::ActiveState::maxFrameDuration
- * \brief Maximum frame duration as set by the FrameDurationLimits control
  */
 
 /**
  * \struct AgcMeanLuminanceAlgorithm::FrameContext
  * \brief Per-frame context for AgcMeanLuminanceAlgorithm
  *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::exposure
- * \brief Exposure time expressed as a number of lines computed by the algorithm
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::gain
- * \brief Analogue gain multiplier computed by the algorithm
- *
- * The gain should be adapted to the sensor specific gain code before applying.
- *
  * \var AgcMeanLuminanceAlgorithm::FrameContext::quantizationGain
  * \brief Quantization gain multiplier computed by the algorithm
  *
@@ -859,59 +778,11 @@  AgcMeanLuminance::calculateNewEv(uint32_t constraintModeIndex,
  * \var AgcMeanLuminanceAlgorithm::FrameContext::yTarget
  * \brief Luminance target computed by the algorithm
  *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::vblank
- * \brief Vertical blanking parameter computed by the algorithm
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::autoExposureEnabled
- * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::autoGainEnabled
- * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
- *
  * \var AgcMeanLuminanceAlgorithm::FrameContext::constraintMode
  * \brief Constraint mode as set by the AeConstraintMode control
  *
  * \var AgcMeanLuminanceAlgorithm::FrameContext::exposureMode
  * \brief Exposure mode as set by the AeExposureMode control
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::minFrameDuration
- * \brief Minimum frame duration as set by the FrameDurationLimits control
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::maxFrameDuration
- * \brief Maximum frame duration as set by the FrameDurationLimits control
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::frameDuration
- * \brief The actual FrameDuration used by the algorithm for the frame
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::autoExposureModeChange
- * \brief Indicate if autoExposureEnabled has changed from true in the previous
- * frame to false in the current frame, and no manual exposure value has been
- * supplied in the current frame.
- *
- * \var AgcMeanLuminanceAlgorithm::FrameContext::autoGainModeChange
- * \brief Indicate if autoGainEnabled has changed from true in the previous
- * frame to false in the current frame, and no manual gain value has been
- * supplied in the current frame.
- */
-
-/**
- * \struct AgcMeanLuminanceAlgorithm::ConfigurationParams
- * \brief Parameters for AgcMeanLuminanceAlgorithm::configure()
- *
- * \var AgcMeanLuminanceAlgorithm::ConfigurationParams::sensor
- * \brief CameraSensorHelper for the sensor
- *
- * \var AgcMeanLuminanceAlgorithm::ConfigurationParams::sensorInfo
- * \brief Details of the sensor
- *
- * \var AgcMeanLuminanceAlgorithm::ConfigurationParams::sensorControls
- * \brief ControlInfoMap of the sensor
- *
- * \var AgcMeanLuminanceAlgorithm::ConfigurationParams::ctrlMap
- * \brief ControlMap to update with controls
- *
- * \var AgcMeanLuminanceAlgorithm::ConfigurationParams::autoAllowed
- * \brief Whether to enable auto controls
  */
 
 /**
@@ -954,104 +825,18 @@  int AgcMeanLuminanceAlgorithm::init(const ValueNode &tuningData)
  */
 int AgcMeanLuminanceAlgorithm::configure(Session &session, ActiveState &state, const ConfigurationParams &config)
 {
-	session = {};
-	session.lineDuration = config.sensorInfo.minLineLength * 1.0s
-		/ config.sensorInfo.pixelRate;
-	session.sensor.outputSize = config.sensorInfo.outputSize;
-	session.autoAllowed = config.autoAllowed;
-
-	const double lineDurationUs = session.lineDuration.get<std::micro>();
-
-	/*
-	 * Compute exposure time limits from the V4L2_CID_EXPOSURE control
-	 * limits and the line duration.
-	 */
-
-	const ControlInfo &v4l2Exposure = config.sensorControls.find(V4L2_CID_EXPOSURE)->second;
-	int32_t minExposure = v4l2Exposure.min().get<int32_t>();
-	int32_t maxExposure = v4l2Exposure.max().get<int32_t>();
-	int32_t defExposure = v4l2Exposure.def().get<int32_t>();
-	config.ctrlMap[&controls::ExposureTime] = ControlInfo{
-		static_cast<int32_t>(minExposure * lineDurationUs),
-		static_cast<int32_t>(maxExposure * lineDurationUs),
-		static_cast<int32_t>(defExposure * lineDurationUs),
-	};
-
-	/* Compute the analogue gain limits. */
-	const ControlInfo &v4l2Gain = config.sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;
-	float minGain = config.sensor.gain(v4l2Gain.min().get<int32_t>());
-	float maxGain = config.sensor.gain(v4l2Gain.max().get<int32_t>());
-	float defGain = config.sensor.gain(v4l2Gain.def().get<int32_t>());
-	config.ctrlMap[&controls::AnalogueGain] = ControlInfo{
-		minGain,
-		maxGain,
-		defGain,
-	};
-
-	LOG(AgcMeanLuminance, Debug)
-		<< "Exposure: [" << minExposure << ", " << maxExposure
-		<< "], gain: [" << minGain << ", " << maxGain << "]";
-
-	/*
-	* Compute the frame duration limits.
-	*
-	* The frame length is computed assuming a fixed line length combined
-	* with the vertical frame sizes.
-	*/
-	const ControlInfo &v4l2HBlank = config.sensorControls.find(V4L2_CID_HBLANK)->second;
-	uint32_t hblank = v4l2HBlank.def().get<int32_t>();
-	uint32_t lineLength = config.sensorInfo.outputSize.width + hblank;
-
-	const ControlInfo &v4l2VBlank = config.sensorControls.find(V4L2_CID_VBLANK)->second;
-	std::array<uint32_t, 3> frameHeights{
-		v4l2VBlank.min().get<int32_t>() + config.sensorInfo.outputSize.height,
-		v4l2VBlank.max().get<int32_t>() + config.sensorInfo.outputSize.height,
-		v4l2VBlank.def().get<int32_t>() + config.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 / (config.sensorInfo.pixelRate / 1000000U);
-	}
-
-	config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
-		frameDurations[0],
-		frameDurations[1],
-		Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },
-	};
-
-	session.minFrameDuration = std::chrono::microseconds(frameDurations[0]);
-	session.maxFrameDuration = std::chrono::microseconds(frameDurations[1]);
-
-	/*
-	 * 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
-	 */
-	session.minExposureTime = minExposure * session.lineDuration;
-	session.maxExposureTime = maxExposure * session.lineDuration;
-	session.minAnalogueGain = minGain;
-	session.maxAnalogueGain = maxGain;
+	int ret = AgcAlgorithm::configure(session, state, config);
+	if (ret)
+		return ret;
 
-	impl_.configure(session.lineDuration, &config.sensor);
+	impl_.configure(session.lineDuration, config.sensor);
 	impl_.setLimits(session.minExposureTime, session.maxExposureTime,
 			session.minAnalogueGain, session.maxAnalogueGain,
 			{});
 	impl_.resetFrameCount();
 
-	/* Configure the default exposure and gain. */
-	state = {};
-	state.automatic.gain = session.minAnalogueGain;
-	state.automatic.exposure = 10ms / session.lineDuration;
-	state.automatic.quantizationGain = 1;
-	state.automatic.yTarget = impl_.effectiveYTarget();
-	state.manual.gain = state.automatic.gain;
-	state.manual.exposure = state.automatic.exposure;
-	state.autoExposureEnabled = session.autoAllowed;
-	state.autoGainEnabled = session.autoAllowed;
+	state.quantizationGain = 1;
+	state.yTarget = impl_.effectiveYTarget();
 	state.exposureValue = 0;
 
 	state.constraintMode =
@@ -1059,34 +844,6 @@  int AgcMeanLuminanceAlgorithm::configure(Session &session, ActiveState &state, c
 	state.exposureMode =
 		static_cast<controls::AeExposureModeEnum>(impl_.exposureModeHelpers().begin()->first);
 
-	state.minFrameDuration = session.minFrameDuration;
-	state.maxFrameDuration = session.maxFrameDuration;
-
-	const auto add = [&](const ControlId &cid, const auto &automatic, const auto &manual) {
-		std::array<ControlValue, 2> values;
-		size_t count = 0;
-
-		if (session.autoAllowed)
-			values[count++] = ControlValue(automatic);
-
-		values[count++] = ControlValue(manual);
-
-		config.ctrlMap[&cid] = ControlInfo{
-			{ values.data(), count },
-			ControlValue(session.autoAllowed ? automatic : manual),
-		};
-	};
-
-	add(controls::ExposureTimeMode, controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual);
-	add(controls::AnalogueGainMode, controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual);
-
-	/* \todo Move this to the `Camera` class. */
-	config.ctrlMap[&controls::AeEnable] = ControlInfo{
-		false,
-		session.autoAllowed,
-		session.autoAllowed,
-	};
-
 	// \todo Should these be added/removed based on `session.autoAllowed` ?
 	config.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);
 
@@ -1100,70 +857,9 @@  int AgcMeanLuminanceAlgorithm::configure(Session &session, ActiveState &state, c
  * \brief Handle a \a queueRequest operation
  */
 void AgcMeanLuminanceAlgorithm::queueRequest(const Session &session, ActiveState &state,
-				FrameContext &frameContext, const ControlList &controls)
+					     FrameContext &frameContext, const ControlList &controls)
 {
-	if (session.autoAllowed) {
-		const auto &aeEnable = controls.get(controls::ExposureTimeMode);
-		if (aeEnable &&
-		    (*aeEnable == controls::ExposureTimeModeAuto) != state.autoExposureEnabled) {
-			state.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto);
-
-			LOG(AgcMeanLuminance, Debug)
-				<< (state.autoExposureEnabled ? "Enabling" : "Disabling")
-				<< " AGC (exposure)";
-
-			/*
-			 * If we go from auto -> manual with no manual control
-			 * set, use the last computed value, which we don't
-			 * know until prepare() so save this information.
-			 *
-			 * \todo Check the previous frame at prepare() time
-			 * instead of saving a flag here
-			 */
-			if (!state.autoExposureEnabled && !controls.get(controls::ExposureTime))
-				frameContext.autoExposureModeChange = true;
-		}
-
-		const auto &agEnable = controls.get(controls::AnalogueGainMode);
-		if (agEnable &&
-		    (*agEnable == controls::AnalogueGainModeAuto) != state.autoGainEnabled) {
-			state.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto);
-
-			LOG(AgcMeanLuminance, Debug)
-				<< (state.autoGainEnabled ? "Enabling" : "Disabling")
-				<< " AGC (gain)";
-			/*
-			 * If we go from auto -> manual with no manual control
-			 * set, use the last computed value, which we don't
-			 * know until prepare() so save this information.
-			 */
-			if (!state.autoGainEnabled && !controls.get(controls::AnalogueGain))
-				frameContext.autoGainModeChange = true;
-		}
-	}
-
-	const auto &exposure = controls.get(controls::ExposureTime);
-	if (exposure && !state.autoExposureEnabled) {
-		state.manual.exposure = *exposure * 1.0us / session.lineDuration;
-
-		LOG(AgcMeanLuminance, Debug)
-			<< "Set exposure to " << state.manual.exposure;
-	}
-
-	const auto &gain = controls.get(controls::AnalogueGain);
-	if (gain && !state.autoGainEnabled) {
-		state.manual.gain = *gain;
-
-		LOG(AgcMeanLuminance, Debug) << "Set gain to " << state.manual.gain;
-	}
-
-	frameContext.autoExposureEnabled = state.autoExposureEnabled;
-	frameContext.autoGainEnabled = state.autoGainEnabled;
-
-	if (!frameContext.autoExposureEnabled)
-		frameContext.exposure = state.manual.exposure;
-	if (!frameContext.autoGainEnabled)
-		frameContext.gain = state.manual.gain;
+	AgcAlgorithm::queueRequest(session, state, frameContext, controls);
 
 	if (!frameContext.autoExposureEnabled &&
 	    !frameContext.autoGainEnabled)
@@ -1185,24 +881,6 @@  void AgcMeanLuminanceAlgorithm::queueRequest(const Session &session, ActiveState
 	if (exposureValue)
 		state.exposureValue = *exposureValue;
 	frameContext.exposureValue = state.exposureValue;
-
-	const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
-	if (frameDurationLimits) {
-		/* Limit the control value to the limits in ControlInfo */
-		state.minFrameDuration = std::clamp<utils::Duration>(
-			std::chrono::microseconds((*frameDurationLimits).front()),
-			session.minFrameDuration,
-			session.maxFrameDuration
-		);
-
-		state.maxFrameDuration = std::clamp<utils::Duration>(
-			std::chrono::microseconds((*frameDurationLimits).back()),
-			session.minFrameDuration,
-			session.maxFrameDuration
-		);
-	}
-	frameContext.minFrameDuration = state.minFrameDuration;
-	frameContext.maxFrameDuration = state.maxFrameDuration;
 }
 
 /**
@@ -1210,35 +888,17 @@  void AgcMeanLuminanceAlgorithm::queueRequest(const Session &session, ActiveState
  */
 void AgcMeanLuminanceAlgorithm::prepare(ActiveState &state, FrameContext &frameContext)
 {
-	uint32_t activeAutoExposure = state.automatic.exposure;
-	double activeAutoGain = state.automatic.gain;
-	double activeAutoQGain = state.automatic.quantizationGain;
+	AgcAlgorithm::prepare(state, frameContext);
 
-	/* Populate exposure and gain in auto mode */
-	if (frameContext.autoExposureEnabled) {
-		frameContext.exposure = activeAutoExposure;
-		frameContext.quantizationGain = activeAutoQGain;
-	}
-	if (frameContext.autoGainEnabled) {
-		frameContext.gain = activeAutoGain;
-		frameContext.quantizationGain = activeAutoQGain;
-	}
+	double activeAutoQGain = state.quantizationGain;
 
-	/*
-	 * Populate manual exposure and gain from the active auto values when
-	 * transitioning from auto to manual
-	 */
-	if (!frameContext.autoExposureEnabled && frameContext.autoExposureModeChange) {
-		state.manual.exposure = activeAutoExposure;
-		frameContext.exposure = activeAutoExposure;
-	}
-	if (!frameContext.autoGainEnabled && frameContext.autoGainModeChange) {
-		state.manual.gain = activeAutoGain;
-		frameContext.gain = activeAutoGain;
+	if (frameContext.autoExposureEnabled || frameContext.autoGainEnabled)
 		frameContext.quantizationGain = activeAutoQGain;
-	}
 
-	frameContext.yTarget = state.automatic.yTarget;
+	if (!frameContext.autoGainEnabled && frameContext.autoGainModeChange)
+		frameContext.quantizationGain = activeAutoQGain; // \todo WHAT?!
+
+	frameContext.yTarget = state.yTarget;
 }
 
 /**
@@ -1254,30 +914,7 @@  void AgcMeanLuminanceAlgorithm::process(const Session &session, ActiveState &sta
 	if (params) {
 		ASSERT(session.autoAllowed);
 
-		/*
-		* Set the AGC limits using the fixed exposure time and/or gain in
-		* manual mode, or the sensor limits in auto mode.
-		*/
-		utils::Duration minExposureTime;
-		utils::Duration maxExposureTime;
-		double minAnalogueGain;
-		double maxAnalogueGain;
-
-		if (frameContext.autoExposureEnabled) {
-			minExposureTime = session.minExposureTime;
-			maxExposureTime = std::clamp(frameContext.maxFrameDuration, session.minExposureTime, session.maxExposureTime);
-		} else {
-			minExposureTime = lineDuration * frameContext.exposure;
-			maxExposureTime = minExposureTime;
-		}
-
-		if (frameContext.autoGainEnabled) {
-			minAnalogueGain = session.minAnalogueGain;
-			maxAnalogueGain = session.maxAnalogueGain;
-		} else {
-			minAnalogueGain = frameContext.gain;
-			maxAnalogueGain = frameContext.gain;
-		}
+		auto limits = AgcAlgorithm::calculateLimits(session, frameContext);
 
 		/*
 		* The Agc algorithm needs to know the effective exposure value that was
@@ -1286,8 +923,8 @@  void AgcMeanLuminanceAlgorithm::process(const Session &session, ActiveState &sta
 		utils::Duration effectiveExposureValue =
 			lineDuration * params->exposure * params->gain;
 
-		impl_.setLimits(minExposureTime, maxExposureTime,
-				minAnalogueGain, maxAnalogueGain,
+		impl_.setLimits(limits.exposure.first, limits.exposure.second,
+				limits.gain.first, limits.gain.second,
 				std::move(params->additionalConstraints));
 
 		impl_.setExposureCompensation(pow(2.0, frameContext.exposureValue));
@@ -1306,31 +943,11 @@  void AgcMeanLuminanceAlgorithm::process(const Session &session, ActiveState &sta
 		/* Update the estimated exposure and gain. */
 		state.automatic.exposure = newExposureTime / lineDuration;
 		state.automatic.gain = aGain;
-		state.automatic.quantizationGain = qGain;
-		state.automatic.yTarget = impl_.effectiveYTarget();
+		state.quantizationGain = qGain;
+		state.yTarget = impl_.effectiveYTarget();
 	}
 
-	/*
-	 * Expand the target frame duration so that we do not run faster than
-	 * the minimum frame duration when we have short exposures.
-	 */
-	const auto frameDuration = std::max(frameContext.minFrameDuration, newExposureTime);
-	frameContext.vblank = (frameDuration / lineDuration) - session.sensor.outputSize.height;
-
-	/* Update frame duration accounting for line length quantization. */
-	frameContext.frameDuration = (session.sensor.outputSize.height + frameContext.vblank) * lineDuration;
-
-	metadata.set(controls::AnalogueGain, frameContext.gain);
-	metadata.set(controls::ExposureTime, utils::Duration(lineDuration * frameContext.exposure).get<std::micro>());
-	metadata.set(controls::FrameDuration, frameContext.frameDuration.get<std::micro>());
-	metadata.set(controls::ExposureTimeMode,
-		     frameContext.autoExposureEnabled
-		     ? controls::ExposureTimeModeAuto
-		     : controls::ExposureTimeModeManual);
-	metadata.set(controls::AnalogueGainMode,
-		     frameContext.autoGainEnabled
-		     ? controls::AnalogueGainModeAuto
-		     : controls::AnalogueGainModeManual);
+	AgcAlgorithm::process(session, frameContext, newExposureTime, metadata);
 
 	metadata.set(controls::AeExposureMode, frameContext.exposureMode);
 	metadata.set(controls::AeConstraintMode, frameContext.constraintMode);
diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
index e7e2d8ad0d..ceae33ba3e 100644
--- a/src/ipa/libipa/agc_mean_luminance.h
+++ b/src/ipa/libipa/agc_mean_luminance.h
@@ -22,6 +22,7 @@ 
 
 #include "libcamera/internal/value_node.h"
 
+#include "agc.h"
 #include "exposure_mode_helper.h"
 #include "histogram.h"
 #include "pwl.h"
@@ -119,71 +120,23 @@  private:
 	ControlInfoMap::Map controls_;
 };
 
-class AgcMeanLuminanceAlgorithm
+class AgcMeanLuminanceAlgorithm : public AgcAlgorithm
 {
 public:
-	struct Session {
-		utils::Duration minExposureTime;
-		utils::Duration maxExposureTime;
-		double minAnalogueGain;
-		double maxAnalogueGain;
-		utils::Duration minFrameDuration;
-		utils::Duration maxFrameDuration;
-
-		utils::Duration lineDuration;
-
-		struct {
-			Size outputSize;
-		} sensor;
-
-		bool autoAllowed;
-	};
-
-	struct ActiveState {
-		struct {
-			uint32_t exposure;
-			double gain;
-		} manual;
-		struct {
-			uint32_t exposure;
-			double gain;
-			double quantizationGain;
-			double yTarget;
-		} automatic;
-
-		bool autoExposureEnabled;
-		bool autoGainEnabled;
+	struct ActiveState : AgcAlgorithm::ActiveState {
+		double quantizationGain;
+		double yTarget;
 		double exposureValue;
 		controls::AeConstraintModeEnum constraintMode;
 		controls::AeExposureModeEnum exposureMode;
-		utils::Duration minFrameDuration;
-		utils::Duration maxFrameDuration;
 	};
 
-	struct FrameContext {
-		uint32_t exposure;
-		double gain;
+	struct FrameContext : AgcAlgorithm::FrameContext {
 		double quantizationGain;
-		double exposureValue;
 		double yTarget;
-		uint32_t vblank;
-		bool autoExposureEnabled;
-		bool autoGainEnabled;
+		double exposureValue;
 		controls::AeConstraintModeEnum constraintMode;
 		controls::AeExposureModeEnum exposureMode;
-		utils::Duration minFrameDuration;
-		utils::Duration maxFrameDuration;
-		utils::Duration frameDuration;
-		bool autoExposureModeChange;
-		bool autoGainModeChange;
-	};
-
-	struct ConfigurationParams {
-		const CameraSensorHelper &sensor;
-		const IPACameraSensorInfo &sensorInfo;
-		const ControlInfoMap &sensorControls;
-		ControlInfoMap::Map &ctrlMap;
-		bool autoAllowed = true;
 	};
 
 	int init(const ValueNode &tuningData);
diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
index 963c5ee730..47c027f707 100644
--- a/src/ipa/libipa/meson.build
+++ b/src/ipa/libipa/meson.build
@@ -2,6 +2,7 @@ 
 
 libipa_headers = files([
     'agc_mean_luminance.h',
+    'agc.h',
     'algorithm.h',
     'awb_bayes.h',
     'awb_grey.h',
@@ -23,6 +24,7 @@  libipa_headers = files([
 
 libipa_sources = files([
     'agc_mean_luminance.cpp',
+    'agc.cpp',
     'algorithm.cpp',
     'awb_bayes.cpp',
     'awb_grey.cpp',
diff --git a/src/ipa/mali-c55/algorithms/agc.cpp b/src/ipa/mali-c55/algorithms/agc.cpp
index 386541fd58..4c6c2b5acb 100644
--- a/src/ipa/mali-c55/algorithms/agc.cpp
+++ b/src/ipa/mali-c55/algorithms/agc.cpp
@@ -128,7 +128,7 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 		return ret;
 
 	ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
-		.sensor = *context.camHelper,
+		.sensor = context.camHelper.get(),
 		.sensorInfo = context.sensorInfo,
 		.sensorControls = context.sensorControls,
 		.ctrlMap = context.ctrlMap,
@@ -148,7 +148,7 @@  int Agc::configure(IPAContext &context,
 		return ret;
 
 	ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
-		.sensor = *context.camHelper,
+		.sensor = context.camHelper.get(),
 		.sensorInfo = context.sensorInfo,
 		.sensorControls = context.sensorControls,
 		.ctrlMap = context.ctrlMap,
diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
index 0d01ec1bda..ceef60c029 100644
--- a/src/ipa/rkisp1/algorithms/agc.cpp
+++ b/src/ipa/rkisp1/algorithms/agc.cpp
@@ -141,7 +141,7 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 		return ret;
 
 	ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
-		.sensor = *context.camHelper,
+		.sensor = context.camHelper.get(),
 		.sensorInfo = context.sensorInfo,
 		.sensorControls = context.sensorControls,
 		.ctrlMap = context.ctrlMap,
@@ -167,7 +167,7 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
 {
 	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
-		.sensor = *context.camHelper,
+		.sensor = context.camHelper.get(),
 		.sensorInfo = context.sensorInfo,
 		.sensorControls = context.sensorControls,
 		.ctrlMap = context.ctrlMap,
diff --git a/src/ipa/rkisp1/algorithms/wdr.cpp b/src/ipa/rkisp1/algorithms/wdr.cpp
index c3d73da2c5..b16aa7dd3b 100644
--- a/src/ipa/rkisp1/algorithms/wdr.cpp
+++ b/src/ipa/rkisp1/algorithms/wdr.cpp
@@ -464,7 +464,7 @@  void WideDynamicRange::process(IPAContext &context, [[maybe_unused]] const uint3
 
 	/* Calculate the gain needed to reach the requested yTarget. */
 	double value = cumHist.interQuantileMean(0, 1.0) / cumHist.bins();
-	double gain = context.activeState.agc.automatic.yTarget / value;
+	double gain = context.activeState.agc.yTarget / value;
 	gain = std::max(gain, 1.0);
 
 	double speed = 0.2;