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

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

Commit Message

Barnabás Pőcze Aug. 17, 2026, 11:43 a.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>
---
 src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
 src/ipa/libipa/agc.h              | 106 +++++
 src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
 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 +--
 src/ipa/rkisp1/rkisp1.cpp         |   4 -
 8 files changed, 805 insertions(+), 563 deletions(-)

Comments

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

On Mon, Aug 17, 2026 at 01:43:22PM +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>
> ---
>  src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
>  src/ipa/libipa/agc.h              | 106 +++++
>  src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
>  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 +--
>  src/ipa/rkisp1/rkisp1.cpp         |   4 -
>  8 files changed, 805 insertions(+), 563 deletions(-)
>
> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
> index 415a6d831f..3c6b12e452 100644
> --- a/src/ipa/libipa/agc.cpp
> +++ b/src/ipa/libipa/agc.cpp
> @@ -1,16 +1,32 @@
>  /* 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 <optional>
> +
> +#include <linux/v4l2-controls.h>
> +
> +#include <libcamera/base/log.h>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +
>  namespace libcamera {
>
>  namespace ipa {
>
> +using namespace std::chrono_literals;
> +
> +LOG_DEFINE_CATEGORY(Agc)
> +
>  namespace agc {
>
>  /**
> @@ -40,6 +56,625 @@ 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
> + */
> +int AgcAlgorithm::init(const ValueNode &tuningData)
> +{
> +	int ret = impl_.parseTuningData(tuningData);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +/**
> + * \brief Initialize the session configuration and active state
> + *
> + * \note The IPA algorithm implementation will most likely need to call
> + * this in its Algorithm::init() implementation in order to provide
> + * the initial controls for the camera.
> + */
> +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 Handle a \a queueRequest operation
> + */
> +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 Handle a \a prepare operation
> + */
> +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 Handle a \a process operation
> + */
> +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;
> +
> +	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 = 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 4789c06ef8..66aa0eacb0 100644
> --- a/src/ipa/libipa/agc.h
> +++ b/src/ipa/libipa/agc.h
> @@ -7,13 +7,19 @@
>
>  #pragma once
>
> +#include <optional>
>  #include <utility>
>
>  #include <linux/v4l2-controls.h>
>
> +#include <libcamera/control_ids.h>
>  #include <libcamera/controls.h>
>
> +#include <libcamera/ipa/core_ipa_interface.h>
> +
> +#include "agc_mean_luminance.h"
>  #include "camera_sensor_helper.h"
> +#include "histogram.h"
>
>  namespace libcamera {
>
> @@ -21,6 +27,61 @@ namespace ipa {
>
>  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)
> @@ -47,6 +108,51 @@ 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);
> +
> +	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 fc228452c3..4c2a066e86 100644
> --- a/src/ipa/rkisp1/algorithms/agc.cpp
> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> @@ -8,9 +8,7 @@
>  #include "agc.h"
>
>  #include <algorithm>
> -#include <chrono>
>  #include <cmath>
> -#include <tuple>
>  #include <vector>
>
>  #include <libcamera/base/log.h>
> @@ -35,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
> @@ -221,7 +136,16 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
>  {
>  	int ret;
>
> -	ret = agc_.parseTuningData(tuningData);
> +	ret = agc_.init(tuningData);
> +	if (ret)
> +		return ret;
> +
> +	ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
> +		.sensor = context.camHelper.get(),
> +		.sensorInfo = context.sensorInfo,
> +		.sensorControls = context.sensorControls,
> +		.ctrlMap = context.ctrlMap,
> +	});

The only comment, which might eventually be addressed as an on-top
change, is about the requirement to call AgcAlgorithm::init() and
configure() in the IPA init function.

What if the paramters required for configure() are passed to
AgcAlgorithm::init() and this function calls AgcAlgorithm::configure()
internally ?

Apart from this:
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

Thanks
  j

>  	if (ret)
>  		return ret;
>
> @@ -230,21 +154,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;
>  }
>
> @@ -257,47 +166,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;
>  }
>
> @@ -311,73 +197,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) {
> @@ -386,42 +206,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;
>  }
>
>  /**
> @@ -430,41 +214,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;
>
> @@ -520,50 +276,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
> @@ -637,21 +349,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
> @@ -660,95 +357,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;
> diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
> index 98ec5a5748..731a362cee 100644
> --- a/src/ipa/rkisp1/rkisp1.cpp
> +++ b/src/ipa/rkisp1/rkisp1.cpp
> @@ -6,8 +6,6 @@
>   */
>
>  #include <algorithm>
> -#include <array>
> -#include <chrono>
>  #include <stdint.h>
>  #include <string.h>
>
> @@ -40,8 +38,6 @@ namespace libcamera {
>
>  LOG_DEFINE_CATEGORY(IPARkISP1)
>
> -using namespace std::literals::chrono_literals;
> -
>  namespace ipa::rkisp1 {
>
>  /* Maximum number of frame contexts to be held */
> --
> 2.55.0
>
Stefan Klug Aug. 19, 2026, 3:31 p.m. UTC | #2
Hi Barnabás,

Thank you for the patch.

Quoting Barnabás Pőcze (2026-08-17 13:43:22)
> 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()`.

Eek I completely missed that in v4. Thanks for pointing it out.
I thought that this was problematic as cam showed only controls that
were available before configure. But testing it reveals that this is not
the case. So either I remember incorrectly or cam got fixed :-)

> 
> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
> ---
>  src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
>  src/ipa/libipa/agc.h              | 106 +++++
>  src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
>  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 +--
>  src/ipa/rkisp1/rkisp1.cpp         |   4 -
>  8 files changed, 805 insertions(+), 563 deletions(-)
> 
> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
> index 415a6d831f..3c6b12e452 100644
> --- a/src/ipa/libipa/agc.cpp
> +++ b/src/ipa/libipa/agc.cpp
> @@ -1,16 +1,32 @@
>  /* 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

Nit: line break

>   */
>  
>  #include "agc.h"
>  
> +#include <algorithm>
> +#include <array>
> +#include <chrono>
> +#include <optional>
> +
> +#include <linux/v4l2-controls.h>
> +
> +#include <libcamera/base/log.h>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/controls.h>
> +
>  namespace libcamera {
>  
>  namespace ipa {
>  
> +using namespace std::chrono_literals;
> +
> +LOG_DEFINE_CATEGORY(Agc)
> +
>  namespace agc {
>  
>  /**
> @@ -40,6 +56,625 @@ 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
> + */
> +

Moving these variables into agc::FrameContext and agx::ActiveState in a separate
preparatory patch might have reduced the size of this patch by quite a
bit. I don't want to send a new Yak, so I won't dwell on it :-)

> +/**
> + * \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
> + */
> +int AgcAlgorithm::init(const ValueNode &tuningData)
> +{
> +       int ret = impl_.parseTuningData(tuningData);
> +       if (ret)
> +               return ret;
> +
> +       return 0;
> +}
> +
> +/**
> + * \brief Initialize the session configuration and active state
> + *
> + * \note The IPA algorithm implementation will most likely need to call
> + * this in its Algorithm::init() implementation in order to provide
> + * the initial controls for the camera.
> + */
> +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 Handle a \a queueRequest operation
> + */
> +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 Handle a \a prepare operation
> + */
> +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 Handle a \a process operation
> + */
> +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;
> +
> +       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 = 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,

Does this one need documentation as it lives in libipa now?

> +                               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 4789c06ef8..66aa0eacb0 100644
> --- a/src/ipa/libipa/agc.h
> +++ b/src/ipa/libipa/agc.h
> @@ -7,13 +7,19 @@
>  
>  #pragma once
>  
> +#include <optional>
>  #include <utility>
>  
>  #include <linux/v4l2-controls.h>
>  
> +#include <libcamera/control_ids.h>
>  #include <libcamera/controls.h>
>  
> +#include <libcamera/ipa/core_ipa_interface.h>
> +
> +#include "agc_mean_luminance.h"
>  #include "camera_sensor_helper.h"
> +#include "histogram.h"
>  
>  namespace libcamera {
>  
> @@ -21,6 +27,61 @@ namespace ipa {
>  
>  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)
> @@ -47,6 +108,51 @@ 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);
> +
> +       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 fc228452c3..4c2a066e86 100644
> --- a/src/ipa/rkisp1/algorithms/agc.cpp
> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> @@ -8,9 +8,7 @@
>  #include "agc.h"
>  
>  #include <algorithm>
> -#include <chrono>
>  #include <cmath>
> -#include <tuple>
>  #include <vector>
>  
>  #include <libcamera/base/log.h>
> @@ -35,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
> @@ -221,7 +136,16 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
>  {
>         int ret;
>  
> -       ret = agc_.parseTuningData(tuningData);
> +       ret = agc_.init(tuningData);
> +       if (ret)
> +               return ret;
> +
> +       ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
> +               .sensor = context.camHelper.get(),
> +               .sensorInfo = context.sensorInfo,
> +               .sensorControls = context.sensorControls,
> +               .ctrlMap = context.ctrlMap,
> +       });
>         if (ret)
>                 return ret;
>  
> @@ -230,21 +154,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;
>  }
>  
> @@ -257,47 +166,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;
>  }
>  
> @@ -311,73 +197,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) {
> @@ -386,42 +206,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;
>  }
>  
>  /**
> @@ -430,41 +214,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;
>  
> @@ -520,50 +276,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
> @@ -637,21 +349,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
> @@ -660,95 +357,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;
> diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
> index 98ec5a5748..731a362cee 100644
> --- a/src/ipa/rkisp1/rkisp1.cpp
> +++ b/src/ipa/rkisp1/rkisp1.cpp
> @@ -6,8 +6,6 @@
>   */
>  
>  #include <algorithm>
> -#include <array>
> -#include <chrono>

These changes look unrelated?

>  #include <stdint.h>
>  #include <string.h>
>  
> @@ -40,8 +38,6 @@ namespace libcamera {
>  
>  LOG_DEFINE_CATEGORY(IPARkISP1)
>  
> -using namespace std::literals::chrono_literals;
> -
>  namespace ipa::rkisp1 {
>  
>  /* Maximum number of frame contexts to be held */
> -- 
> 2.55.0
> 

This is a massive beast. Thanks for squashing it. I like that it is now
possible to compare new code and old code in one patch. I didn't do
another detailed review, but that already happened on v4.

So

Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com> 
Tested-by: Stefan Klug <stefan.klug@ideasonboard.com> 

Best regards,
Stefan
Barnabás Pőcze Aug. 20, 2026, 9:08 a.m. UTC | #3
2026. 08. 19. 17:31 keltezéssel, Stefan Klug írta:
> Hi Barnabás,
> 
> Thank you for the patch.
> 
> Quoting Barnabás Pőcze (2026-08-17 13:43:22)
>> 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()`.
> 
> Eek I completely missed that in v4. Thanks for pointing it out.
> I thought that this was problematic as cam showed only controls that
> were available before configure. But testing it reveals that this is not
> the case. So either I remember incorrectly or cam got fixed :-)

Well, every user calls `configure()` in its `init()` to provide initial
controls for the camera (before configuration).


> 
>>
>> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
>> ---
>>   src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
>>   src/ipa/libipa/agc.h              | 106 +++++
>>   src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
>>   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 +--
>>   src/ipa/rkisp1/rkisp1.cpp         |   4 -
>>   8 files changed, 805 insertions(+), 563 deletions(-)
>>
>> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
>> index 415a6d831f..3c6b12e452 100644
>> --- a/src/ipa/libipa/agc.cpp
>> +++ b/src/ipa/libipa/agc.cpp
>> @@ -1,16 +1,32 @@
>>   /* 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
> 
> Nit: line break

How do you mean?


> 
>>    */
>>   
>>   #include "agc.h"
>>   
>> +#include <algorithm>
>> +#include <array>
>> +#include <chrono>
>> +#include <optional>
>> +
>> +#include <linux/v4l2-controls.h>
>> +
>> +#include <libcamera/base/log.h>
>> +
>> +#include <libcamera/control_ids.h>
>> +#include <libcamera/controls.h>
>> +
>>   namespace libcamera {
>>   
>>   namespace ipa {
>>   
>> +using namespace std::chrono_literals;
>> +
>> +LOG_DEFINE_CATEGORY(Agc)
>> +
>>   namespace agc {
>>   
>>   /**
>> @@ -40,6 +56,625 @@ 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
>> + */
>> +
> 
> Moving these variables into agc::FrameContext and agx::ActiveState in a separate
> preparatory patch might have reduced the size of this patch by quite a
> bit. I don't want to send a new Yak, so I won't dwell on it :-)

I thought about it, but now I'm not sure why I didn't do it. Maybe I'll try again.


> 
>> +/**
>> + * \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
>> + */
>> +int AgcAlgorithm::init(const ValueNode &tuningData)
>> +{
>> +       int ret = impl_.parseTuningData(tuningData);
>> +       if (ret)
>> +               return ret;
>> +
>> +       return 0;
>> +}
>> +
>> +/**
>> + * \brief Initialize the session configuration and active state
>> + *
>> + * \note The IPA algorithm implementation will most likely need to call
>> + * this in its Algorithm::init() implementation in order to provide
>> + * the initial controls for the camera.
>> + */
>> +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 Handle a \a queueRequest operation
>> + */
>> +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 Handle a \a prepare operation
>> + */
>> +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 Handle a \a process operation
>> + */
>> +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;
>> +
>> +       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 = 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,
> 
> Does this one need documentation as it lives in libipa now?

I don't know. It's a private function, implementation detail,
and fairly straightforward in my opinion.


> 
>> +                               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/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;
>> diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
>> index 98ec5a5748..731a362cee 100644
>> --- a/src/ipa/rkisp1/rkisp1.cpp
>> +++ b/src/ipa/rkisp1/rkisp1.cpp
>> @@ -6,8 +6,6 @@
>>    */
>>   
>>   #include <algorithm>
>> -#include <array>
>> -#include <chrono>
> 
> These changes look unrelated?

These are now unused because the code has been moved.


> 
>>   #include <stdint.h>
>>   #include <string.h>
>>   
>> @@ -40,8 +38,6 @@ namespace libcamera {
>>   
>>   LOG_DEFINE_CATEGORY(IPARkISP1)
>>   
>> -using namespace std::literals::chrono_literals;
>> -
>>   namespace ipa::rkisp1 {
>>   
>>   /* Maximum number of frame contexts to be held */
>> -- 
>> 2.55.0
>>
> 
> This is a massive beast. Thanks for squashing it. I like that it is now
> possible to compare new code and old code in one patch. I didn't do
> another detailed review, but that already happened on v4.
> 
> So
> 
> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
> Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
> 
> Best regards,
> Stefan
Stefan Klug Aug. 20, 2026, 10:10 a.m. UTC | #4
Hi,

Quoting Jacopo Mondi (2026-08-17 17:06:39)
> Hi Barnabás
> 
> On Mon, Aug 17, 2026 at 01:43:22PM +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>
> > ---
> >  src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
> >  src/ipa/libipa/agc.h              | 106 +++++
> >  src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
> >  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 +--
> >  src/ipa/rkisp1/rkisp1.cpp         |   4 -
> >  8 files changed, 805 insertions(+), 563 deletions(-)
> >
> > diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
> > index 415a6d831f..3c6b12e452 100644
> > --- a/src/ipa/libipa/agc.cpp
> > +++ b/src/ipa/libipa/agc.cpp
> > @@ -1,16 +1,32 @@
> >  /* 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 <optional>
> > +
> > +#include <linux/v4l2-controls.h>
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +#include <libcamera/control_ids.h>
> > +#include <libcamera/controls.h>
> > +
> >  namespace libcamera {
> >
> >  namespace ipa {
> >
> > +using namespace std::chrono_literals;
> > +
> > +LOG_DEFINE_CATEGORY(Agc)
> > +
> >  namespace agc {
> >
> >  /**
> > @@ -40,6 +56,625 @@ 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
> > + */
> > +int AgcAlgorithm::init(const ValueNode &tuningData)
> > +{
> > +     int ret = impl_.parseTuningData(tuningData);
> > +     if (ret)
> > +             return ret;
> > +
> > +     return 0;
> > +}
> > +
> > +/**
> > + * \brief Initialize the session configuration and active state
> > + *
> > + * \note The IPA algorithm implementation will most likely need to call
> > + * this in its Algorithm::init() implementation in order to provide
> > + * the initial controls for the camera.
> > + */
> > +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 Handle a \a queueRequest operation
> > + */
> > +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 Handle a \a prepare operation
> > + */
> > +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 Handle a \a process operation
> > + */
> > +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;
> > +
> > +     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 = 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 4789c06ef8..66aa0eacb0 100644
> > --- a/src/ipa/libipa/agc.h
> > +++ b/src/ipa/libipa/agc.h
> > @@ -7,13 +7,19 @@
> >
> >  #pragma once
> >
> > +#include <optional>
> >  #include <utility>
> >
> >  #include <linux/v4l2-controls.h>
> >
> > +#include <libcamera/control_ids.h>
> >  #include <libcamera/controls.h>
> >
> > +#include <libcamera/ipa/core_ipa_interface.h>
> > +
> > +#include "agc_mean_luminance.h"
> >  #include "camera_sensor_helper.h"
> > +#include "histogram.h"
> >
> >  namespace libcamera {
> >
> > @@ -21,6 +27,61 @@ namespace ipa {
> >
> >  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)
> > @@ -47,6 +108,51 @@ 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);
> > +
> > +     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 fc228452c3..4c2a066e86 100644
> > --- a/src/ipa/rkisp1/algorithms/agc.cpp
> > +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> > @@ -8,9 +8,7 @@
> >  #include "agc.h"
> >
> >  #include <algorithm>
> > -#include <chrono>
> >  #include <cmath>
> > -#include <tuple>
> >  #include <vector>
> >
> >  #include <libcamera/base/log.h>
> > @@ -35,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
> > @@ -221,7 +136,16 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
> >  {
> >       int ret;
> >
> > -     ret = agc_.parseTuningData(tuningData);
> > +     ret = agc_.init(tuningData);
> > +     if (ret)
> > +             return ret;
> > +
> > +     ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
> > +             .sensor = context.camHelper.get(),
> > +             .sensorInfo = context.sensorInfo,
> > +             .sensorControls = context.sensorControls,
> > +             .ctrlMap = context.ctrlMap,
> > +     });
> 
> The only comment, which might eventually be addressed as an on-top
> change, is about the requirement to call AgcAlgorithm::init() and
> configure() in the IPA init function.
> 
> What if the paramters required for configure() are passed to
> AgcAlgorithm::init() and this function calls AgcAlgorithm::configure()
> internally ?

Now I stumbled over that part in my review also. I think I like that
idea. Having the outer algorithm call the equally named function only
(init() calls init() and configure() calls configure()) seems to be a
good idea.

Best regards,
Stefan

> 
> Apart from this:
> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> 
> Thanks
>   j
> 
> >       if (ret)
> >               return ret;
> >
> > @@ -230,21 +154,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;
> >  }
> >
> > @@ -257,47 +166,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;
> >  }
> >
> > @@ -311,73 +197,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) {
> > @@ -386,42 +206,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;
> >  }
> >
> >  /**
> > @@ -430,41 +214,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;
> >
> > @@ -520,50 +276,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
> > @@ -637,21 +349,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
> > @@ -660,95 +357,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;
> > diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
> > index 98ec5a5748..731a362cee 100644
> > --- a/src/ipa/rkisp1/rkisp1.cpp
> > +++ b/src/ipa/rkisp1/rkisp1.cpp
> > @@ -6,8 +6,6 @@
> >   */
> >
> >  #include <algorithm>
> > -#include <array>
> > -#include <chrono>
> >  #include <stdint.h>
> >  #include <string.h>
> >
> > @@ -40,8 +38,6 @@ namespace libcamera {
> >
> >  LOG_DEFINE_CATEGORY(IPARkISP1)
> >
> > -using namespace std::literals::chrono_literals;
> > -
> >  namespace ipa::rkisp1 {
> >
> >  /* Maximum number of frame contexts to be held */
> > --
> > 2.55.0
> >
Stefan Klug Aug. 20, 2026, 10:19 a.m. UTC | #5
Hi Barnabás,

Quoting Barnabás Pőcze (2026-08-20 11:08:00)
> 2026. 08. 19. 17:31 keltezéssel, Stefan Klug írta:
> > Hi Barnabás,
> > 
> > Thank you for the patch.
> > 
> > Quoting Barnabás Pőcze (2026-08-17 13:43:22)
> >> 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()`.
> > 
> > Eek I completely missed that in v4. Thanks for pointing it out.
> > I thought that this was problematic as cam showed only controls that
> > were available before configure. But testing it reveals that this is not
> > the case. So either I remember incorrectly or cam got fixed :-)
> 
> Well, every user calls `configure()` in its `init()` to provide initial
> controls for the camera (before configuration).

Oh I missed that, Tanks for clarification. Maybe the proposal from
Jacopo in his reply is a good one?

> 
> 
> > 
> >>
> >> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
> >> ---
> >>   src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
> >>   src/ipa/libipa/agc.h              | 106 +++++
> >>   src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
> >>   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 +--
> >>   src/ipa/rkisp1/rkisp1.cpp         |   4 -
> >>   8 files changed, 805 insertions(+), 563 deletions(-)
> >>
> >> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
> >> index 415a6d831f..3c6b12e452 100644
> >> --- a/src/ipa/libipa/agc.cpp
> >> +++ b/src/ipa/libipa/agc.cpp
> >> @@ -1,16 +1,32 @@
> >>   /* 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
> > 
> > Nit: line break
> 
> How do you mean?
> 

Sorry, I miscounted in the email. The line looked so long but is
actually exactly 80 chars.

> 
> > 
> >>    */
> >>   
> >>   #include "agc.h"
> >>   
> >> +#include <algorithm>
> >> +#include <array>
> >> +#include <chrono>
> >> +#include <optional>
> >> +
> >> +#include <linux/v4l2-controls.h>
> >> +
> >> +#include <libcamera/base/log.h>
> >> +
> >> +#include <libcamera/control_ids.h>
> >> +#include <libcamera/controls.h>
> >> +
> >>   namespace libcamera {
> >>   
> >>   namespace ipa {
> >>   
> >> +using namespace std::chrono_literals;
> >> +
> >> +LOG_DEFINE_CATEGORY(Agc)
> >> +
> >>   namespace agc {
> >>   
> >>   /**
> >> @@ -40,6 +56,625 @@ 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
> >> + */
> >> +
> > 
> > Moving these variables into agc::FrameContext and agx::ActiveState in a separate
> > preparatory patch might have reduced the size of this patch by quite a
> > bit. I don't want to send a new Yak, so I won't dwell on it :-)
> 
> I thought about it, but now I'm not sure why I didn't do it. Maybe I'll try again.
> 
> 
> > 
> >> +/**
> >> + * \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
> >> + */
> >> +int AgcAlgorithm::init(const ValueNode &tuningData)
> >> +{
> >> +       int ret = impl_.parseTuningData(tuningData);
> >> +       if (ret)
> >> +               return ret;
> >> +
> >> +       return 0;
> >> +}
> >> +
> >> +/**
> >> + * \brief Initialize the session configuration and active state
> >> + *
> >> + * \note The IPA algorithm implementation will most likely need to call
> >> + * this in its Algorithm::init() implementation in order to provide
> >> + * the initial controls for the camera.
> >> + */
> >> +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 Handle a \a queueRequest operation
> >> + */
> >> +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 Handle a \a prepare operation
> >> + */
> >> +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 Handle a \a process operation
> >> + */
> >> +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;
> >> +
> >> +       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 = 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,
> > 
> > Does this one need documentation as it lives in libipa now?
> 
> I don't know. It's a private function, implementation detail,
> and fairly straightforward in my opinion.

Oh I expected the doxygen to nag it. But as it's private it won't. Then
it is fine with me.

> 
> 
> > 
> >> +                               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/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;
> >> diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
> >> index 98ec5a5748..731a362cee 100644
> >> --- a/src/ipa/rkisp1/rkisp1.cpp
> >> +++ b/src/ipa/rkisp1/rkisp1.cpp
> >> @@ -6,8 +6,6 @@
> >>    */
> >>   
> >>   #include <algorithm>
> >> -#include <array>
> >> -#include <chrono>
> > 
> > These changes look unrelated?
> 
> These are now unused because the code has been moved.
> 

But then these were wrong already before this patch as there were no
other changes in this file?

But no worries, cleanup is always good.

Best regards,
Stefan

> 
> > 
> >>   #include <stdint.h>
> >>   #include <string.h>
> >>   
> >> @@ -40,8 +38,6 @@ namespace libcamera {
> >>   
> >>   LOG_DEFINE_CATEGORY(IPARkISP1)
> >>   
> >> -using namespace std::literals::chrono_literals;
> >> -
> >>   namespace ipa::rkisp1 {
> >>   
> >>   /* Maximum number of frame contexts to be held */
> >> -- 
> >> 2.55.0
> >>
> > 
> > This is a massive beast. Thanks for squashing it. I like that it is now
> > possible to compare new code and old code in one patch. I didn't do
> > another detailed review, but that already happened on v4.
> > 
> > So
> > 
> > Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
> > Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
> > 
> > Best regards,
> > Stefan
>
Barnabás Pőcze Aug. 20, 2026, 10:34 a.m. UTC | #6
2026. 08. 20. 12:10 keltezéssel, Stefan Klug írta:
> Hi,
> 
> Quoting Jacopo Mondi (2026-08-17 17:06:39)
>> Hi Barnabás
>>
>> On Mon, Aug 17, 2026 at 01:43:22PM +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>
>>> ---
>>>   src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
>>>   src/ipa/libipa/agc.h              | 106 +++++
>>>   src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
>>>   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 +--
>>>   src/ipa/rkisp1/rkisp1.cpp         |   4 -
>>>   8 files changed, 805 insertions(+), 563 deletions(-)
>>>
>>> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
>>> index 415a6d831f..3c6b12e452 100644
>>> --- a/src/ipa/libipa/agc.cpp
>>> +++ b/src/ipa/libipa/agc.cpp
>>> @@ -1,16 +1,32 @@
>>>   /* 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 <optional>
>>> +
>>> +#include <linux/v4l2-controls.h>
>>> +
>>> +#include <libcamera/base/log.h>
>>> +
>>> +#include <libcamera/control_ids.h>
>>> +#include <libcamera/controls.h>
>>> +
>>>   namespace libcamera {
>>>
>>>   namespace ipa {
>>>
>>> +using namespace std::chrono_literals;
>>> +
>>> +LOG_DEFINE_CATEGORY(Agc)
>>> +
>>>   namespace agc {
>>>
>>>   /**
>>> @@ -40,6 +56,625 @@ 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
>>> + */
>>> +int AgcAlgorithm::init(const ValueNode &tuningData)
>>> +{
>>> +     int ret = impl_.parseTuningData(tuningData);
>>> +     if (ret)
>>> +             return ret;
>>> +
>>> +     return 0;
>>> +}
>>> +
>>> +/**
>>> + * \brief Initialize the session configuration and active state
>>> + *
>>> + * \note The IPA algorithm implementation will most likely need to call
>>> + * this in its Algorithm::init() implementation in order to provide
>>> + * the initial controls for the camera.
>>> + */
>>> +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 Handle a \a queueRequest operation
>>> + */
>>> +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 Handle a \a prepare operation
>>> + */
>>> +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 Handle a \a process operation
>>> + */
>>> +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;
>>> +
>>> +     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 = 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 4789c06ef8..66aa0eacb0 100644
>>> --- a/src/ipa/libipa/agc.h
>>> +++ b/src/ipa/libipa/agc.h
>>> @@ -7,13 +7,19 @@
>>>
>>>   #pragma once
>>>
>>> +#include <optional>
>>>   #include <utility>
>>>
>>>   #include <linux/v4l2-controls.h>
>>>
>>> +#include <libcamera/control_ids.h>
>>>   #include <libcamera/controls.h>
>>>
>>> +#include <libcamera/ipa/core_ipa_interface.h>
>>> +
>>> +#include "agc_mean_luminance.h"
>>>   #include "camera_sensor_helper.h"
>>> +#include "histogram.h"
>>>
>>>   namespace libcamera {
>>>
>>> @@ -21,6 +27,61 @@ namespace ipa {
>>>
>>>   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)
>>> @@ -47,6 +108,51 @@ 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);
>>> +
>>> +     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 fc228452c3..4c2a066e86 100644
>>> --- a/src/ipa/rkisp1/algorithms/agc.cpp
>>> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
>>> @@ -8,9 +8,7 @@
>>>   #include "agc.h"
>>>
>>>   #include <algorithm>
>>> -#include <chrono>
>>>   #include <cmath>
>>> -#include <tuple>
>>>   #include <vector>
>>>
>>>   #include <libcamera/base/log.h>
>>> @@ -35,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
>>> @@ -221,7 +136,16 @@ int Agc::init(IPAContext &context, const ValueNode &tuningData)
>>>   {
>>>        int ret;
>>>
>>> -     ret = agc_.parseTuningData(tuningData);
>>> +     ret = agc_.init(tuningData);
>>> +     if (ret)
>>> +             return ret;
>>> +
>>> +     ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
>>> +             .sensor = context.camHelper.get(),
>>> +             .sensorInfo = context.sensorInfo,
>>> +             .sensorControls = context.sensorControls,
>>> +             .ctrlMap = context.ctrlMap,
>>> +     });
>>
>> The only comment, which might eventually be addressed as an on-top
>> change, is about the requirement to call AgcAlgorithm::init() and
>> configure() in the IPA init function.
>>
>> What if the paramters required for configure() are passed to
>> AgcAlgorithm::init() and this function calls AgcAlgorithm::configure()
>> internally ?
> 
> Now I stumbled over that part in my review also. I think I like that
> idea. Having the outer algorithm call the equally named function only
> (init() calls init() and configure() calls configure()) seems to be a
> good idea.

Okay, let's do that. Although I would rather modify each algorithm to manage
the controls exclusively in the `configure()` phase.


> 
> Best regards,
> Stefan
> 
>>
>> Apart from this:
>> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>>
>> Thanks
>>    j
>>
>>>        if (ret)
>>>                return ret;
>>>
>>> @@ -230,21 +154,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;
>>>   }
>>>
>>> @@ -257,47 +166,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;
>>>   }
>>>
>>> @@ -311,73 +197,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) {
>>> @@ -386,42 +206,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;
>>>   }
>>>
>>>   /**
>>> @@ -430,41 +214,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;
>>>
>>> @@ -520,50 +276,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
>>> @@ -637,21 +349,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
>>> @@ -660,95 +357,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;
>>> diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
>>> index 98ec5a5748..731a362cee 100644
>>> --- a/src/ipa/rkisp1/rkisp1.cpp
>>> +++ b/src/ipa/rkisp1/rkisp1.cpp
>>> @@ -6,8 +6,6 @@
>>>    */
>>>
>>>   #include <algorithm>
>>> -#include <array>
>>> -#include <chrono>
>>>   #include <stdint.h>
>>>   #include <string.h>
>>>
>>> @@ -40,8 +38,6 @@ namespace libcamera {
>>>
>>>   LOG_DEFINE_CATEGORY(IPARkISP1)
>>>
>>> -using namespace std::literals::chrono_literals;
>>> -
>>>   namespace ipa::rkisp1 {
>>>
>>>   /* Maximum number of frame contexts to be held */
>>> --
>>> 2.55.0
>>>
Barnabás Pőcze Aug. 20, 2026, 10:38 a.m. UTC | #7
2026. 08. 20. 12:19 keltezéssel, Stefan Klug írta:
> Hi Barnabás,
> 
> Quoting Barnabás Pőcze (2026-08-20 11:08:00)
>> 2026. 08. 19. 17:31 keltezéssel, Stefan Klug írta:
>>> Hi Barnabás,
>>>
>>> Thank you for the patch.
>>>
>>> Quoting Barnabás Pőcze (2026-08-17 13:43:22)
>>>> 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()`.
>>>
>>> Eek I completely missed that in v4. Thanks for pointing it out.
>>> I thought that this was problematic as cam showed only controls that
>>> were available before configure. But testing it reveals that this is not
>>> the case. So either I remember incorrectly or cam got fixed :-)
>>
>> Well, every user calls `configure()` in its `init()` to provide initial
>> controls for the camera (before configuration).
> 
> Oh I missed that, Tanks for clarification. Maybe the proposal from
> Jacopo in his reply is a good one?
> 
>>
>>
>>>
>>>>
>>>> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
>>>> ---
>>>>    src/ipa/libipa/agc.cpp            | 637 +++++++++++++++++++++++++++++-
>>>>    src/ipa/libipa/agc.h              | 106 +++++
>>>>    src/ipa/rkisp1/algorithms/agc.cpp | 464 +++-------------------
>>>>    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 +--
>>>>    src/ipa/rkisp1/rkisp1.cpp         |   4 -
>>>>    8 files changed, 805 insertions(+), 563 deletions(-)
>>>>
>>>> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
>>>> index 415a6d831f..3c6b12e452 100644
>>>> --- a/src/ipa/libipa/agc.cpp
>>>> +++ b/src/ipa/libipa/agc.cpp
>>>> @@ -1,16 +1,32 @@
>>>>    /* 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
>>>
>>> Nit: line break
>>
>> How do you mean?
>>
> 
> Sorry, I miscounted in the email. The line looked so long but is
> actually exactly 80 chars.
> 
>>
>>>
>>>>     */
>>>>    
>>>>    #include "agc.h"
>>>>    
>>>> +#include <algorithm>
>>>> +#include <array>
>>>> +#include <chrono>
>>>> +#include <optional>
>>>> +
>>>> +#include <linux/v4l2-controls.h>
>>>> +
>>>> +#include <libcamera/base/log.h>
>>>> +
>>>> +#include <libcamera/control_ids.h>
>>>> +#include <libcamera/controls.h>
>>>> +
>>>>    namespace libcamera {
>>>>    
>>>>    namespace ipa {
>>>>    
>>>> +using namespace std::chrono_literals;
>>>> +
>>>> +LOG_DEFINE_CATEGORY(Agc)
>>>> +
>>>>    namespace agc {
>>>>    
>>>>    /**
>>>> @@ -40,6 +56,625 @@ 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
>>>> + */
>>>> +
>>>
>>> Moving these variables into agc::FrameContext and agx::ActiveState in a separate
>>> preparatory patch might have reduced the size of this patch by quite a
>>> bit. I don't want to send a new Yak, so I won't dwell on it :-)
>>
>> I thought about it, but now I'm not sure why I didn't do it. Maybe I'll try again.
>>
>>
>>>
>>>> +/**
>>>> + * \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
>>>> + */
>>>> +int AgcAlgorithm::init(const ValueNode &tuningData)
>>>> +{
>>>> +       int ret = impl_.parseTuningData(tuningData);
>>>> +       if (ret)
>>>> +               return ret;
>>>> +
>>>> +       return 0;
>>>> +}
>>>> +
>>>> +/**
>>>> + * \brief Initialize the session configuration and active state
>>>> + *
>>>> + * \note The IPA algorithm implementation will most likely need to call
>>>> + * this in its Algorithm::init() implementation in order to provide
>>>> + * the initial controls for the camera.
>>>> + */
>>>> +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 Handle a \a queueRequest operation
>>>> + */
>>>> +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 Handle a \a prepare operation
>>>> + */
>>>> +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 Handle a \a process operation
>>>> + */
>>>> +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;
>>>> +
>>>> +       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 = 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,
>>>
>>> Does this one need documentation as it lives in libipa now?
>>
>> I don't know. It's a private function, implementation detail,
>> and fairly straightforward in my opinion.
> 
> Oh I expected the doxygen to nag it. But as it's private it won't. Then
> it is fine with me.
> 
>>
>>
>>>
>>>> +                               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/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;
>>>> diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
>>>> index 98ec5a5748..731a362cee 100644
>>>> --- a/src/ipa/rkisp1/rkisp1.cpp
>>>> +++ b/src/ipa/rkisp1/rkisp1.cpp
>>>> @@ -6,8 +6,6 @@
>>>>     */
>>>>    
>>>>    #include <algorithm>
>>>> -#include <array>
>>>> -#include <chrono>
>>>
>>> These changes look unrelated?
>>
>> These are now unused because the code has been moved.
>>
> 
> But then these were wrong already before this patch as there were no
> other changes in this file?
> 
> But no worries, cleanup is always good.

Ahh, you're right, this change is indeed in the wrong commit, I have now fixed that.


> 
> Best regards,
> Stefan
> 
>>
>>>
>>>>    #include <stdint.h>
>>>>    #include <string.h>
>>>>    
>>>> @@ -40,8 +38,6 @@ namespace libcamera {
>>>>    
>>>>    LOG_DEFINE_CATEGORY(IPARkISP1)
>>>>    
>>>> -using namespace std::literals::chrono_literals;
>>>> -
>>>>    namespace ipa::rkisp1 {
>>>>    
>>>>    /* Maximum number of frame contexts to be held */
>>>> -- 
>>>> 2.55.0
>>>>
>>>
>>> This is a massive beast. Thanks for squashing it. I like that it is now
>>> possible to compare new code and old code in one patch. I didn't do
>>> another detailed review, but that already happened on v4.
>>>
>>> So
>>>
>>> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
>>> Tested-by: Stefan Klug <stefan.klug@ideasonboard.com>
>>>
>>> Best regards,
>>> Stefan
>>

Patch
diff mbox series

diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
index 415a6d831f..3c6b12e452 100644
--- a/src/ipa/libipa/agc.cpp
+++ b/src/ipa/libipa/agc.cpp
@@ -1,16 +1,32 @@ 
 /* 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 <optional>
+
+#include <linux/v4l2-controls.h>
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+
 namespace libcamera {
 
 namespace ipa {
 
+using namespace std::chrono_literals;
+
+LOG_DEFINE_CATEGORY(Agc)
+
 namespace agc {
 
 /**
@@ -40,6 +56,625 @@  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
+ */
+int AgcAlgorithm::init(const ValueNode &tuningData)
+{
+	int ret = impl_.parseTuningData(tuningData);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+/**
+ * \brief Initialize the session configuration and active state
+ *
+ * \note The IPA algorithm implementation will most likely need to call
+ * this in its Algorithm::init() implementation in order to provide
+ * the initial controls for the camera.
+ */
+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 Handle a \a queueRequest operation
+ */
+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 Handle a \a prepare operation
+ */
+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 Handle a \a process operation
+ */
+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;
+
+	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 = 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 4789c06ef8..66aa0eacb0 100644
--- a/src/ipa/libipa/agc.h
+++ b/src/ipa/libipa/agc.h
@@ -7,13 +7,19 @@ 
 
 #pragma once
 
+#include <optional>
 #include <utility>
 
 #include <linux/v4l2-controls.h>
 
+#include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "agc_mean_luminance.h"
 #include "camera_sensor_helper.h"
+#include "histogram.h"
 
 namespace libcamera {
 
@@ -21,6 +27,61 @@  namespace ipa {
 
 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)
@@ -47,6 +108,51 @@  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);
+
+	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 fc228452c3..4c2a066e86 100644
--- a/src/ipa/rkisp1/algorithms/agc.cpp
+++ b/src/ipa/rkisp1/algorithms/agc.cpp
@@ -8,9 +8,7 @@ 
 #include "agc.h"
 
 #include <algorithm>
-#include <chrono>
 #include <cmath>
-#include <tuple>
 #include <vector>
 
 #include <libcamera/base/log.h>
@@ -35,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
@@ -221,7 +136,16 @@  int Agc::init(IPAContext &context, const ValueNode &tuningData)
 {
 	int ret;
 
-	ret = agc_.parseTuningData(tuningData);
+	ret = agc_.init(tuningData);
+	if (ret)
+		return ret;
+
+	ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
+		.sensor = context.camHelper.get(),
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+	});
 	if (ret)
 		return ret;
 
@@ -230,21 +154,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;
 }
 
@@ -257,47 +166,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;
 }
 
@@ -311,73 +197,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) {
@@ -386,42 +206,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;
 }
 
 /**
@@ -430,41 +214,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;
 
@@ -520,50 +276,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
@@ -637,21 +349,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
@@ -660,95 +357,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;
diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
index 98ec5a5748..731a362cee 100644
--- a/src/ipa/rkisp1/rkisp1.cpp
+++ b/src/ipa/rkisp1/rkisp1.cpp
@@ -6,8 +6,6 @@ 
  */
 
 #include <algorithm>
-#include <array>
-#include <chrono>
 #include <stdint.h>
 #include <string.h>
 
@@ -40,8 +38,6 @@  namespace libcamera {
 
 LOG_DEFINE_CATEGORY(IPARkISP1)
 
-using namespace std::literals::chrono_literals;
-
 namespace ipa::rkisp1 {
 
 /* Maximum number of frame contexts to be held */