[v7,21/47] ipa: libipa: Add `AgcAlgorithm`
diff mbox series

Message ID 20260821130218.2706196-22-barnabas.pocze@ideasonboard.com
State Superseded
Headers show
Series
  • ipa: libipa: agc rework
Related show

Commit Message

Barnabás Pőcze Aug. 21, 2026, 1:01 p.m. UTC
Add a class that implements the `Algorithm` interface using `AgcMeanLuminance`
based on the rkisp1 `Agc` algorithm, with the following main adjustments:

* the parameters for `process()` have been made optional to handle
  the cases where statistics are not available;
* the "raw" capture check has been replaced with the "autoAllowed"
  session parameter;
* the controls are only provided after `configure()`.

Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
---
 src/ipa/libipa/agc.cpp            | 673 +++++++++++++++++++++++++++++-
 src/ipa/libipa/agc.h              | 113 +++++
 src/ipa/rkisp1/algorithms/agc.cpp | 461 +++-----------------
 src/ipa/rkisp1/algorithms/agc.h   |  10 +-
 src/ipa/rkisp1/algorithms/lux.cpp |   2 +-
 src/ipa/rkisp1/ipa_context.cpp    |  98 -----
 src/ipa/rkisp1/ipa_context.h      |  47 +--
 7 files changed, 844 insertions(+), 560 deletions(-)

Comments

Jacopo Mondi Aug. 21, 2026, 3:16 p.m. UTC | #1
Hi Barnabás

On Fri, Aug 21, 2026 at 03:01:51PM +0200, Barnabás Pőcze wrote:
> Add a class that implements the `Algorithm` interface using `AgcMeanLuminance`
> based on the rkisp1 `Agc` algorithm, with the following main adjustments:
>
> * the parameters for `process()` have been made optional to handle
>   the cases where statistics are not available;
> * the "raw" capture check has been replaced with the "autoAllowed"
>   session parameter;
> * the controls are only provided after `configure()`.
>
> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>

Where did these tags appeared from ? :)

I hope I will have to just confirm mine during this review

> Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
> ---
>  src/ipa/libipa/agc.cpp            | 673 +++++++++++++++++++++++++++++-
>  src/ipa/libipa/agc.h              | 113 +++++
>  src/ipa/rkisp1/algorithms/agc.cpp | 461 +++-----------------
>  src/ipa/rkisp1/algorithms/agc.h   |  10 +-
>  src/ipa/rkisp1/algorithms/lux.cpp |   2 +-
>  src/ipa/rkisp1/ipa_context.cpp    |  98 -----
>  src/ipa/rkisp1/ipa_context.h      |  47 +--
>  7 files changed, 844 insertions(+), 560 deletions(-)
>
> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
> index 415a6d831f..a3d63b7c44 100644
> --- a/src/ipa/libipa/agc.cpp
> +++ b/src/ipa/libipa/agc.cpp
> @@ -1,16 +1,37 @@
>  /* SPDX-License-Identifier: LGPL-2.1-or-later */
>  /*
> - * Copyright (C) 2026 Ideas On Board
> + * Copyright (C) 2021-2026 Ideas On Board
>   *
>   * Auto exposure/gain algorithm for implementing the IPA-specific AGC algorithms
>   */
>
>  #include "agc.h"
>
> +#include <algorithm>
> +#include <array>
> +#include <chrono>
> +#include <cmath>
> +#include <optional>
> +#include <ratio>
> +
> +#include <linux/v4l2-controls.h>
> +
> +#include <libcamera/base/log.h>
> +#include <libcamera/base/span.h>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +
> +#include <libcamera/ipa/core_ipa_interface.h>
> +
>  namespace libcamera {
>
>  namespace ipa {
>
> +using namespace std::chrono_literals;
> +
> +LOG_DEFINE_CATEGORY(Agc)
> +
>  namespace agc {
>
>  /**
> @@ -40,6 +61,656 @@ namespace agc {
>
>  } /* namespace agc */
>
> +/**
> + * \class AgcAlgorithm
> + * \brief AgcMeanLuminance wrapper for implementing the Algorithm interface

I really would have hoped for a more detailed documentation of the
class and its function.

I did suggest to use LscAlgorithm as a reference for the adequate
level of detail for an algorithm in libipa, but I realize mine was an
unfair comparison. Explaining LSC is easier than explain Agc
and its several knobs. We should probably be collectively
spending time on documenting the algorithm, its expected tuning file
parameters and the basic notions that it operates on.

This documentation wasn't there to start with, and I think it's unfair
to ask you to write it from scratch, albeit this was a good occasion
to do so.

So, with a plan on how to document this pillar algorithm for future
IPA modules authors, you can keep my tag and go ahead with this
series.

> + *
> + * \todo DigitalGain, DigitalGainMode
> + */
> +
> +/**
> + * \struct agc::Session
> + * \brief Session configuration for AgcAlgorithm
> + *
> + * \var agc::Session::minExposureTime
> + * \brief Minimum exposure time for the streaming session
> + *
> + * \var agc::Session::maxExposureTime
> + * \brief Maximum exposure time for the streaming session
> + *
> + * \var agc::Session::minAnalogueGain
> + * \brief Minimum analogue gain for the streaming session
> + *
> + * \var agc::Session::maxAnalogueGain
> + * \brief Maximum analogue gain for the streaming session
> + *
> + * \var agc::Session::minFrameDuration
> + * \brief Minimum frame duration for the streaming session
> + *
> + * \var agc::Session::maxFrameDuration
> + * \brief Maximum frame duration for the streaming session
> + *
> + * \var agc::Session::lineDuration
> + * \brief Line duration for the streaming session
> + *
> + * \var agc::Session::sensor
> + * \brief Details of the sensor configuration
> + *
> + * \var agc::Session::sensor.outputSize
> + * \brief Configured output size of the sensor
> + *
> + * \var agc::Session::autoAllowed
> + * \copybrief AgcAlgorithm::ConfigurationParams::autoAllowed
> + * \sa AgcAlgorithm::ConfigurationParams::autoAllowed
> + */
> +
> +/**
> + * \struct agc::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 agc::ActiveState::manual
> + * \brief Manual exposure time and analog gain (set through requests)
> + *
> + * \var agc::ActiveState::manual.exposure
> + * \brief Manual exposure time expressed as a number of lines as set by the
> + * ExposureTime control
> + *
> + * \var agc::ActiveState::manual.gain
> + * \brief Manual analogue gain as set by the AnalogueGain control
> + *
> + * \var agc::ActiveState::automatic
> + * \brief Automatic exposure time and analog gain (computed by the algorithm)
> + *
> + * \var agc::ActiveState::automatic.exposure
> + * \brief Automatic exposure time expressed as a number of lines
> + *
> + * \var agc::ActiveState::automatic.gain
> + * \brief Automatic analogue gain multiplier
> + *
> + * \var agc::ActiveState::automatic.quantizationGain
> + * \brief Automatic quantization gain multiplier
> + *
> + * \var agc::ActiveState::automatic.yTarget
> + * \brief Automatically determined luminance target
> + *
> + * \var agc::ActiveState::autoExposureEnabled
> + * \brief Whether automatic exposure control is enabled by the ExposureTimeMode control
> + *
> + * \var agc::ActiveState::autoGainEnabled
> + * \brief Whether automatic gain control is enabled by the AnalogueGainMode control
> + *
> + * \var agc::ActiveState::exposureValue
> + * \brief Exposure value as set by the ExposureValue control
> + *
> + * \var agc::ActiveState::constraintMode
> + * \brief Constraint mode as set by the AeConstraintMode control
> + *
> + * \var agc::ActiveState::exposureMode
> + * \brief Exposure mode as set by the AeExposureMode control
> + *
> + * \var agc::ActiveState::minFrameDuration
> + * \brief Minimum frame duration as set by the FrameDurationLimits control
> + *
> + * \var agc::ActiveState::maxFrameDuration
> + * \brief Maximum frame duration as set by the FrameDurationLimits control
> + */
> +
> +/**
> + * \struct agc::FrameContext
> + * \brief Per-frame context for AgcAlgorithm
> + *
> + * \var agc::FrameContext::exposure
> + * \brief Exposure time expressed as a number of lines computed by the algorithm
> + *
> + * \var agc::FrameContext::gain
> + * \brief Analogue gain multiplier computed by the algorithm
> + *
> + * The gain should be translated to the sensor specific gain code before applying.
> + *
> + * \var agc::FrameContext::quantizationGain
> + * \brief Quantization gain multiplier computed by the algorithm
> + *
> + * \var agc::FrameContext::exposureValue
> + * \brief Exposure value as set by the ExposureValue control
> + *
> + * \var agc::FrameContext::yTarget
> + * \brief Luminance target computed by the algorithm
> + *
> + * \var agc::FrameContext::vblank
> + * \brief Vertical blanking parameter computed by the algorithm
> + *
> + * \var agc::FrameContext::autoExposureEnabled
> + * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
> + *
> + * \var agc::FrameContext::autoGainEnabled
> + * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
> + *
> + * \var agc::FrameContext::constraintMode
> + * \brief Constraint mode as set by the AeConstraintMode control
> + *
> + * \var agc::FrameContext::exposureMode
> + * \brief Exposure mode as set by the AeExposureMode control
> + *
> + * \var agc::FrameContext::minFrameDuration
> + * \brief Minimum frame duration as set by the FrameDurationLimits control
> + *
> + * \var agc::FrameContext::maxFrameDuration
> + * \brief Maximum frame duration as set by the FrameDurationLimits control
> + *
> + * \var agc::FrameContext::frameDuration
> + * \brief The actual FrameDuration used by the algorithm for the frame
> + *
> + * \var agc::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 agc::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 Current configuration of the sensor
> + *
> + * \var AgcAlgorithm::ConfigurationParams::sensorControls
> + * \brief ControlInfoMap of the sensor
> + *
> + * \var AgcAlgorithm::ConfigurationParams::ctrlMap
> + * \brief ControlInfoMap::Map to update with controls
> + *
> + * \var AgcAlgorithm::ConfigurationParams::autoAllowed
> + * \brief Whether to enable auto controls
> + *
> + * If \a false, the algorithm is set up for manual exposure and gain
> + * control only, without automatic adjustments. In this mode statistics
> + * must not be provided to AgcAlgorithm::process(), and ExposureTimeMode
> + * and AnalogueGainMode will only advertise manual control.
> + */
> +
> +/**
> + * \struct AgcAlgorithm::ProcessParams
> + * \brief Parameters for AgcAlgorithm::process()
> + *
> + * \var AgcAlgorithm::ProcessParams::traits
> + * \brief Implementation of AgcMeanLuminance::Traits
> + *
> + * \var AgcAlgorithm::ProcessParams::yHist
> + * \brief Luminance histogram of the frame
> + *
> + * \var AgcAlgorithm::ProcessParams::exposure
> + * \brief Effective exposure of the frame
> + *
> + * \var AgcAlgorithm::ProcessParams::gain
> + * \brief Effective gain of the frame
> + *
> + * \var AgcAlgorithm::ProcessParams::additionalConstraints
> + * \brief Additional AgcMeanLuminance::AgcConstraints to apply
> + *
> + * \var AgcAlgorithm::ProcessParams::lux
> + * \brief Effective lux value of the frame
> + */
> +
> +/**
> + * \brief Load tuning data and configure
> + * \param[in] tuningData The tuning data
> + * \param[in] session The agc session configuration
> + * \param[in] state The agc active state
> + * \param[in] config The algorithm configuration
> + *
> + * This function loads the tuning data and configures the algorithm as if
> + * by a call to configure().
> + */
> +int AgcAlgorithm::init(const ValueNode &tuningData,
> +		       agc::Session &session, agc::ActiveState &state,
> +		       const ConfigurationParams &config)
> +{
> +	int ret = impl_.parseTuningData(tuningData);
> +	if (ret)
> +		return ret;
> +
> +	return configure(session, state, config);
> +}
> +
> +/**
> + * \brief Initialize the session configuration and active state
> + * \param[in] session The agc session configuration
> + * \param[in] state The agc active state
> + * \param[in] config The algorithm configuration
> + *
> + * This function initializes \a session and \a state based on the tuning
> + * data loaded by init() and the configuration in \a config.
> + */
> +int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state,
> +			    const ConfigurationParams &config)
> +{
> +	session = {};
> +	session.autoAllowed = config.autoAllowed;
> +	session.lineDuration =
> +		config.sensorInfo.minLineLength * 1.0s / config.sensorInfo.pixelRate;
> +	session.sensor.outputSize = config.sensorInfo.outputSize;
> +
> +	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>();
> +
> +	/* 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>());
> +
> +	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);
> +	}
> +
> +	/*
> +	 * 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;
> +	session.minFrameDuration = std::chrono::microseconds(frameDurations[0]);
> +	session.maxFrameDuration = std::chrono::microseconds(frameDurations[1]);
> +
> +	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(0, 1);
> +	state.manual.gain = state.automatic.gain;
> +	state.manual.exposure = state.automatic.exposure;
> +	state.autoExposureEnabled = session.autoAllowed;
> +	state.autoGainEnabled = session.autoAllowed;
> +	state.exposureValue = 0;
> +	state.constraintMode =
> +		static_cast<controls::AeConstraintModeEnum>(impl_.constraintModes().begin()->first);
> +	state.exposureMode =
> +		static_cast<controls::AeExposureModeEnum>(impl_.exposureModeHelpers().begin()->first);
> +	state.minFrameDuration = session.minFrameDuration;
> +	state.maxFrameDuration = session.maxFrameDuration;
> +
> +	/* \todo Move this to the `Camera` class. */
> +	config.ctrlMap[&controls::AeEnable] = ControlInfo{
> +		false, session.autoAllowed, session.autoAllowed
> +	};
> +	config.ctrlMap[&controls::AnalogueGain] = ControlInfo{
> +		minGain, maxGain, defGain
> +	};
> +	config.ctrlMap[&controls::ExposureTime] = ControlInfo{
> +		static_cast<int32_t>(minExposure * lineDurationUs),
> +		static_cast<int32_t>(maxExposure * lineDurationUs),
> +		static_cast<int32_t>(defExposure * lineDurationUs),
> +	};
> +	config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
> +		frameDurations[0], frameDurations[1],
> +		Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },
> +	};
> +	config.ctrlMap[&controls::ExposureTimeMode] = ControlInfo{
> +		{{ controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual }},
> +		controls::ExposureTimeModeAuto,
> +	};
> +	config.ctrlMap[&controls::AnalogueGainMode] = ControlInfo{
> +		{{ controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual }},
> +		controls::AnalogueGainModeAuto,
> +	};
> +	config.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);
> +	config.ctrlMap.merge(impl_.controls());
> +
> +	return 0;
> +}
> +
> +/**
> + * \brief Queue a request
> + * \param[in] session The agc session configuration
> + * \param[in] state The agc active state
> + * \param[in] frameContext The agc frame context
> + * \param[in] controls The list of controls associated with a Request
> + *
> + * This functions processes the agc-related controls in \a controls for the frame
> + * denoted by \a frameContext, and updates \a state and \a frameContext accordingly.
> + */
> +void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &state,
> +				agc::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;
> +
> +	if (!frameContext.autoExposureEnabled && !frameContext.autoGainEnabled)
> +		frameContext.quantizationGain = 1.0;
> +
> +	const auto &exposureMode = controls.get(controls::AeExposureMode);
> +	if (exposureMode)
> +		state.exposureMode =
> +			static_cast<controls::AeExposureModeEnum>(*exposureMode);
> +	frameContext.exposureMode = state.exposureMode;
> +
> +	const auto &constraintMode = controls.get(controls::AeConstraintMode);
> +	if (constraintMode)
> +		state.constraintMode =
> +			static_cast<controls::AeConstraintModeEnum>(*constraintMode);
> +	frameContext.constraintMode = state.constraintMode;
> +
> +	const auto &exposureValue = controls.get(controls::ExposureValue);
> +	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;
> +}
> +
> +/**
> + * \brief Prepare a frame
> + * \param[in] state The agc active state
> + * \param[in] frameContext The agc frame context
> + *
> + * This function prepares the parameters for the frame denoted by \a frameContext.
> + * After a call to this function, the values of \ref agc::FrameContext::exposure
> + * "frameContext.exposure" and \ref agc::FrameContext::gain "frameContext.gain"
> + * will be finalized and may be used by the caller.
> + *
> + * \todo Finalize \ref agc::FrameContext::vblank "frameContext.vblank" as well
> + */
> +void AgcAlgorithm::prepare(agc::ActiveState &state, agc::FrameContext &frameContext)
> +{
> +	uint32_t activeAutoExposure = state.automatic.exposure;
> +	double activeAutoGain = state.automatic.gain;
> +	double activeAutoQGain = state.automatic.quantizationGain;
> +
> +	/* 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;
> +	}
> +
> +	/*
> +	 * 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;
> +		frameContext.quantizationGain = activeAutoQGain;
> +	}
> +
> +	frameContext.yTarget = state.automatic.yTarget;
> +}
> +
> +/**
> + * \brief Process frame statistics
> + * \param[in] session The agc session configuration
> + * \param[in] state The agc active state
> + * \param[in] frameContext The agc frame context
> + * \param[in] params The algorithm parameters
> + * \param[in] metadata The list of metadata
> + *
> + * Process the statistics for the completed frame denoted by \a frameContext
> + * and update \a state appropriately. This function also populates \a metadata.
> + */
> +void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state,
> +			   agc::FrameContext &frameContext, std::optional<ProcessParams> &&params,
> +			   ControlList &metadata)
> +{
> +	if (!params) {
> +		processFrameDuration(session, frameContext, frameContext.minFrameDuration);
> +		fillMetadata(session, frameContext, metadata);
> +		return;
> +	}
> +
> +	ASSERT(session.autoAllowed);
> +
> +	const utils::Duration &lineDuration = session.lineDuration;
> +
> +	/*
> +	 * 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;
> +
> +	/* \todo This uses the configuration from an already completed frame. */
> +
> +	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;
> +	}
> +
> +	/*
> +	 * The Agc algorithm needs to know the effective exposure value that was
> +	 * applied to the sensor when the statistics were collected.
> +	 */
> +	utils::Duration effectiveExposureValue =
> +		lineDuration * params->exposure * params->gain;
> +
> +	impl_.setLimits(minExposureTime, maxExposureTime,
> +			minAnalogueGain, maxAnalogueGain,
> +			std::move(params->additionalConstraints));
> +
> +	const auto &newEv = impl_.calculateNewEv({
> +		.traits = params->traits,
> +		.yHist = params->yHist,
> +		.effectiveExposureValue = effectiveExposureValue,
> +		.constraintModeIndex = frameContext.constraintMode,
> +		.exposureModeIndex = frameContext.exposureMode,
> +		.lux = params->lux,
> +		.exposureCompensation = std::pow(2.0, frameContext.exposureValue),
> +	});
> +
> +	/* Update the estimated exposure and gain. */
> +	state.automatic.exposure = newEv.exposureTime / lineDuration;
> +	state.automatic.gain = newEv.analogueGain;
> +	state.automatic.quantizationGain = newEv.quantizationGain;
> +	state.automatic.yTarget = newEv.yTarget;
> +
> +	LOG(Agc, Debug)
> +		<< "Divided up exposure time, analogue gain, quantization gain"
> +		<< " and digital gain are " << newEv.exposureTime
> +		<< ", " << state.automatic.gain << ", " << state.automatic.quantizationGain
> +		<< " and " << newEv.digitalGain;
> +
> +	/*
> +	 * Expand the target frame duration so that we do not run faster than
> +	 * the minimum frame duration when we have short exposures.
> +	 */
> +	processFrameDuration(session, frameContext,
> +			     std::max(frameContext.minFrameDuration, newEv.exposureTime));
> +
> +	fillMetadata(session, frameContext, metadata);
> +}
> +
> +/**
> + * \brief Process frame duration and compute vblank
> + * \param[in] session The session parameters
> + * \param[in] frameContext The current frame context
> + * \param[in] frameDuration The target frame duration
> + *
> + * Compute and populate vblank from the target frame duration.
> + */
> +void AgcAlgorithm::processFrameDuration(const agc::Session &session,
> +					agc::FrameContext &frameContext,
> +					utils::Duration frameDuration)
> +{
> +	const utils::Duration &lineDuration = session.lineDuration;
> +
> +	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;
> +}
> +
> +void AgcAlgorithm::fillMetadata(const agc::Session &session,
> +				const agc::FrameContext &frameContext,
> +				ControlList &metadata)
> +{
> +
> +	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);
> +
> +	metadata.set(controls::AeExposureMode, frameContext.exposureMode);
> +	metadata.set(controls::AeConstraintMode, frameContext.constraintMode);
> +	metadata.set(controls::ExposureValue, frameContext.exposureValue);
> +}
> +
>  } /* namespace ipa */
>
>  } /* namespace libcamera */
> diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h
> index 5a67464086..6795479f93 100644
> --- a/src/ipa/libipa/agc.h
> +++ b/src/ipa/libipa/agc.h
> @@ -7,21 +7,87 @@
>
>  #pragma once
>
> +#include <optional>
>  #include <stdint.h>
>  #include <utility>
> +#include <vector>
>
>  #include <linux/v4l2-controls.h>
>
> +#include <libcamera/base/utils.h>
> +
> +#include <libcamera/control_ids.h>
>  #include <libcamera/controls.h>
> +#include <libcamera/geometry.h>
>
> +#include "agc_mean_luminance.h"
>  #include "camera_sensor_helper.h"
>
>  namespace libcamera {
>
> +struct IPACameraSensorInfo;
> +
>  namespace ipa {
>
> +class Histogram;
> +
>  namespace agc {
>
> +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;
> +	double exposureValue;
> +	controls::AeConstraintModeEnum constraintMode;
> +	controls::AeExposureModeEnum exposureMode;
> +	utils::Duration minFrameDuration;
> +	utils::Duration maxFrameDuration;
> +};
> +
> +struct FrameContext {
> +	uint32_t exposure;
> +	double gain;
> +	double quantizationGain;
> +	double exposureValue;
> +	double yTarget;
> +	uint32_t vblank;
> +	bool autoExposureEnabled;
> +	bool autoGainEnabled;
> +	controls::AeConstraintModeEnum constraintMode;
> +	controls::AeExposureModeEnum exposureMode;
> +	utils::Duration minFrameDuration;
> +	utils::Duration maxFrameDuration;
> +	utils::Duration frameDuration;
> +	bool autoExposureModeChange;
> +	bool autoGainModeChange;
> +};
> +
>  [[nodiscard]]
>  inline std::pair<uint32_t, double>
>  extractControls(const ControlList &controls, const CameraSensorHelper *sensor)
> @@ -48,6 +114,53 @@ prepareControls(ControlList &controls, const CameraSensorHelper *sensor,
>
>  } /* namespace agc */
>
> +class AgcAlgorithm
> +{
> +public:
> +	struct ConfigurationParams {
> +		const CameraSensorHelper *sensor;
> +		const IPACameraSensorInfo &sensorInfo;
> +		const ControlInfoMap &sensorControls;
> +		ControlInfoMap::Map &ctrlMap;
> +		bool autoAllowed = true;
> +	};
> +
> +	struct ProcessParams {
> +		const AgcMeanLuminance::Traits &traits;
> +		const Histogram &yHist;
> +		uint32_t exposure;
> +		double gain;
> +		std::vector<AgcMeanLuminance::AgcConstraint> &&additionalConstraints = {};
> +		double lux = 0;
> +	};
> +
> +	int init(const ValueNode &tuningData,
> +		 agc::Session &session, agc::ActiveState &state,
> +		 const ConfigurationParams &config);
> +
> +	int configure(agc::Session &session, agc::ActiveState &state,
> +		      const ConfigurationParams &config);
> +
> +	void queueRequest(const agc::Session &session, agc::ActiveState &state,
> +			  agc::FrameContext &frameContext, const ControlList &controls);
> +
> +	void prepare(agc::ActiveState &state, agc::FrameContext &frameContext);
> +
> +	void process(const agc::Session &session, agc::ActiveState &state,
> +		     agc::FrameContext &frameContext, std::optional<ProcessParams> &&params,
> +		     ControlList &metadata);
> +
> +private:
> +	void processFrameDuration(const agc::Session &session,
> +				  agc::FrameContext &frameContext,
> +				  utils::Duration frameDuration);
> +	void fillMetadata(const agc::Session &session,
> +			  const agc::FrameContext &frameContext,
> +			  ControlList &metadata);
> +
> +	AgcMeanLuminance impl_;
> +};
> +
>  } /* namespace ipa */
>
>  } /* namespace libcamera */
> diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
> index 7230232d24..a83ee33b20 100644
> --- a/src/ipa/rkisp1/algorithms/agc.cpp
> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> @@ -8,10 +8,7 @@
>  #include "agc.h"
>
>  #include <algorithm>
> -#include <array>
> -#include <chrono>
>  #include <cmath>
> -#include <tuple>
>  #include <vector>
>
>  #include <libcamera/base/log.h>
> @@ -36,89 +33,6 @@ namespace ipa::rkisp1::algorithms {
>
>  LOG_DEFINE_CATEGORY(RkISP1Agc)
>
> -namespace {
> -
> -void reconfigure(IPAContext &context)
> -{
> -	context.configuration.sensor.lineDuration =
> -		context.sensorInfo.minLineLength * 1.0s / context.sensorInfo.pixelRate;
> -
> -	double lineDurationUs = context.configuration.sensor.lineDuration.get<std::micro>();
> -
> -	/*
> -	 * Compute exposure time limits from the V4L2_CID_EXPOSURE control
> -	 * limits and the line duration.
> -	 */
> -
> -	const ControlInfo &v4l2Exposure = context.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>();
> -	context.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 = context.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>());
> -	context.ctrlMap[&controls::AnalogueGain] = ControlInfo{
> -		minGain,
> -		maxGain,
> -		defGain,
> -	};
> -
> -	LOG(RkISP1Agc, 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 = context.sensorControls.find(V4L2_CID_HBLANK)->second;
> -	uint32_t hblank = v4l2HBlank.def().get<int32_t>();
> -	uint32_t lineLength = context.sensorInfo.outputSize.width + hblank;
> -
> -	const ControlInfo &v4l2VBlank = context.sensorControls.find(V4L2_CID_VBLANK)->second;
> -	std::array<uint32_t, 3> frameHeights{
> -		v4l2VBlank.min().get<int32_t>() + context.sensorInfo.outputSize.height,
> -		v4l2VBlank.max().get<int32_t>() + context.sensorInfo.outputSize.height,
> -		v4l2VBlank.def().get<int32_t>() + context.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 / (context.sensorInfo.pixelRate / 1000000U);
> -	}
> -
> -	context.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
> -		frameDurations[0],
> -		frameDurations[1],
> -		Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },
> -	};
> -
> -	/*
> -	 * 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 = minGain;
> -	context.configuration.sensor.maxAnalogueGain = maxGain;
> -}
> -
> -} /* namespace */
> -
>  /**
>   * \class Agc
>   * \brief A mean-based auto-exposure algorithm
> @@ -222,7 +136,12 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
>  {
>  	int ret;
>
> -	ret = agc_.parseTuningData(tuningData);
> +	ret = agc_.init(tuningData, context.configuration.agc, context.activeState.agc, {
> +		.sensor = context.camHelper.get(),
> +		.sensorInfo = context.sensorInfo,
> +		.sensorControls = context.sensorControls,
> +		.ctrlMap = context.ctrlMap,
> +	});
>  	if (ret)
>  		return ret;
>
> @@ -231,21 +150,6 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
>  	if (ret)
>  		return ret;
>
> -	context.ctrlMap[&controls::ExposureTimeMode] =
> -		ControlInfo({ { ControlValue(controls::ExposureTimeModeAuto),
> -				ControlValue(controls::ExposureTimeModeManual) } },
> -			    ControlValue(controls::ExposureTimeModeAuto));
> -	context.ctrlMap[&controls::AnalogueGainMode] =
> -		ControlInfo({ { ControlValue(controls::AnalogueGainModeAuto),
> -				ControlValue(controls::AnalogueGainModeManual) } },
> -			    ControlValue(controls::AnalogueGainModeAuto));
> -	/* \todo Move this to the Camera class */
> -	context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true, true);
> -	context.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);
> -	context.ctrlMap.merge(agc_.controls());
> -
> -	reconfigure(context);
> -
>  	return 0;
>  }
>
> @@ -258,47 +162,24 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
>   */
>  int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
>  {
> -	reconfigure(context);
> -
> -	/* Configure the default exposure and gain. */
> -	context.activeState.agc.automatic.gain = context.configuration.sensor.minAnalogueGain;
> -	context.activeState.agc.automatic.exposure =
> -		10ms / context.configuration.sensor.lineDuration;
> -	context.activeState.agc.automatic.quantizationGain = 1.0;
> -	context.activeState.agc.manual.gain = context.activeState.agc.automatic.gain;
> -	context.activeState.agc.manual.exposure = context.activeState.agc.automatic.exposure;
> -	context.activeState.agc.autoExposureEnabled = !context.configuration.raw;
> -	context.activeState.agc.autoGainEnabled = !context.configuration.raw;
> -	context.activeState.agc.exposureValue = 0.0;
> -
> -	context.activeState.agc.constraintMode =
> -		static_cast<controls::AeConstraintModeEnum>(agc_.constraintModes().begin()->first);
> -	context.activeState.agc.exposureMode =
> -		static_cast<controls::AeExposureModeEnum>(agc_.exposureModeHelpers().begin()->first);
> +	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
> +		.sensor = context.camHelper.get(),
> +		.sensorInfo = context.sensorInfo,
> +		.sensorControls = context.sensorControls,
> +		.ctrlMap = context.ctrlMap,
> +		.autoAllowed = !context.configuration.raw,
> +	});
> +	if (ret)
> +		return ret;
> +
>  	context.activeState.agc.meteringMode =
>  		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
>
> -	/* Limit the frame duration to match current initialisation */
> -	ControlInfo &frameDurationLimits = context.ctrlMap[&controls::FrameDurationLimits];
> -	context.activeState.agc.minFrameDuration = std::chrono::microseconds(frameDurationLimits.min().get<int64_t>());
> -	context.activeState.agc.maxFrameDuration = std::chrono::microseconds(frameDurationLimits.max().get<int64_t>());
> -
>  	context.configuration.agc.measureWindow.h_offs = 0;
>  	context.configuration.agc.measureWindow.v_offs = 0;
>  	context.configuration.agc.measureWindow.h_size = configInfo.outputSize.width;
>  	context.configuration.agc.measureWindow.v_size = configInfo.outputSize.height;
>
> -	agc_.configure(context.configuration.sensor.lineDuration, context.camHelper.get());
> -
> -	agc_.setLimits(context.configuration.sensor.minExposureTime,
> -		       context.configuration.sensor.maxExposureTime,
> -		       context.configuration.sensor.minAnalogueGain,
> -		       context.configuration.sensor.maxAnalogueGain, {});
> -
> -	context.activeState.agc.automatic.yTarget = agc_.effectiveYTarget(0, 1);
> -
> -	agc_.resetFrameCount();
> -
>  	return 0;
>  }
>
> @@ -312,73 +193,7 @@ void Agc::queueRequest(IPAContext &context,
>  {
>  	auto &agc = context.activeState.agc;
>
> -	if (!context.configuration.raw) {
> -		const auto &aeEnable = controls.get(controls::ExposureTimeMode);
> -		if (aeEnable &&
> -		    (*aeEnable == controls::ExposureTimeModeAuto) != agc.autoExposureEnabled) {
> -			agc.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto);
> -
> -			LOG(RkISP1Agc, Debug)
> -				<< (agc.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 (!agc.autoExposureEnabled && !controls.get(controls::ExposureTime))
> -				frameContext.agc.autoExposureModeChange = true;
> -		}
> -
> -		const auto &agEnable = controls.get(controls::AnalogueGainMode);
> -		if (agEnable &&
> -		    (*agEnable == controls::AnalogueGainModeAuto) != agc.autoGainEnabled) {
> -			agc.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto);
> -
> -			LOG(RkISP1Agc, Debug)
> -				<< (agc.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 (!agc.autoGainEnabled && !controls.get(controls::AnalogueGain))
> -				frameContext.agc.autoGainModeChange = true;
> -		}
> -	}
> -
> -	const auto &exposure = controls.get(controls::ExposureTime);
> -	if (exposure && !agc.autoExposureEnabled) {
> -		agc.manual.exposure = *exposure * 1.0us
> -				    / context.configuration.sensor.lineDuration;
> -
> -		LOG(RkISP1Agc, Debug)
> -			<< "Set exposure to " << agc.manual.exposure;
> -	}
> -
> -	const auto &gain = controls.get(controls::AnalogueGain);
> -	if (gain && !agc.autoGainEnabled) {
> -		agc.manual.gain = *gain;
> -
> -		LOG(RkISP1Agc, Debug) << "Set gain to " << agc.manual.gain;
> -	}
> -
> -	frameContext.agc.autoExposureEnabled = agc.autoExposureEnabled;
> -	frameContext.agc.autoGainEnabled = agc.autoGainEnabled;
> -
> -	if (!frameContext.agc.autoExposureEnabled)
> -		frameContext.agc.exposure = agc.manual.exposure;
> -	if (!frameContext.agc.autoGainEnabled)
> -		frameContext.agc.gain = agc.manual.gain;
> -
> -	if (!frameContext.agc.autoExposureEnabled &&
> -	    !frameContext.agc.autoGainEnabled)
> -		frameContext.agc.quantizationGain = 1.0;
> +	agc_.queueRequest(context.configuration.agc, agc, frameContext.agc, controls);
>
>  	const auto &meteringMode = controls.get(controls::AeMeteringMode);
>  	if (meteringMode) {
> @@ -387,42 +202,6 @@ void Agc::queueRequest(IPAContext &context,
>  			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
>  	}
>  	frameContext.agc.meteringMode = agc.meteringMode;
> -
> -	const auto &exposureMode = controls.get(controls::AeExposureMode);
> -	if (exposureMode)
> -		agc.exposureMode =
> -			static_cast<controls::AeExposureModeEnum>(*exposureMode);
> -	frameContext.agc.exposureMode = agc.exposureMode;
> -
> -	const auto &constraintMode = controls.get(controls::AeConstraintMode);
> -	if (constraintMode)
> -		agc.constraintMode =
> -			static_cast<controls::AeConstraintModeEnum>(*constraintMode);
> -	frameContext.agc.constraintMode = agc.constraintMode;
> -
> -	const auto &exposureValue = controls.get(controls::ExposureValue);
> -	if (exposureValue)
> -		agc.exposureValue = *exposureValue;
> -	frameContext.agc.exposureValue = agc.exposureValue;
> -
> -	const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
> -	if (frameDurationLimits) {
> -		/* Limit the control value to the limits in ControlInfo */
> -		ControlInfo &limits = context.ctrlMap[&controls::FrameDurationLimits];
> -		int64_t minFrameDuration =
> -			std::clamp((*frameDurationLimits).front(),
> -				   limits.min().get<int64_t>(),
> -				   limits.max().get<int64_t>());
> -		int64_t maxFrameDuration =
> -			std::clamp((*frameDurationLimits).back(),
> -				   limits.min().get<int64_t>(),
> -				   limits.max().get<int64_t>());
> -
> -		agc.minFrameDuration = std::chrono::microseconds(minFrameDuration);
> -		agc.maxFrameDuration = std::chrono::microseconds(maxFrameDuration);
> -	}
> -	frameContext.agc.minFrameDuration = agc.minFrameDuration;
> -	frameContext.agc.maxFrameDuration = agc.maxFrameDuration;
>  }
>
>  /**
> @@ -431,41 +210,13 @@ void Agc::queueRequest(IPAContext &context,
>  void Agc::prepare(IPAContext &context, const uint32_t frame,
>  		  IPAFrameContext &frameContext, RkISP1Params *params)
>  {
> -	uint32_t activeAutoExposure = context.activeState.agc.automatic.exposure;
> -	double activeAutoGain = context.activeState.agc.automatic.gain;
> -	double activeAutoQGain = context.activeState.agc.automatic.quantizationGain;
> -
> -	/* Populate exposure and gain in auto mode */
> -	if (frameContext.agc.autoExposureEnabled) {
> -		frameContext.agc.exposure = activeAutoExposure;
> -		frameContext.agc.quantizationGain = activeAutoQGain;
> -	}
> -	if (frameContext.agc.autoGainEnabled) {
> -		frameContext.agc.gain = activeAutoGain;
> -		frameContext.agc.quantizationGain = activeAutoQGain;
> -	}
> -
> -	/*
> -	 * Populate manual exposure and gain from the active auto values when
> -	 * transitioning from auto to manual
> -	 */
> -	if (!frameContext.agc.autoExposureEnabled && frameContext.agc.autoExposureModeChange) {
> -		context.activeState.agc.manual.exposure = activeAutoExposure;
> -		frameContext.agc.exposure = activeAutoExposure;
> -	}
> -	if (!frameContext.agc.autoGainEnabled && frameContext.agc.autoGainModeChange) {
> -		context.activeState.agc.manual.gain = activeAutoGain;
> -		frameContext.agc.gain = activeAutoGain;
> -		frameContext.agc.quantizationGain = activeAutoQGain;
> -	}
> +	agc_.prepare(context.activeState.agc, frameContext.agc);
>
>  	if (context.configuration.compress.supported) {
>  		frameContext.compress.enable = true;
>  		frameContext.compress.gain = frameContext.agc.quantizationGain;
>  	}
>
> -	frameContext.agc.yTarget = context.activeState.agc.automatic.yTarget;
> -
>  	if (frame > 0 && !frameContext.agc.updateMetering)
>  		return;
>
> @@ -521,50 +272,6 @@ void Agc::prepare(IPAContext &context, const uint32_t frame,
>  					   static_cast<rkisp1_cif_isp_histogram_mode>(hstConfig->mode));
>  }
>
> -void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
> -		       ControlList &metadata)
> -{
> -	utils::Duration exposureTime = context.configuration.sensor.lineDuration
> -				     * frameContext.sensor.exposure;
> -	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
> -	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
> -	metadata.set(controls::FrameDuration, frameContext.agc.frameDuration.get<std::micro>());
> -	metadata.set(controls::ExposureTimeMode,
> -		     frameContext.agc.autoExposureEnabled
> -		     ? controls::ExposureTimeModeAuto
> -		     : controls::ExposureTimeModeManual);
> -	metadata.set(controls::AnalogueGainMode,
> -		     frameContext.agc.autoGainEnabled
> -		     ? controls::AnalogueGainModeAuto
> -		     : controls::AnalogueGainModeManual);
> -
> -	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
> -	metadata.set(controls::AeExposureMode, frameContext.agc.exposureMode);
> -	metadata.set(controls::AeConstraintMode, frameContext.agc.constraintMode);
> -	metadata.set(controls::ExposureValue, frameContext.agc.exposureValue);
> -}
> -
> -/**
> - * \brief Process frame duration and compute vblank
> - * \param[in] context The shared IPA context
> - * \param[in] frameContext The current frame context
> - * \param[in] frameDuration The target frame duration
> - *
> - * Compute and populate vblank from the target frame duration.
> - */
> -void Agc::processFrameDuration(IPAContext &context,
> -			       IPAFrameContext &frameContext,
> -			       utils::Duration frameDuration)
> -{
> -	IPACameraSensorInfo &sensorInfo = context.sensorInfo;
> -	utils::Duration lineDuration = context.configuration.sensor.lineDuration;
> -
> -	frameContext.agc.vblank = (frameDuration / lineDuration) - sensorInfo.outputSize.height;
> -
> -	/* Update frame duration accounting for line length quantization. */
> -	frameContext.agc.frameDuration = (sensorInfo.outputSize.height + frameContext.agc.vblank) * lineDuration;
> -}
> -
>  namespace {
>
>  class AgcTraits final : public AgcMeanLuminance::Traits
> @@ -638,21 +345,6 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>  		  IPAFrameContext &frameContext, const rkisp1_stat_buffer *stats,
>  		  ControlList &metadata)
>  {
> -	if (!stats) {
> -		processFrameDuration(context, frameContext,
> -				     frameContext.agc.minFrameDuration);
> -		fillMetadata(context, frameContext, metadata);
> -		return;
> -	}
> -
> -	if (!(stats->meas_type & RKISP1_CIF_ISP_STAT_AUTOEXP)) {
> -		fillMetadata(context, frameContext, metadata);
> -		LOG(RkISP1Agc, Error) << "AUTOEXP data is missing in statistics";
> -		return;
> -	}
> -
> -	const utils::Duration &lineDuration = context.configuration.sensor.lineDuration;
> -
>  	/*
>  	 * \todo Verify that the exposure and gain applied by the sensor for
>  	 * this frame match what has been requested. This isn't a hard
> @@ -661,95 +353,46 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>  	 * we receive), but is important in manual mode.
>  	 */
>
> -	const rkisp1_cif_isp_stat *params = &stats->params;
> +	const rkisp1_cif_isp_stat *params = nullptr;
>
> -	/*
> -	 * 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.agc.autoExposureEnabled) {
> -		minExposureTime = context.configuration.sensor.minExposureTime;
> -		maxExposureTime = std::clamp(frameContext.agc.maxFrameDuration,
> -					     context.configuration.sensor.minExposureTime,
> -					     context.configuration.sensor.maxExposureTime);
> -	} else {
> -		minExposureTime = context.configuration.sensor.lineDuration
> -				* frameContext.agc.exposure;
> -		maxExposureTime = minExposureTime;
> +	if (stats) {
> +		if (stats->meas_type & RKISP1_CIF_ISP_STAT_AUTOEXP)
> +			params = &stats->params;
> +		else
> +			LOG(RkISP1Agc, Error) << "AUTOEXP data is missing in statistics";
>  	}
>
> -	if (frameContext.agc.autoGainEnabled) {
> -		minAnalogueGain = context.configuration.sensor.minAnalogueGain;
> -		maxAnalogueGain = context.configuration.sensor.maxAnalogueGain;
> +	if (params) {
> +		std::vector<AgcMeanLuminance::AgcConstraint> additionalConstraints;
> +		if (context.activeState.wdr.mode != controls::WdrOff)
> +			additionalConstraints.push_back(context.activeState.wdr.constraint);
> +
> +		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
> +			.traits = AgcTraits{
> +				{ params->ae.exp_mean, context.hw.numAeCells },
> +				meteringModes_.at(frameContext.agc.meteringMode),
> +			},
> +			.yHist = {
> +				/* The lower 4 bits are fractional and meant to be discarded. */
> +				{ params->hist.hist_bins, context.hw.numHistogramBins },
> +				[](uint32_t x) { return x >> 4; },
> +			},
> +			.exposure = frameContext.sensor.exposure,
> +			/*
> +			 * Include the quantization gain if it was applied. Do not use
> +			 * compress.gain because it will include gains that shall not be
> +			 * reported to the user when HDR is implemented.
> +			 */
> +			.gain = frameContext.sensor.gain
> +			        * (frameContext.compress.enable ? frameContext.agc.quantizationGain : 1),
> +			.additionalConstraints = std::move(additionalConstraints),
> +			.lux = frameContext.lux.lux,
> +		}}, metadata);
>  	} else {
> -		minAnalogueGain = frameContext.agc.gain;
> -		maxAnalogueGain = frameContext.agc.gain;
> +		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
>  	}
>
> -	std::vector<AgcMeanLuminance::AgcConstraint> additionalConstraints;
> -	if (context.activeState.wdr.mode != controls::WdrOff)
> -		additionalConstraints.push_back(context.activeState.wdr.constraint);
> -
> -	agc_.setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain,
> -		       std::move(additionalConstraints));
> -
> -	/*
> -	 * The Agc algorithm needs to know the effective exposure value that was
> -	 * applied to the sensor when the statistics were collected.
> -	 */
> -	utils::Duration exposureTime = lineDuration * frameContext.sensor.exposure;
> -	double analogueGain = frameContext.sensor.gain;
> -	utils::Duration effectiveExposureValue = exposureTime * analogueGain;
> -
> -	/*
> -	 * Include the quantization gain if it was applied. Do not use
> -	 * compress.gain because it will include gains that shall not be
> -	 * reported to the user when HDR is implemented.
> -	 */
> -	if (frameContext.compress.enable)
> -		effectiveExposureValue *= frameContext.agc.quantizationGain;
> -
> -	/* The lower 4 bits are fractional and meant to be discarded. */
> -	Histogram hist({ params->hist.hist_bins, context.hw.numHistogramBins },
> -		       [](uint32_t x) { return x >> 4; });
> -
> -	const auto &newEv = agc_.calculateNewEv({
> -		.traits = AgcTraits{
> -			{ params->ae.exp_mean, context.hw.numAeCells },
> -			meteringModes_.at(frameContext.agc.meteringMode),
> -		},
> -		.yHist = hist,
> -		.effectiveExposureValue = effectiveExposureValue,
> -		.constraintModeIndex = frameContext.agc.constraintMode,
> -		.exposureModeIndex = frameContext.agc.exposureMode,
> -		.lux = frameContext.lux.lux,
> -		.exposureCompensation = pow(2.0, frameContext.agc.exposureValue),
> -	});
> -
> -	LOG(RkISP1Agc, Debug)
> -		<< "Divided up exposure time, analogue gain, quantization gain"
> -		<< " and digital gain are " << newEv.exposureTime << ", " << newEv.analogueGain
> -		<< ", " << newEv.quantizationGain << " and " << newEv.digitalGain;
> -
> -	IPAActiveState &activeState = context.activeState;
> -	/* Update the estimated exposure and gain. */
> -	activeState.agc.automatic.exposure = newEv.exposureTime / lineDuration;
> -	activeState.agc.automatic.gain = newEv.analogueGain;
> -	activeState.agc.automatic.quantizationGain = newEv.quantizationGain;
> -	activeState.agc.automatic.yTarget = newEv.yTarget;
> -	/*
> -	 * Expand the target frame duration so that we do not run faster than
> -	 * the minimum frame duration when we have short exposures.
> -	 */
> -	processFrameDuration(context, frameContext,
> -			     std::max(frameContext.agc.minFrameDuration, newEv.exposureTime));
> -
> -	fillMetadata(context, frameContext, metadata);
> +	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
>  }
>
>  REGISTER_IPA_ALGORITHM(Agc, "Agc")
> diff --git a/src/ipa/rkisp1/algorithms/agc.h b/src/ipa/rkisp1/algorithms/agc.h
> index 0527ca0d5f..3a4d7bc546 100644
> --- a/src/ipa/rkisp1/algorithms/agc.h
> +++ b/src/ipa/rkisp1/algorithms/agc.h
> @@ -14,7 +14,7 @@
>
>  #include <libcamera/geometry.h>
>
> -#include "libipa/agc_mean_luminance.h"
> +#include "libipa/agc.h"
>
>  #include "algorithm.h"
>
> @@ -47,14 +47,8 @@ private:
>  	uint8_t computeHistogramPredivider(const Size &size,
>  					   enum rkisp1_cif_isp_histogram_mode mode);
>
> -	void fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
> -			  ControlList &metadata);
> -	void processFrameDuration(IPAContext &context,
> -				  IPAFrameContext &frameContext,
> -				  utils::Duration frameDuration);
> -
>  	std::map<int32_t, std::vector<uint8_t>> meteringModes_;
> -	AgcMeanLuminance agc_;
> +	AgcAlgorithm agc_;
>  };
>
>  } /* namespace ipa::rkisp1::algorithms */
> diff --git a/src/ipa/rkisp1/algorithms/lux.cpp b/src/ipa/rkisp1/algorithms/lux.cpp
> index 86e46c492f..ce6928a55d 100644
> --- a/src/ipa/rkisp1/algorithms/lux.cpp
> +++ b/src/ipa/rkisp1/algorithms/lux.cpp
> @@ -74,7 +74,7 @@ void Lux::process(IPAContext &context,
>  	if (!stats)
>  		return;
>
> -	utils::Duration exposureTime = context.configuration.sensor.lineDuration *
> +	utils::Duration exposureTime = context.configuration.agc.lineDuration *
>  				       frameContext.sensor.exposure;
>  	double gain = frameContext.sensor.gain;
>
> diff --git a/src/ipa/rkisp1/ipa_context.cpp b/src/ipa/rkisp1/ipa_context.cpp
> index 1f94afda6b..47691674ad 100644
> --- a/src/ipa/rkisp1/ipa_context.cpp
> +++ b/src/ipa/rkisp1/ipa_context.cpp
> @@ -86,21 +86,6 @@ namespace libcamera::ipa::rkisp1 {
>   * \var IPASessionConfiguration::sensor
>   * \brief Sensor-specific configuration of the IPA
>   *
> - * \var IPASessionConfiguration::sensor.minExposureTime
> - * \brief Minimum exposure time supported with the sensor
> - *
> - * \var IPASessionConfiguration::sensor.maxExposureTime
> - * \brief Maximum exposure time supported with the sensor
> - *
> - * \var IPASessionConfiguration::sensor.minAnalogueGain
> - * \brief Minimum analogue gain supported with the sensor
> - *
> - * \var IPASessionConfiguration::sensor.maxAnalogueGain
> - * \brief Maximum analogue gain supported with the sensor
> - *
> - * \var IPASessionConfiguration::sensor.lineDuration
> - * \brief Line duration in microseconds
> - *
>   * \var IPASessionConfiguration::sensor.size
>   * \brief Sensor output resolution
>   */
> @@ -147,49 +132,8 @@ namespace libcamera::ipa::rkisp1 {
>   * \var IPAActiveState::agc
>   * \brief State for the Automatic Gain Control algorithm
>   *
> - * 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.
> - *
> - * \struct IPAActiveState::agc.manual
> - * \brief Manual exposure time and analog gain (set through requests)
> - *
> - * \var IPAActiveState::agc.manual.exposure
> - * \brief Manual exposure time expressed as a number of lines as set by the
> - * ExposureTime control
> - *
> - * \var IPAActiveState::agc.manual.gain
> - * \brief Manual analogue gain as set by the AnalogueGain control
> - *
> - * \struct IPAActiveState::agc.automatic
> - * \brief Automatic exposure time and analog gain (computed by the algorithm)
> - *
> - * \var IPAActiveState::agc.automatic.exposure
> - * \brief Automatic exposure time expressed as a number of lines
> - *
> - * \var IPAActiveState::agc.automatic.gain
> - * \brief Automatic analogue gain multiplier
> - *
> - * \var IPAActiveState::agc.autoExposureEnabled
> - * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
> - *
> - * \var IPAActiveState::agc.autoGainEnabled
> - * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
> - *
> - * \var IPAActiveState::agc.constraintMode
> - * \brief Constraint mode as set by the AeConstraintMode control
> - *
> - * \var IPAActiveState::agc.exposureMode
> - * \brief Exposure mode as set by the AeExposureMode control
> - *
>   * \var IPAActiveState::agc.meteringMode
>   * \brief Metering mode as set by the AeMeteringMode control
> - *
> - * \var IPAActiveState::agc.minFrameDuration
> - * \brief Minimum frame duration as set by the FrameDurationLimits control
> - *
> - * \var IPAActiveState::agc.maxFrameDuration
> - * \brief Maximum frame duration as set by the FrameDurationLimits control
>   */
>
>  /**
> @@ -314,53 +258,11 @@ namespace libcamera::ipa::rkisp1 {
>   * the vertical blanking period is determined to maintain a consistent frame
>   * rate matched to the FrameDurationLimits as set by the user.
>   *
> - * \var IPAFrameContext::agc.exposure
> - * \brief Exposure time expressed as a number of lines computed by the algorithm
> - *
> - * \var IPAFrameContext::agc.gain
> - * \brief Analogue gain multiplier computed by the algorithm
> - *
> - * The gain should be adapted to the sensor specific gain code before applying.
> - *
> - * \var IPAFrameContext::agc.vblank
> - * \brief Vertical blanking parameter computed by the algorithm
> - *
> - * \var IPAFrameContext::agc.autoExposureEnabled
> - * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
> - *
> - * \var IPAFrameContext::agc.autoGainEnabled
> - * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
> - *
> - * \var IPAFrameContext::agc.constraintMode
> - * \brief Constraint mode as set by the AeConstraintMode control
> - *
> - * \var IPAFrameContext::agc.exposureMode
> - * \brief Exposure mode as set by the AeExposureMode control
> - *
>   * \var IPAFrameContext::agc.meteringMode
>   * \brief Metering mode as set by the AeMeteringMode control
>   *
> - * \var IPAFrameContext::agc.minFrameDuration
> - * \brief Minimum frame duration as set by the FrameDurationLimits control
> - *
> - * \var IPAFrameContext::agc.maxFrameDuration
> - * \brief Maximum frame duration as set by the FrameDurationLimits control
> - *
> - * \var IPAFrameContext::agc.frameDuration
> - * \brief The actual FrameDuration used by the algorithm for the frame
> - *
>   * \var IPAFrameContext::agc.updateMetering
>   * \brief Indicate if new ISP AGC metering parameters need to be applied
> - *
> - * \var IPAFrameContext::agc.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 IPAFrameContext::agc.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.
>   */
>
>  /**
> diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> index cd213dd991..cc07bb9462 100644
> --- a/src/ipa/rkisp1/ipa_context.h
> +++ b/src/ipa/rkisp1/ipa_context.h
> @@ -24,7 +24,7 @@
>  #include "libcamera/internal/matrix.h"
>  #include "libcamera/internal/vector.h"
>
> -#include "libipa/agc_mean_luminance.h"
> +#include "libipa/agc.h"
>  #include "libipa/awb.h"
>  #include "libipa/camera_sensor_helper.h"
>  #include "libipa/ccm.h"
> @@ -57,7 +57,7 @@ struct RKISP1AwbSession {
>  };
>
>  struct IPASessionConfiguration {
> -	struct {
> +	struct Agc : agc::Session {
>  		struct rkisp1_cif_isp_window measureWindow;
>  	} agc;
>
> @@ -68,12 +68,6 @@ struct IPASessionConfiguration {
>  	} compress;
>
>  	struct {
> -		utils::Duration minExposureTime;
> -		utils::Duration maxExposureTime;
> -		double minAnalogueGain;
> -		double maxAnalogueGain;
> -
> -		utils::Duration lineDuration;
>  		Size size;
>  	} sensor;
>
> @@ -82,26 +76,8 @@ struct IPASessionConfiguration {
>  };
>
>  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;
> +	struct Agc : agc::ActiveState {
>  		controls::AeMeteringModeEnum meteringMode;
> -		utils::Duration minFrameDuration;
> -		utils::Duration maxFrameDuration;
>  	} agc;
>
>  	ipa::awb::ActiveState awb;
> @@ -145,24 +121,9 @@ struct IPAActiveState {
>  };
>
>  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;
> +	struct Agc : agc::FrameContext {
>  		controls::AeMeteringModeEnum meteringMode;
> -		utils::Duration minFrameDuration;
> -		utils::Duration maxFrameDuration;
> -		utils::Duration frameDuration;
>  		bool updateMetering;
> -		bool autoExposureModeChange;
> -		bool autoGainModeChange;
>  	} agc;
>
>  	ipa::awb::FrameContext awb;
> --
> 2.55.0
>
Barnabás Pőcze Aug. 21, 2026, 3:23 p.m. UTC | #2
2026. 08. 21. 17:16 keltezéssel, Jacopo Mondi írta:
> Hi Barnabás
> 
> On Fri, Aug 21, 2026 at 03:01:51PM +0200, Barnabás Pőcze wrote:
>> Add a class that implements the `Algorithm` interface using `AgcMeanLuminance`
>> based on the rkisp1 `Agc` algorithm, with the following main adjustments:
>>
>> * the parameters for `process()` have been made optional to handle
>>    the cases where statistics are not available;
>> * the "raw" capture check has been replaced with the "autoAllowed"
>>    session parameter;
>> * the controls are only provided after `configure()`.
>>
>> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
>> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
> 
> Where did these tags appeared from ? :)
> 
> I hope I will have to just confirm mine during this review
> 

Oops, no idea, probably from v5.



>> Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
>> ---
>>   src/ipa/libipa/agc.cpp            | 673 +++++++++++++++++++++++++++++-
>>   src/ipa/libipa/agc.h              | 113 +++++
>>   src/ipa/rkisp1/algorithms/agc.cpp | 461 +++-----------------
>>   src/ipa/rkisp1/algorithms/agc.h   |  10 +-
>>   src/ipa/rkisp1/algorithms/lux.cpp |   2 +-
>>   src/ipa/rkisp1/ipa_context.cpp    |  98 -----
>>   src/ipa/rkisp1/ipa_context.h      |  47 +--
>>   7 files changed, 844 insertions(+), 560 deletions(-)
>>
>> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
>> index 415a6d831f..a3d63b7c44 100644
>> --- a/src/ipa/libipa/agc.cpp
>> +++ b/src/ipa/libipa/agc.cpp
>> @@ -1,16 +1,37 @@
>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>   /*
>> - * Copyright (C) 2026 Ideas On Board
>> + * Copyright (C) 2021-2026 Ideas On Board
>>    *
>>    * Auto exposure/gain algorithm for implementing the IPA-specific AGC algorithms
>>    */
>>
>>   #include "agc.h"
>>
>> +#include <algorithm>
>> +#include <array>
>> +#include <chrono>
>> +#include <cmath>
>> +#include <optional>
>> +#include <ratio>
>> +
>> +#include <linux/v4l2-controls.h>
>> +
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/span.h>
>> +
>> +#include <libcamera/control_ids.h>
>> +#include <libcamera/controls.h>
>> +
>> +#include <libcamera/ipa/core_ipa_interface.h>
>> +
>>   namespace libcamera {
>>
>>   namespace ipa {
>>
>> +using namespace std::chrono_literals;
>> +
>> +LOG_DEFINE_CATEGORY(Agc)
>> +
>>   namespace agc {
>>
>>   /**
>> @@ -40,6 +61,656 @@ namespace agc {
>>
>>   } /* namespace agc */
>>
>> +/**
>> + * \class AgcAlgorithm
>> + * \brief AgcMeanLuminance wrapper for implementing the Algorithm interface
> 
> I really would have hoped for a more detailed documentation of the
> class and its function.

Oops, sorry, for some reason I thought you meant only the member functions.


> 
> I did suggest to use LscAlgorithm as a reference for the adequate
> level of detail for an algorithm in libipa, but I realize mine was an
> unfair comparison. Explaining LSC is easier than explain Agc
> and its several knobs. We should probably be collectively
> spending time on documenting the algorithm, its expected tuning file
> parameters and the basic notions that it operates on.
> 
> This documentation wasn't there to start with, and I think it's unfair
> to ask you to write it from scratch, albeit this was a good occasion
> to do so.
> 
> So, with a plan on how to document this pillar algorithm for future
> IPA modules authors, you can keep my tag and go ahead with this
> series.
> [...]
Barnabás Pőcze Aug. 21, 2026, 9:01 p.m. UTC | #3
2026. 08. 21. 17:23 keltezéssel, Barnabás Pőcze írta:
> 2026. 08. 21. 17:16 keltezéssel, Jacopo Mondi írta:
>> Hi Barnabás
>>
>> On Fri, Aug 21, 2026 at 03:01:51PM +0200, Barnabás Pőcze wrote:
>>> Add a class that implements the `Algorithm` interface using `AgcMeanLuminance`
>>> based on the rkisp1 `Agc` algorithm, with the following main adjustments:
>>>
>>> * the parameters for `process()` have been made optional to handle
>>>    the cases where statistics are not available;
>>> * the "raw" capture check has been replaced with the "autoAllowed"
>>>    session parameter;
>>> * the controls are only provided after `configure()`.
>>>
>>> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
>>> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>>> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
>>
>> Where did these tags appeared from ? :)
>>
>> I hope I will have to just confirm mine during this review
>>
> 
> Oops, no idea, probably from v5.
> 
> 
> 
>>> Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
>>> ---
>>>   src/ipa/libipa/agc.cpp            | 673 +++++++++++++++++++++++++++++-
>>>   src/ipa/libipa/agc.h              | 113 +++++
>>>   src/ipa/rkisp1/algorithms/agc.cpp | 461 +++-----------------
>>>   src/ipa/rkisp1/algorithms/agc.h   |  10 +-
>>>   src/ipa/rkisp1/algorithms/lux.cpp |   2 +-
>>>   src/ipa/rkisp1/ipa_context.cpp    |  98 -----
>>>   src/ipa/rkisp1/ipa_context.h      |  47 +--
>>>   7 files changed, 844 insertions(+), 560 deletions(-)
>>>
>>> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
>>> index 415a6d831f..a3d63b7c44 100644
>>> --- a/src/ipa/libipa/agc.cpp
>>> +++ b/src/ipa/libipa/agc.cpp
>>> @@ -1,16 +1,37 @@
>>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>   /*
>>> - * Copyright (C) 2026 Ideas On Board
>>> + * Copyright (C) 2021-2026 Ideas On Board
>>>    *
>>>    * Auto exposure/gain algorithm for implementing the IPA-specific AGC algorithms
>>>    */
>>>
>>>   #include "agc.h"
>>>
>>> +#include <algorithm>
>>> +#include <array>
>>> +#include <chrono>
>>> +#include <cmath>
>>> +#include <optional>
>>> +#include <ratio>
>>> +
>>> +#include <linux/v4l2-controls.h>
>>> +
>>> +#include <libcamera/base/log.h>
>>> +#include <libcamera/base/span.h>
>>> +
>>> +#include <libcamera/control_ids.h>
>>> +#include <libcamera/controls.h>
>>> +
>>> +#include <libcamera/ipa/core_ipa_interface.h>
>>> +
>>>   namespace libcamera {
>>>
>>>   namespace ipa {
>>>
>>> +using namespace std::chrono_literals;
>>> +
>>> +LOG_DEFINE_CATEGORY(Agc)
>>> +
>>>   namespace agc {
>>>
>>>   /**
>>> @@ -40,6 +61,656 @@ namespace agc {
>>>
>>>   } /* namespace agc */
>>>
>>> +/**
>>> + * \class AgcAlgorithm
>>> + * \brief AgcMeanLuminance wrapper for implementing the Algorithm interface
>>
>> I really would have hoped for a more detailed documentation of the
>> class and its function.
> 
> Oops, sorry, for some reason I thought you meant only the member functions.
> 
> 
>>
>> I did suggest to use LscAlgorithm as a reference for the adequate
>> level of detail for an algorithm in libipa, but I realize mine was an
>> unfair comparison. Explaining LSC is easier than explain Agc
>> and its several knobs. We should probably be collectively
>> spending time on documenting the algorithm, its expected tuning file
>> parameters and the basic notions that it operates on.
>>
>> This documentation wasn't there to start with, and I think it's unfair
>> to ask you to write it from scratch, albeit this was a good occasion
>> to do so.
>>
>> So, with a plan on how to document this pillar algorithm for future
>> IPA modules authors, you can keep my tag and go ahead with this
>> series.
>> [...]

I started expanding it even more, but I feel like I spent too much time
on it because I seem to have a hard time coming up with what's actually
worth putting into writing (everything wrt. "how to use it" seems evident,
and easily understandable from the examples). And `AgcMeanLuminance` is already
documented, and arguably there isn't actually much in `AgcAlgorithm`. So I'm
a bit lost, I looked at `lsc.cpp`, but it's still not entirely clear what
level of detail you were expecting, and how much focus should be given
to the implementation details vs. "how to use it".

Patch
diff mbox series

diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
index 415a6d831f..a3d63b7c44 100644
--- a/src/ipa/libipa/agc.cpp
+++ b/src/ipa/libipa/agc.cpp
@@ -1,16 +1,37 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
- * Copyright (C) 2026 Ideas On Board
+ * Copyright (C) 2021-2026 Ideas On Board
  *
  * Auto exposure/gain algorithm for implementing the IPA-specific AGC algorithms
  */
 
 #include "agc.h"
 
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <cmath>
+#include <optional>
+#include <ratio>
+
+#include <linux/v4l2-controls.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
 namespace libcamera {
 
 namespace ipa {
 
+using namespace std::chrono_literals;
+
+LOG_DEFINE_CATEGORY(Agc)
+
 namespace agc {
 
 /**
@@ -40,6 +61,656 @@  namespace agc {
 
 } /* namespace agc */
 
+/**
+ * \class AgcAlgorithm
+ * \brief AgcMeanLuminance wrapper for implementing the Algorithm interface
+ *
+ * \todo DigitalGain, DigitalGainMode
+ */
+
+/**
+ * \struct agc::Session
+ * \brief Session configuration for AgcAlgorithm
+ *
+ * \var agc::Session::minExposureTime
+ * \brief Minimum exposure time for the streaming session
+ *
+ * \var agc::Session::maxExposureTime
+ * \brief Maximum exposure time for the streaming session
+ *
+ * \var agc::Session::minAnalogueGain
+ * \brief Minimum analogue gain for the streaming session
+ *
+ * \var agc::Session::maxAnalogueGain
+ * \brief Maximum analogue gain for the streaming session
+ *
+ * \var agc::Session::minFrameDuration
+ * \brief Minimum frame duration for the streaming session
+ *
+ * \var agc::Session::maxFrameDuration
+ * \brief Maximum frame duration for the streaming session
+ *
+ * \var agc::Session::lineDuration
+ * \brief Line duration for the streaming session
+ *
+ * \var agc::Session::sensor
+ * \brief Details of the sensor configuration
+ *
+ * \var agc::Session::sensor.outputSize
+ * \brief Configured output size of the sensor
+ *
+ * \var agc::Session::autoAllowed
+ * \copybrief AgcAlgorithm::ConfigurationParams::autoAllowed
+ * \sa AgcAlgorithm::ConfigurationParams::autoAllowed
+ */
+
+/**
+ * \struct agc::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 agc::ActiveState::manual
+ * \brief Manual exposure time and analog gain (set through requests)
+ *
+ * \var agc::ActiveState::manual.exposure
+ * \brief Manual exposure time expressed as a number of lines as set by the
+ * ExposureTime control
+ *
+ * \var agc::ActiveState::manual.gain
+ * \brief Manual analogue gain as set by the AnalogueGain control
+ *
+ * \var agc::ActiveState::automatic
+ * \brief Automatic exposure time and analog gain (computed by the algorithm)
+ *
+ * \var agc::ActiveState::automatic.exposure
+ * \brief Automatic exposure time expressed as a number of lines
+ *
+ * \var agc::ActiveState::automatic.gain
+ * \brief Automatic analogue gain multiplier
+ *
+ * \var agc::ActiveState::automatic.quantizationGain
+ * \brief Automatic quantization gain multiplier
+ *
+ * \var agc::ActiveState::automatic.yTarget
+ * \brief Automatically determined luminance target
+ *
+ * \var agc::ActiveState::autoExposureEnabled
+ * \brief Whether automatic exposure control is enabled by the ExposureTimeMode control
+ *
+ * \var agc::ActiveState::autoGainEnabled
+ * \brief Whether automatic gain control is enabled by the AnalogueGainMode control
+ *
+ * \var agc::ActiveState::exposureValue
+ * \brief Exposure value as set by the ExposureValue control
+ *
+ * \var agc::ActiveState::constraintMode
+ * \brief Constraint mode as set by the AeConstraintMode control
+ *
+ * \var agc::ActiveState::exposureMode
+ * \brief Exposure mode as set by the AeExposureMode control
+ *
+ * \var agc::ActiveState::minFrameDuration
+ * \brief Minimum frame duration as set by the FrameDurationLimits control
+ *
+ * \var agc::ActiveState::maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
+ */
+
+/**
+ * \struct agc::FrameContext
+ * \brief Per-frame context for AgcAlgorithm
+ *
+ * \var agc::FrameContext::exposure
+ * \brief Exposure time expressed as a number of lines computed by the algorithm
+ *
+ * \var agc::FrameContext::gain
+ * \brief Analogue gain multiplier computed by the algorithm
+ *
+ * The gain should be translated to the sensor specific gain code before applying.
+ *
+ * \var agc::FrameContext::quantizationGain
+ * \brief Quantization gain multiplier computed by the algorithm
+ *
+ * \var agc::FrameContext::exposureValue
+ * \brief Exposure value as set by the ExposureValue control
+ *
+ * \var agc::FrameContext::yTarget
+ * \brief Luminance target computed by the algorithm
+ *
+ * \var agc::FrameContext::vblank
+ * \brief Vertical blanking parameter computed by the algorithm
+ *
+ * \var agc::FrameContext::autoExposureEnabled
+ * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
+ *
+ * \var agc::FrameContext::autoGainEnabled
+ * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
+ *
+ * \var agc::FrameContext::constraintMode
+ * \brief Constraint mode as set by the AeConstraintMode control
+ *
+ * \var agc::FrameContext::exposureMode
+ * \brief Exposure mode as set by the AeExposureMode control
+ *
+ * \var agc::FrameContext::minFrameDuration
+ * \brief Minimum frame duration as set by the FrameDurationLimits control
+ *
+ * \var agc::FrameContext::maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
+ *
+ * \var agc::FrameContext::frameDuration
+ * \brief The actual FrameDuration used by the algorithm for the frame
+ *
+ * \var agc::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 agc::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 Current configuration of the sensor
+ *
+ * \var AgcAlgorithm::ConfigurationParams::sensorControls
+ * \brief ControlInfoMap of the sensor
+ *
+ * \var AgcAlgorithm::ConfigurationParams::ctrlMap
+ * \brief ControlInfoMap::Map to update with controls
+ *
+ * \var AgcAlgorithm::ConfigurationParams::autoAllowed
+ * \brief Whether to enable auto controls
+ *
+ * If \a false, the algorithm is set up for manual exposure and gain
+ * control only, without automatic adjustments. In this mode statistics
+ * must not be provided to AgcAlgorithm::process(), and ExposureTimeMode
+ * and AnalogueGainMode will only advertise manual control.
+ */
+
+/**
+ * \struct AgcAlgorithm::ProcessParams
+ * \brief Parameters for AgcAlgorithm::process()
+ *
+ * \var AgcAlgorithm::ProcessParams::traits
+ * \brief Implementation of AgcMeanLuminance::Traits
+ *
+ * \var AgcAlgorithm::ProcessParams::yHist
+ * \brief Luminance histogram of the frame
+ *
+ * \var AgcAlgorithm::ProcessParams::exposure
+ * \brief Effective exposure of the frame
+ *
+ * \var AgcAlgorithm::ProcessParams::gain
+ * \brief Effective gain of the frame
+ *
+ * \var AgcAlgorithm::ProcessParams::additionalConstraints
+ * \brief Additional AgcMeanLuminance::AgcConstraints to apply
+ *
+ * \var AgcAlgorithm::ProcessParams::lux
+ * \brief Effective lux value of the frame
+ */
+
+/**
+ * \brief Load tuning data and configure
+ * \param[in] tuningData The tuning data
+ * \param[in] session The agc session configuration
+ * \param[in] state The agc active state
+ * \param[in] config The algorithm configuration
+ *
+ * This function loads the tuning data and configures the algorithm as if
+ * by a call to configure().
+ */
+int AgcAlgorithm::init(const ValueNode &tuningData,
+		       agc::Session &session, agc::ActiveState &state,
+		       const ConfigurationParams &config)
+{
+	int ret = impl_.parseTuningData(tuningData);
+	if (ret)
+		return ret;
+
+	return configure(session, state, config);
+}
+
+/**
+ * \brief Initialize the session configuration and active state
+ * \param[in] session The agc session configuration
+ * \param[in] state The agc active state
+ * \param[in] config The algorithm configuration
+ *
+ * This function initializes \a session and \a state based on the tuning
+ * data loaded by init() and the configuration in \a config.
+ */
+int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state,
+			    const ConfigurationParams &config)
+{
+	session = {};
+	session.autoAllowed = config.autoAllowed;
+	session.lineDuration =
+		config.sensorInfo.minLineLength * 1.0s / config.sensorInfo.pixelRate;
+	session.sensor.outputSize = config.sensorInfo.outputSize;
+
+	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>();
+
+	/* 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>());
+
+	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);
+	}
+
+	/*
+	 * 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;
+	session.minFrameDuration = std::chrono::microseconds(frameDurations[0]);
+	session.maxFrameDuration = std::chrono::microseconds(frameDurations[1]);
+
+	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(0, 1);
+	state.manual.gain = state.automatic.gain;
+	state.manual.exposure = state.automatic.exposure;
+	state.autoExposureEnabled = session.autoAllowed;
+	state.autoGainEnabled = session.autoAllowed;
+	state.exposureValue = 0;
+	state.constraintMode =
+		static_cast<controls::AeConstraintModeEnum>(impl_.constraintModes().begin()->first);
+	state.exposureMode =
+		static_cast<controls::AeExposureModeEnum>(impl_.exposureModeHelpers().begin()->first);
+	state.minFrameDuration = session.minFrameDuration;
+	state.maxFrameDuration = session.maxFrameDuration;
+
+	/* \todo Move this to the `Camera` class. */
+	config.ctrlMap[&controls::AeEnable] = ControlInfo{
+		false, session.autoAllowed, session.autoAllowed
+	};
+	config.ctrlMap[&controls::AnalogueGain] = ControlInfo{
+		minGain, maxGain, defGain
+	};
+	config.ctrlMap[&controls::ExposureTime] = ControlInfo{
+		static_cast<int32_t>(minExposure * lineDurationUs),
+		static_cast<int32_t>(maxExposure * lineDurationUs),
+		static_cast<int32_t>(defExposure * lineDurationUs),
+	};
+	config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
+		frameDurations[0], frameDurations[1],
+		Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },
+	};
+	config.ctrlMap[&controls::ExposureTimeMode] = ControlInfo{
+		{{ controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual }},
+		controls::ExposureTimeModeAuto,
+	};
+	config.ctrlMap[&controls::AnalogueGainMode] = ControlInfo{
+		{{ controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual }},
+		controls::AnalogueGainModeAuto,
+	};
+	config.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);
+	config.ctrlMap.merge(impl_.controls());
+
+	return 0;
+}
+
+/**
+ * \brief Queue a request
+ * \param[in] session The agc session configuration
+ * \param[in] state The agc active state
+ * \param[in] frameContext The agc frame context
+ * \param[in] controls The list of controls associated with a Request
+ *
+ * This functions processes the agc-related controls in \a controls for the frame
+ * denoted by \a frameContext, and updates \a state and \a frameContext accordingly.
+ */
+void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &state,
+				agc::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;
+
+	if (!frameContext.autoExposureEnabled && !frameContext.autoGainEnabled)
+		frameContext.quantizationGain = 1.0;
+
+	const auto &exposureMode = controls.get(controls::AeExposureMode);
+	if (exposureMode)
+		state.exposureMode =
+			static_cast<controls::AeExposureModeEnum>(*exposureMode);
+	frameContext.exposureMode = state.exposureMode;
+
+	const auto &constraintMode = controls.get(controls::AeConstraintMode);
+	if (constraintMode)
+		state.constraintMode =
+			static_cast<controls::AeConstraintModeEnum>(*constraintMode);
+	frameContext.constraintMode = state.constraintMode;
+
+	const auto &exposureValue = controls.get(controls::ExposureValue);
+	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;
+}
+
+/**
+ * \brief Prepare a frame
+ * \param[in] state The agc active state
+ * \param[in] frameContext The agc frame context
+ *
+ * This function prepares the parameters for the frame denoted by \a frameContext.
+ * After a call to this function, the values of \ref agc::FrameContext::exposure
+ * "frameContext.exposure" and \ref agc::FrameContext::gain "frameContext.gain"
+ * will be finalized and may be used by the caller.
+ *
+ * \todo Finalize \ref agc::FrameContext::vblank "frameContext.vblank" as well
+ */
+void AgcAlgorithm::prepare(agc::ActiveState &state, agc::FrameContext &frameContext)
+{
+	uint32_t activeAutoExposure = state.automatic.exposure;
+	double activeAutoGain = state.automatic.gain;
+	double activeAutoQGain = state.automatic.quantizationGain;
+
+	/* 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;
+	}
+
+	/*
+	 * 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;
+		frameContext.quantizationGain = activeAutoQGain;
+	}
+
+	frameContext.yTarget = state.automatic.yTarget;
+}
+
+/**
+ * \brief Process frame statistics
+ * \param[in] session The agc session configuration
+ * \param[in] state The agc active state
+ * \param[in] frameContext The agc frame context
+ * \param[in] params The algorithm parameters
+ * \param[in] metadata The list of metadata
+ *
+ * Process the statistics for the completed frame denoted by \a frameContext
+ * and update \a state appropriately. This function also populates \a metadata.
+ */
+void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state,
+			   agc::FrameContext &frameContext, std::optional<ProcessParams> &&params,
+			   ControlList &metadata)
+{
+	if (!params) {
+		processFrameDuration(session, frameContext, frameContext.minFrameDuration);
+		fillMetadata(session, frameContext, metadata);
+		return;
+	}
+
+	ASSERT(session.autoAllowed);
+
+	const utils::Duration &lineDuration = session.lineDuration;
+
+	/*
+	 * 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;
+
+	/* \todo This uses the configuration from an already completed frame. */
+
+	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;
+	}
+
+	/*
+	 * The Agc algorithm needs to know the effective exposure value that was
+	 * applied to the sensor when the statistics were collected.
+	 */
+	utils::Duration effectiveExposureValue =
+		lineDuration * params->exposure * params->gain;
+
+	impl_.setLimits(minExposureTime, maxExposureTime,
+			minAnalogueGain, maxAnalogueGain,
+			std::move(params->additionalConstraints));
+
+	const auto &newEv = impl_.calculateNewEv({
+		.traits = params->traits,
+		.yHist = params->yHist,
+		.effectiveExposureValue = effectiveExposureValue,
+		.constraintModeIndex = frameContext.constraintMode,
+		.exposureModeIndex = frameContext.exposureMode,
+		.lux = params->lux,
+		.exposureCompensation = std::pow(2.0, frameContext.exposureValue),
+	});
+
+	/* Update the estimated exposure and gain. */
+	state.automatic.exposure = newEv.exposureTime / lineDuration;
+	state.automatic.gain = newEv.analogueGain;
+	state.automatic.quantizationGain = newEv.quantizationGain;
+	state.automatic.yTarget = newEv.yTarget;
+
+	LOG(Agc, Debug)
+		<< "Divided up exposure time, analogue gain, quantization gain"
+		<< " and digital gain are " << newEv.exposureTime
+		<< ", " << state.automatic.gain << ", " << state.automatic.quantizationGain
+		<< " and " << newEv.digitalGain;
+
+	/*
+	 * Expand the target frame duration so that we do not run faster than
+	 * the minimum frame duration when we have short exposures.
+	 */
+	processFrameDuration(session, frameContext,
+			     std::max(frameContext.minFrameDuration, newEv.exposureTime));
+
+	fillMetadata(session, frameContext, metadata);
+}
+
+/**
+ * \brief Process frame duration and compute vblank
+ * \param[in] session The session parameters
+ * \param[in] frameContext The current frame context
+ * \param[in] frameDuration The target frame duration
+ *
+ * Compute and populate vblank from the target frame duration.
+ */
+void AgcAlgorithm::processFrameDuration(const agc::Session &session,
+					agc::FrameContext &frameContext,
+					utils::Duration frameDuration)
+{
+	const utils::Duration &lineDuration = session.lineDuration;
+
+	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;
+}
+
+void AgcAlgorithm::fillMetadata(const agc::Session &session,
+				const agc::FrameContext &frameContext,
+				ControlList &metadata)
+{
+
+	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);
+
+	metadata.set(controls::AeExposureMode, frameContext.exposureMode);
+	metadata.set(controls::AeConstraintMode, frameContext.constraintMode);
+	metadata.set(controls::ExposureValue, frameContext.exposureValue);
+}
+
 } /* namespace ipa */
 
 } /* namespace libcamera */
diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h
index 5a67464086..6795479f93 100644
--- a/src/ipa/libipa/agc.h
+++ b/src/ipa/libipa/agc.h
@@ -7,21 +7,87 @@ 
 
 #pragma once
 
+#include <optional>
 #include <stdint.h>
 #include <utility>
+#include <vector>
 
 #include <linux/v4l2-controls.h>
 
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
+#include <libcamera/geometry.h>
 
+#include "agc_mean_luminance.h"
 #include "camera_sensor_helper.h"
 
 namespace libcamera {
 
+struct IPACameraSensorInfo;
+
 namespace ipa {
 
+class Histogram;
+
 namespace agc {
 
+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;
+	double exposureValue;
+	controls::AeConstraintModeEnum constraintMode;
+	controls::AeExposureModeEnum exposureMode;
+	utils::Duration minFrameDuration;
+	utils::Duration maxFrameDuration;
+};
+
+struct FrameContext {
+	uint32_t exposure;
+	double gain;
+	double quantizationGain;
+	double exposureValue;
+	double yTarget;
+	uint32_t vblank;
+	bool autoExposureEnabled;
+	bool autoGainEnabled;
+	controls::AeConstraintModeEnum constraintMode;
+	controls::AeExposureModeEnum exposureMode;
+	utils::Duration minFrameDuration;
+	utils::Duration maxFrameDuration;
+	utils::Duration frameDuration;
+	bool autoExposureModeChange;
+	bool autoGainModeChange;
+};
+
 [[nodiscard]]
 inline std::pair<uint32_t, double>
 extractControls(const ControlList &controls, const CameraSensorHelper *sensor)
@@ -48,6 +114,53 @@  prepareControls(ControlList &controls, const CameraSensorHelper *sensor,
 
 } /* namespace agc */
 
+class AgcAlgorithm
+{
+public:
+	struct ConfigurationParams {
+		const CameraSensorHelper *sensor;
+		const IPACameraSensorInfo &sensorInfo;
+		const ControlInfoMap &sensorControls;
+		ControlInfoMap::Map &ctrlMap;
+		bool autoAllowed = true;
+	};
+
+	struct ProcessParams {
+		const AgcMeanLuminance::Traits &traits;
+		const Histogram &yHist;
+		uint32_t exposure;
+		double gain;
+		std::vector<AgcMeanLuminance::AgcConstraint> &&additionalConstraints = {};
+		double lux = 0;
+	};
+
+	int init(const ValueNode &tuningData,
+		 agc::Session &session, agc::ActiveState &state,
+		 const ConfigurationParams &config);
+
+	int configure(agc::Session &session, agc::ActiveState &state,
+		      const ConfigurationParams &config);
+
+	void queueRequest(const agc::Session &session, agc::ActiveState &state,
+			  agc::FrameContext &frameContext, const ControlList &controls);
+
+	void prepare(agc::ActiveState &state, agc::FrameContext &frameContext);
+
+	void process(const agc::Session &session, agc::ActiveState &state,
+		     agc::FrameContext &frameContext, std::optional<ProcessParams> &&params,
+		     ControlList &metadata);
+
+private:
+	void processFrameDuration(const agc::Session &session,
+				  agc::FrameContext &frameContext,
+				  utils::Duration frameDuration);
+	void fillMetadata(const agc::Session &session,
+			  const agc::FrameContext &frameContext,
+			  ControlList &metadata);
+
+	AgcMeanLuminance impl_;
+};
+
 } /* namespace ipa */
 
 } /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
index 7230232d24..a83ee33b20 100644
--- a/src/ipa/rkisp1/algorithms/agc.cpp
+++ b/src/ipa/rkisp1/algorithms/agc.cpp
@@ -8,10 +8,7 @@ 
 #include "agc.h"
 
 #include <algorithm>
-#include <array>
-#include <chrono>
 #include <cmath>
-#include <tuple>
 #include <vector>
 
 #include <libcamera/base/log.h>
@@ -36,89 +33,6 @@  namespace ipa::rkisp1::algorithms {
 
 LOG_DEFINE_CATEGORY(RkISP1Agc)
 
-namespace {
-
-void reconfigure(IPAContext &context)
-{
-	context.configuration.sensor.lineDuration =
-		context.sensorInfo.minLineLength * 1.0s / context.sensorInfo.pixelRate;
-
-	double lineDurationUs = context.configuration.sensor.lineDuration.get<std::micro>();
-
-	/*
-	 * Compute exposure time limits from the V4L2_CID_EXPOSURE control
-	 * limits and the line duration.
-	 */
-
-	const ControlInfo &v4l2Exposure = context.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>();
-	context.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 = context.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>());
-	context.ctrlMap[&controls::AnalogueGain] = ControlInfo{
-		minGain,
-		maxGain,
-		defGain,
-	};
-
-	LOG(RkISP1Agc, 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 = context.sensorControls.find(V4L2_CID_HBLANK)->second;
-	uint32_t hblank = v4l2HBlank.def().get<int32_t>();
-	uint32_t lineLength = context.sensorInfo.outputSize.width + hblank;
-
-	const ControlInfo &v4l2VBlank = context.sensorControls.find(V4L2_CID_VBLANK)->second;
-	std::array<uint32_t, 3> frameHeights{
-		v4l2VBlank.min().get<int32_t>() + context.sensorInfo.outputSize.height,
-		v4l2VBlank.max().get<int32_t>() + context.sensorInfo.outputSize.height,
-		v4l2VBlank.def().get<int32_t>() + context.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 / (context.sensorInfo.pixelRate / 1000000U);
-	}
-
-	context.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
-		frameDurations[0],
-		frameDurations[1],
-		Span<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },
-	};
-
-	/*
-	 * 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 = minGain;
-	context.configuration.sensor.maxAnalogueGain = maxGain;
-}
-
-} /* namespace */
-
 /**
  * \class Agc
  * \brief A mean-based auto-exposure algorithm
@@ -222,7 +136,12 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 {
 	int ret;
 
-	ret = agc_.parseTuningData(tuningData);
+	ret = agc_.init(tuningData, context.configuration.agc, context.activeState.agc, {
+		.sensor = context.camHelper.get(),
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+	});
 	if (ret)
 		return ret;
 
@@ -231,21 +150,6 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 	if (ret)
 		return ret;
 
-	context.ctrlMap[&controls::ExposureTimeMode] =
-		ControlInfo({ { ControlValue(controls::ExposureTimeModeAuto),
-				ControlValue(controls::ExposureTimeModeManual) } },
-			    ControlValue(controls::ExposureTimeModeAuto));
-	context.ctrlMap[&controls::AnalogueGainMode] =
-		ControlInfo({ { ControlValue(controls::AnalogueGainModeAuto),
-				ControlValue(controls::AnalogueGainModeManual) } },
-			    ControlValue(controls::AnalogueGainModeAuto));
-	/* \todo Move this to the Camera class */
-	context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true, true);
-	context.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);
-	context.ctrlMap.merge(agc_.controls());
-
-	reconfigure(context);
-
 	return 0;
 }
 
@@ -258,47 +162,24 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
  */
 int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
 {
-	reconfigure(context);
-
-	/* Configure the default exposure and gain. */
-	context.activeState.agc.automatic.gain = context.configuration.sensor.minAnalogueGain;
-	context.activeState.agc.automatic.exposure =
-		10ms / context.configuration.sensor.lineDuration;
-	context.activeState.agc.automatic.quantizationGain = 1.0;
-	context.activeState.agc.manual.gain = context.activeState.agc.automatic.gain;
-	context.activeState.agc.manual.exposure = context.activeState.agc.automatic.exposure;
-	context.activeState.agc.autoExposureEnabled = !context.configuration.raw;
-	context.activeState.agc.autoGainEnabled = !context.configuration.raw;
-	context.activeState.agc.exposureValue = 0.0;
-
-	context.activeState.agc.constraintMode =
-		static_cast<controls::AeConstraintModeEnum>(agc_.constraintModes().begin()->first);
-	context.activeState.agc.exposureMode =
-		static_cast<controls::AeExposureModeEnum>(agc_.exposureModeHelpers().begin()->first);
+	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
+		.sensor = context.camHelper.get(),
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+		.autoAllowed = !context.configuration.raw,
+	});
+	if (ret)
+		return ret;
+
 	context.activeState.agc.meteringMode =
 		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
 
-	/* Limit the frame duration to match current initialisation */
-	ControlInfo &frameDurationLimits = context.ctrlMap[&controls::FrameDurationLimits];
-	context.activeState.agc.minFrameDuration = std::chrono::microseconds(frameDurationLimits.min().get<int64_t>());
-	context.activeState.agc.maxFrameDuration = std::chrono::microseconds(frameDurationLimits.max().get<int64_t>());
-
 	context.configuration.agc.measureWindow.h_offs = 0;
 	context.configuration.agc.measureWindow.v_offs = 0;
 	context.configuration.agc.measureWindow.h_size = configInfo.outputSize.width;
 	context.configuration.agc.measureWindow.v_size = configInfo.outputSize.height;
 
-	agc_.configure(context.configuration.sensor.lineDuration, context.camHelper.get());
-
-	agc_.setLimits(context.configuration.sensor.minExposureTime,
-		       context.configuration.sensor.maxExposureTime,
-		       context.configuration.sensor.minAnalogueGain,
-		       context.configuration.sensor.maxAnalogueGain, {});
-
-	context.activeState.agc.automatic.yTarget = agc_.effectiveYTarget(0, 1);
-
-	agc_.resetFrameCount();
-
 	return 0;
 }
 
@@ -312,73 +193,7 @@  void Agc::queueRequest(IPAContext &context,
 {
 	auto &agc = context.activeState.agc;
 
-	if (!context.configuration.raw) {
-		const auto &aeEnable = controls.get(controls::ExposureTimeMode);
-		if (aeEnable &&
-		    (*aeEnable == controls::ExposureTimeModeAuto) != agc.autoExposureEnabled) {
-			agc.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto);
-
-			LOG(RkISP1Agc, Debug)
-				<< (agc.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 (!agc.autoExposureEnabled && !controls.get(controls::ExposureTime))
-				frameContext.agc.autoExposureModeChange = true;
-		}
-
-		const auto &agEnable = controls.get(controls::AnalogueGainMode);
-		if (agEnable &&
-		    (*agEnable == controls::AnalogueGainModeAuto) != agc.autoGainEnabled) {
-			agc.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto);
-
-			LOG(RkISP1Agc, Debug)
-				<< (agc.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 (!agc.autoGainEnabled && !controls.get(controls::AnalogueGain))
-				frameContext.agc.autoGainModeChange = true;
-		}
-	}
-
-	const auto &exposure = controls.get(controls::ExposureTime);
-	if (exposure && !agc.autoExposureEnabled) {
-		agc.manual.exposure = *exposure * 1.0us
-				    / context.configuration.sensor.lineDuration;
-
-		LOG(RkISP1Agc, Debug)
-			<< "Set exposure to " << agc.manual.exposure;
-	}
-
-	const auto &gain = controls.get(controls::AnalogueGain);
-	if (gain && !agc.autoGainEnabled) {
-		agc.manual.gain = *gain;
-
-		LOG(RkISP1Agc, Debug) << "Set gain to " << agc.manual.gain;
-	}
-
-	frameContext.agc.autoExposureEnabled = agc.autoExposureEnabled;
-	frameContext.agc.autoGainEnabled = agc.autoGainEnabled;
-
-	if (!frameContext.agc.autoExposureEnabled)
-		frameContext.agc.exposure = agc.manual.exposure;
-	if (!frameContext.agc.autoGainEnabled)
-		frameContext.agc.gain = agc.manual.gain;
-
-	if (!frameContext.agc.autoExposureEnabled &&
-	    !frameContext.agc.autoGainEnabled)
-		frameContext.agc.quantizationGain = 1.0;
+	agc_.queueRequest(context.configuration.agc, agc, frameContext.agc, controls);
 
 	const auto &meteringMode = controls.get(controls::AeMeteringMode);
 	if (meteringMode) {
@@ -387,42 +202,6 @@  void Agc::queueRequest(IPAContext &context,
 			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
 	}
 	frameContext.agc.meteringMode = agc.meteringMode;
-
-	const auto &exposureMode = controls.get(controls::AeExposureMode);
-	if (exposureMode)
-		agc.exposureMode =
-			static_cast<controls::AeExposureModeEnum>(*exposureMode);
-	frameContext.agc.exposureMode = agc.exposureMode;
-
-	const auto &constraintMode = controls.get(controls::AeConstraintMode);
-	if (constraintMode)
-		agc.constraintMode =
-			static_cast<controls::AeConstraintModeEnum>(*constraintMode);
-	frameContext.agc.constraintMode = agc.constraintMode;
-
-	const auto &exposureValue = controls.get(controls::ExposureValue);
-	if (exposureValue)
-		agc.exposureValue = *exposureValue;
-	frameContext.agc.exposureValue = agc.exposureValue;
-
-	const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
-	if (frameDurationLimits) {
-		/* Limit the control value to the limits in ControlInfo */
-		ControlInfo &limits = context.ctrlMap[&controls::FrameDurationLimits];
-		int64_t minFrameDuration =
-			std::clamp((*frameDurationLimits).front(),
-				   limits.min().get<int64_t>(),
-				   limits.max().get<int64_t>());
-		int64_t maxFrameDuration =
-			std::clamp((*frameDurationLimits).back(),
-				   limits.min().get<int64_t>(),
-				   limits.max().get<int64_t>());
-
-		agc.minFrameDuration = std::chrono::microseconds(minFrameDuration);
-		agc.maxFrameDuration = std::chrono::microseconds(maxFrameDuration);
-	}
-	frameContext.agc.minFrameDuration = agc.minFrameDuration;
-	frameContext.agc.maxFrameDuration = agc.maxFrameDuration;
 }
 
 /**
@@ -431,41 +210,13 @@  void Agc::queueRequest(IPAContext &context,
 void Agc::prepare(IPAContext &context, const uint32_t frame,
 		  IPAFrameContext &frameContext, RkISP1Params *params)
 {
-	uint32_t activeAutoExposure = context.activeState.agc.automatic.exposure;
-	double activeAutoGain = context.activeState.agc.automatic.gain;
-	double activeAutoQGain = context.activeState.agc.automatic.quantizationGain;
-
-	/* Populate exposure and gain in auto mode */
-	if (frameContext.agc.autoExposureEnabled) {
-		frameContext.agc.exposure = activeAutoExposure;
-		frameContext.agc.quantizationGain = activeAutoQGain;
-	}
-	if (frameContext.agc.autoGainEnabled) {
-		frameContext.agc.gain = activeAutoGain;
-		frameContext.agc.quantizationGain = activeAutoQGain;
-	}
-
-	/*
-	 * Populate manual exposure and gain from the active auto values when
-	 * transitioning from auto to manual
-	 */
-	if (!frameContext.agc.autoExposureEnabled && frameContext.agc.autoExposureModeChange) {
-		context.activeState.agc.manual.exposure = activeAutoExposure;
-		frameContext.agc.exposure = activeAutoExposure;
-	}
-	if (!frameContext.agc.autoGainEnabled && frameContext.agc.autoGainModeChange) {
-		context.activeState.agc.manual.gain = activeAutoGain;
-		frameContext.agc.gain = activeAutoGain;
-		frameContext.agc.quantizationGain = activeAutoQGain;
-	}
+	agc_.prepare(context.activeState.agc, frameContext.agc);
 
 	if (context.configuration.compress.supported) {
 		frameContext.compress.enable = true;
 		frameContext.compress.gain = frameContext.agc.quantizationGain;
 	}
 
-	frameContext.agc.yTarget = context.activeState.agc.automatic.yTarget;
-
 	if (frame > 0 && !frameContext.agc.updateMetering)
 		return;
 
@@ -521,50 +272,6 @@  void Agc::prepare(IPAContext &context, const uint32_t frame,
 					   static_cast<rkisp1_cif_isp_histogram_mode>(hstConfig->mode));
 }
 
-void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
-		       ControlList &metadata)
-{
-	utils::Duration exposureTime = context.configuration.sensor.lineDuration
-				     * frameContext.sensor.exposure;
-	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
-	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
-	metadata.set(controls::FrameDuration, frameContext.agc.frameDuration.get<std::micro>());
-	metadata.set(controls::ExposureTimeMode,
-		     frameContext.agc.autoExposureEnabled
-		     ? controls::ExposureTimeModeAuto
-		     : controls::ExposureTimeModeManual);
-	metadata.set(controls::AnalogueGainMode,
-		     frameContext.agc.autoGainEnabled
-		     ? controls::AnalogueGainModeAuto
-		     : controls::AnalogueGainModeManual);
-
-	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
-	metadata.set(controls::AeExposureMode, frameContext.agc.exposureMode);
-	metadata.set(controls::AeConstraintMode, frameContext.agc.constraintMode);
-	metadata.set(controls::ExposureValue, frameContext.agc.exposureValue);
-}
-
-/**
- * \brief Process frame duration and compute vblank
- * \param[in] context The shared IPA context
- * \param[in] frameContext The current frame context
- * \param[in] frameDuration The target frame duration
- *
- * Compute and populate vblank from the target frame duration.
- */
-void Agc::processFrameDuration(IPAContext &context,
-			       IPAFrameContext &frameContext,
-			       utils::Duration frameDuration)
-{
-	IPACameraSensorInfo &sensorInfo = context.sensorInfo;
-	utils::Duration lineDuration = context.configuration.sensor.lineDuration;
-
-	frameContext.agc.vblank = (frameDuration / lineDuration) - sensorInfo.outputSize.height;
-
-	/* Update frame duration accounting for line length quantization. */
-	frameContext.agc.frameDuration = (sensorInfo.outputSize.height + frameContext.agc.vblank) * lineDuration;
-}
-
 namespace {
 
 class AgcTraits final : public AgcMeanLuminance::Traits
@@ -638,21 +345,6 @@  void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 		  IPAFrameContext &frameContext, const rkisp1_stat_buffer *stats,
 		  ControlList &metadata)
 {
-	if (!stats) {
-		processFrameDuration(context, frameContext,
-				     frameContext.agc.minFrameDuration);
-		fillMetadata(context, frameContext, metadata);
-		return;
-	}
-
-	if (!(stats->meas_type & RKISP1_CIF_ISP_STAT_AUTOEXP)) {
-		fillMetadata(context, frameContext, metadata);
-		LOG(RkISP1Agc, Error) << "AUTOEXP data is missing in statistics";
-		return;
-	}
-
-	const utils::Duration &lineDuration = context.configuration.sensor.lineDuration;
-
 	/*
 	 * \todo Verify that the exposure and gain applied by the sensor for
 	 * this frame match what has been requested. This isn't a hard
@@ -661,95 +353,46 @@  void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 	 * we receive), but is important in manual mode.
 	 */
 
-	const rkisp1_cif_isp_stat *params = &stats->params;
+	const rkisp1_cif_isp_stat *params = nullptr;
 
-	/*
-	 * 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.agc.autoExposureEnabled) {
-		minExposureTime = context.configuration.sensor.minExposureTime;
-		maxExposureTime = std::clamp(frameContext.agc.maxFrameDuration,
-					     context.configuration.sensor.minExposureTime,
-					     context.configuration.sensor.maxExposureTime);
-	} else {
-		minExposureTime = context.configuration.sensor.lineDuration
-				* frameContext.agc.exposure;
-		maxExposureTime = minExposureTime;
+	if (stats) {
+		if (stats->meas_type & RKISP1_CIF_ISP_STAT_AUTOEXP)
+			params = &stats->params;
+		else
+			LOG(RkISP1Agc, Error) << "AUTOEXP data is missing in statistics";
 	}
 
-	if (frameContext.agc.autoGainEnabled) {
-		minAnalogueGain = context.configuration.sensor.minAnalogueGain;
-		maxAnalogueGain = context.configuration.sensor.maxAnalogueGain;
+	if (params) {
+		std::vector<AgcMeanLuminance::AgcConstraint> additionalConstraints;
+		if (context.activeState.wdr.mode != controls::WdrOff)
+			additionalConstraints.push_back(context.activeState.wdr.constraint);
+
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
+			.traits = AgcTraits{
+				{ params->ae.exp_mean, context.hw.numAeCells },
+				meteringModes_.at(frameContext.agc.meteringMode),
+			},
+			.yHist = {
+				/* The lower 4 bits are fractional and meant to be discarded. */
+				{ params->hist.hist_bins, context.hw.numHistogramBins },
+				[](uint32_t x) { return x >> 4; },
+			},
+			.exposure = frameContext.sensor.exposure,
+			/*
+			 * Include the quantization gain if it was applied. Do not use
+			 * compress.gain because it will include gains that shall not be
+			 * reported to the user when HDR is implemented.
+			 */
+			.gain = frameContext.sensor.gain
+			        * (frameContext.compress.enable ? frameContext.agc.quantizationGain : 1),
+			.additionalConstraints = std::move(additionalConstraints),
+			.lux = frameContext.lux.lux,
+		}}, metadata);
 	} else {
-		minAnalogueGain = frameContext.agc.gain;
-		maxAnalogueGain = frameContext.agc.gain;
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
 	}
 
-	std::vector<AgcMeanLuminance::AgcConstraint> additionalConstraints;
-	if (context.activeState.wdr.mode != controls::WdrOff)
-		additionalConstraints.push_back(context.activeState.wdr.constraint);
-
-	agc_.setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain,
-		       std::move(additionalConstraints));
-
-	/*
-	 * The Agc algorithm needs to know the effective exposure value that was
-	 * applied to the sensor when the statistics were collected.
-	 */
-	utils::Duration exposureTime = lineDuration * frameContext.sensor.exposure;
-	double analogueGain = frameContext.sensor.gain;
-	utils::Duration effectiveExposureValue = exposureTime * analogueGain;
-
-	/*
-	 * Include the quantization gain if it was applied. Do not use
-	 * compress.gain because it will include gains that shall not be
-	 * reported to the user when HDR is implemented.
-	 */
-	if (frameContext.compress.enable)
-		effectiveExposureValue *= frameContext.agc.quantizationGain;
-
-	/* The lower 4 bits are fractional and meant to be discarded. */
-	Histogram hist({ params->hist.hist_bins, context.hw.numHistogramBins },
-		       [](uint32_t x) { return x >> 4; });
-
-	const auto &newEv = agc_.calculateNewEv({
-		.traits = AgcTraits{
-			{ params->ae.exp_mean, context.hw.numAeCells },
-			meteringModes_.at(frameContext.agc.meteringMode),
-		},
-		.yHist = hist,
-		.effectiveExposureValue = effectiveExposureValue,
-		.constraintModeIndex = frameContext.agc.constraintMode,
-		.exposureModeIndex = frameContext.agc.exposureMode,
-		.lux = frameContext.lux.lux,
-		.exposureCompensation = pow(2.0, frameContext.agc.exposureValue),
-	});
-
-	LOG(RkISP1Agc, Debug)
-		<< "Divided up exposure time, analogue gain, quantization gain"
-		<< " and digital gain are " << newEv.exposureTime << ", " << newEv.analogueGain
-		<< ", " << newEv.quantizationGain << " and " << newEv.digitalGain;
-
-	IPAActiveState &activeState = context.activeState;
-	/* Update the estimated exposure and gain. */
-	activeState.agc.automatic.exposure = newEv.exposureTime / lineDuration;
-	activeState.agc.automatic.gain = newEv.analogueGain;
-	activeState.agc.automatic.quantizationGain = newEv.quantizationGain;
-	activeState.agc.automatic.yTarget = newEv.yTarget;
-	/*
-	 * Expand the target frame duration so that we do not run faster than
-	 * the minimum frame duration when we have short exposures.
-	 */
-	processFrameDuration(context, frameContext,
-			     std::max(frameContext.agc.minFrameDuration, newEv.exposureTime));
-
-	fillMetadata(context, frameContext, metadata);
+	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
 }
 
 REGISTER_IPA_ALGORITHM(Agc, "Agc")
diff --git a/src/ipa/rkisp1/algorithms/agc.h b/src/ipa/rkisp1/algorithms/agc.h
index 0527ca0d5f..3a4d7bc546 100644
--- a/src/ipa/rkisp1/algorithms/agc.h
+++ b/src/ipa/rkisp1/algorithms/agc.h
@@ -14,7 +14,7 @@ 
 
 #include <libcamera/geometry.h>
 
-#include "libipa/agc_mean_luminance.h"
+#include "libipa/agc.h"
 
 #include "algorithm.h"
 
@@ -47,14 +47,8 @@  private:
 	uint8_t computeHistogramPredivider(const Size &size,
 					   enum rkisp1_cif_isp_histogram_mode mode);
 
-	void fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
-			  ControlList &metadata);
-	void processFrameDuration(IPAContext &context,
-				  IPAFrameContext &frameContext,
-				  utils::Duration frameDuration);
-
 	std::map<int32_t, std::vector<uint8_t>> meteringModes_;
-	AgcMeanLuminance agc_;
+	AgcAlgorithm agc_;
 };
 
 } /* namespace ipa::rkisp1::algorithms */
diff --git a/src/ipa/rkisp1/algorithms/lux.cpp b/src/ipa/rkisp1/algorithms/lux.cpp
index 86e46c492f..ce6928a55d 100644
--- a/src/ipa/rkisp1/algorithms/lux.cpp
+++ b/src/ipa/rkisp1/algorithms/lux.cpp
@@ -74,7 +74,7 @@  void Lux::process(IPAContext &context,
 	if (!stats)
 		return;
 
-	utils::Duration exposureTime = context.configuration.sensor.lineDuration *
+	utils::Duration exposureTime = context.configuration.agc.lineDuration *
 				       frameContext.sensor.exposure;
 	double gain = frameContext.sensor.gain;
 
diff --git a/src/ipa/rkisp1/ipa_context.cpp b/src/ipa/rkisp1/ipa_context.cpp
index 1f94afda6b..47691674ad 100644
--- a/src/ipa/rkisp1/ipa_context.cpp
+++ b/src/ipa/rkisp1/ipa_context.cpp
@@ -86,21 +86,6 @@  namespace libcamera::ipa::rkisp1 {
  * \var IPASessionConfiguration::sensor
  * \brief Sensor-specific configuration of the IPA
  *
- * \var IPASessionConfiguration::sensor.minExposureTime
- * \brief Minimum exposure time supported with the sensor
- *
- * \var IPASessionConfiguration::sensor.maxExposureTime
- * \brief Maximum exposure time supported with the sensor
- *
- * \var IPASessionConfiguration::sensor.minAnalogueGain
- * \brief Minimum analogue gain supported with the sensor
- *
- * \var IPASessionConfiguration::sensor.maxAnalogueGain
- * \brief Maximum analogue gain supported with the sensor
- *
- * \var IPASessionConfiguration::sensor.lineDuration
- * \brief Line duration in microseconds
- *
  * \var IPASessionConfiguration::sensor.size
  * \brief Sensor output resolution
  */
@@ -147,49 +132,8 @@  namespace libcamera::ipa::rkisp1 {
  * \var IPAActiveState::agc
  * \brief State for the Automatic Gain Control algorithm
  *
- * 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.
- *
- * \struct IPAActiveState::agc.manual
- * \brief Manual exposure time and analog gain (set through requests)
- *
- * \var IPAActiveState::agc.manual.exposure
- * \brief Manual exposure time expressed as a number of lines as set by the
- * ExposureTime control
- *
- * \var IPAActiveState::agc.manual.gain
- * \brief Manual analogue gain as set by the AnalogueGain control
- *
- * \struct IPAActiveState::agc.automatic
- * \brief Automatic exposure time and analog gain (computed by the algorithm)
- *
- * \var IPAActiveState::agc.automatic.exposure
- * \brief Automatic exposure time expressed as a number of lines
- *
- * \var IPAActiveState::agc.automatic.gain
- * \brief Automatic analogue gain multiplier
- *
- * \var IPAActiveState::agc.autoExposureEnabled
- * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
- *
- * \var IPAActiveState::agc.autoGainEnabled
- * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
- *
- * \var IPAActiveState::agc.constraintMode
- * \brief Constraint mode as set by the AeConstraintMode control
- *
- * \var IPAActiveState::agc.exposureMode
- * \brief Exposure mode as set by the AeExposureMode control
- *
  * \var IPAActiveState::agc.meteringMode
  * \brief Metering mode as set by the AeMeteringMode control
- *
- * \var IPAActiveState::agc.minFrameDuration
- * \brief Minimum frame duration as set by the FrameDurationLimits control
- *
- * \var IPAActiveState::agc.maxFrameDuration
- * \brief Maximum frame duration as set by the FrameDurationLimits control
  */
 
 /**
@@ -314,53 +258,11 @@  namespace libcamera::ipa::rkisp1 {
  * the vertical blanking period is determined to maintain a consistent frame
  * rate matched to the FrameDurationLimits as set by the user.
  *
- * \var IPAFrameContext::agc.exposure
- * \brief Exposure time expressed as a number of lines computed by the algorithm
- *
- * \var IPAFrameContext::agc.gain
- * \brief Analogue gain multiplier computed by the algorithm
- *
- * The gain should be adapted to the sensor specific gain code before applying.
- *
- * \var IPAFrameContext::agc.vblank
- * \brief Vertical blanking parameter computed by the algorithm
- *
- * \var IPAFrameContext::agc.autoExposureEnabled
- * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control
- *
- * \var IPAFrameContext::agc.autoGainEnabled
- * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control
- *
- * \var IPAFrameContext::agc.constraintMode
- * \brief Constraint mode as set by the AeConstraintMode control
- *
- * \var IPAFrameContext::agc.exposureMode
- * \brief Exposure mode as set by the AeExposureMode control
- *
  * \var IPAFrameContext::agc.meteringMode
  * \brief Metering mode as set by the AeMeteringMode control
  *
- * \var IPAFrameContext::agc.minFrameDuration
- * \brief Minimum frame duration as set by the FrameDurationLimits control
- *
- * \var IPAFrameContext::agc.maxFrameDuration
- * \brief Maximum frame duration as set by the FrameDurationLimits control
- *
- * \var IPAFrameContext::agc.frameDuration
- * \brief The actual FrameDuration used by the algorithm for the frame
- *
  * \var IPAFrameContext::agc.updateMetering
  * \brief Indicate if new ISP AGC metering parameters need to be applied
- *
- * \var IPAFrameContext::agc.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 IPAFrameContext::agc.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.
  */
 
 /**
diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
index cd213dd991..cc07bb9462 100644
--- a/src/ipa/rkisp1/ipa_context.h
+++ b/src/ipa/rkisp1/ipa_context.h
@@ -24,7 +24,7 @@ 
 #include "libcamera/internal/matrix.h"
 #include "libcamera/internal/vector.h"
 
-#include "libipa/agc_mean_luminance.h"
+#include "libipa/agc.h"
 #include "libipa/awb.h"
 #include "libipa/camera_sensor_helper.h"
 #include "libipa/ccm.h"
@@ -57,7 +57,7 @@  struct RKISP1AwbSession {
 };
 
 struct IPASessionConfiguration {
-	struct {
+	struct Agc : agc::Session {
 		struct rkisp1_cif_isp_window measureWindow;
 	} agc;
 
@@ -68,12 +68,6 @@  struct IPASessionConfiguration {
 	} compress;
 
 	struct {
-		utils::Duration minExposureTime;
-		utils::Duration maxExposureTime;
-		double minAnalogueGain;
-		double maxAnalogueGain;
-
-		utils::Duration lineDuration;
 		Size size;
 	} sensor;
 
@@ -82,26 +76,8 @@  struct IPASessionConfiguration {
 };
 
 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;
+	struct Agc : agc::ActiveState {
 		controls::AeMeteringModeEnum meteringMode;
-		utils::Duration minFrameDuration;
-		utils::Duration maxFrameDuration;
 	} agc;
 
 	ipa::awb::ActiveState awb;
@@ -145,24 +121,9 @@  struct IPAActiveState {
 };
 
 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;
+	struct Agc : agc::FrameContext {
 		controls::AeMeteringModeEnum meteringMode;
-		utils::Duration minFrameDuration;
-		utils::Duration maxFrameDuration;
-		utils::Duration frameDuration;
 		bool updateMetering;
-		bool autoExposureModeChange;
-		bool autoGainModeChange;
 	} agc;
 
 	ipa::awb::FrameContext awb;