[v3,14/21] ipa: rppx1: agc: Add
diff mbox series

Message ID 20260918120949.191668-15-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • libcamera: rcar-gen4 + rpp-x1
Related show

Commit Message

Barnabás Pőcze Sept. 18, 2026, 12:09 p.m. UTC
From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

Add the algorithm to the rppx1 ipa module based on the corresponding
algorithm in the rkisp1 ipa module.

Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/ipa/rppx1/algorithms/agc.cpp     | 391 +++++++++++++++++++++++++++
 src/ipa/rppx1/algorithms/agc.h       |  47 ++++
 src/ipa/rppx1/algorithms/meson.build |   1 +
 src/ipa/rppx1/ipa_context.h          |  19 +-
 src/ipa/rppx1/params.h               |   4 +
 src/ipa/rppx1/rppx1.cpp              |  17 +-
 src/ipa/rppx1/stats.h                |   5 +
 7 files changed, 481 insertions(+), 3 deletions(-)
 create mode 100644 src/ipa/rppx1/algorithms/agc.cpp
 create mode 100644 src/ipa/rppx1/algorithms/agc.h

Comments

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

On Fri, Sep 18, 2026 at 02:09:42PM +0200, Barnabás Pőcze wrote:
> From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>
> Add the algorithm to the rppx1 ipa module based on the corresponding
> algorithm in the rkisp1 ipa module.
>
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>  src/ipa/rppx1/algorithms/agc.cpp     | 391 +++++++++++++++++++++++++++
>  src/ipa/rppx1/algorithms/agc.h       |  47 ++++
>  src/ipa/rppx1/algorithms/meson.build |   1 +
>  src/ipa/rppx1/ipa_context.h          |  19 +-
>  src/ipa/rppx1/params.h               |   4 +
>  src/ipa/rppx1/rppx1.cpp              |  17 +-
>  src/ipa/rppx1/stats.h                |   5 +
>  7 files changed, 481 insertions(+), 3 deletions(-)
>  create mode 100644 src/ipa/rppx1/algorithms/agc.cpp
>  create mode 100644 src/ipa/rppx1/algorithms/agc.h
>
> diff --git a/src/ipa/rppx1/algorithms/agc.cpp b/src/ipa/rppx1/algorithms/agc.cpp
> new file mode 100644
> index 0000000000..fd782cea3d
> --- /dev/null
> +++ b/src/ipa/rppx1/algorithms/agc.cpp
> @@ -0,0 +1,391 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * AGC/AEC mean-based control algorithm
> + */
> +
> +#include "agc.h"
> +
> +#include <algorithm>
> +#include <cmath>
> +#include <iterator>
> +#include <vector>
> +
> +#include <libcamera/base/log.h>
> +#include <libcamera/base/utils.h>
> +
> +#include <libcamera/control_ids.h>
> +#include <libcamera/geometry.h>
> +
> +#include <libcamera/ipa/core_ipa_interface.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +#include "libipa/fixedpoint.h"
> +#include "libipa/histogram.h"
> +
> +/**
> + * \file agc.h
> + */
> +
> +namespace libcamera {
> +
> +namespace ipa::rppx1::algorithms {
> +
> +using WindowWeightQ = UQ<1, 4>;
> +using MeasureCoeffQ = UQ<1, 7>;

I'm not sure these make the code any easier to read

> +
> +LOG_DEFINE_CATEGORY(RppX1Agc)
> +
> +int Agc::parseMeteringModes(IPAContext &context, const ValueNode &tuningData)
> +{

One more reason to move metering modes parsing to libipa :)

> +	if (!tuningData.isDictionary())
> +		LOG(RppX1Agc, Warning)
> +			<< "'AeMeteringMode' parameter not found in tuning file";
> +
> +	for (const auto &[key, value] : tuningData.asDict()) {
> +		auto it = controls::AeMeteringModeNameValueMap.find(key);
> +		if (it == controls::AeMeteringModeNameValueMap.end()) {
> +			LOG(RppX1Agc, Warning)
> +				<< "Skipping unknown metering mode '" << key << "'";
> +			continue;
> +		}
> +
> +		auto weights = value.get<std::vector<float>>().value_or(utils::defopt);
> +		if (weights.size() != RPPX1_HIST_WEIGHT_GRIDS_SIZE) {
> +			LOG(RppX1Agc, Error)
> +				<< "Invalid 'AeMeteringMode:" << key << "': "
> +				<< "expected " << RPPX1_HIST_WEIGHT_GRIDS_SIZE << " elements, "
> +				<< "got " << weights.size();
> +			return -EINVAL;
> +		}
> +
> +		for (const auto &x : weights) {
> +			if (x < 0 || x > 1) {
> +				LOG(RppX1Agc, Error)
> +					<< "Invalid 'AeMeteringMode:" << key << "' values: "
> +					<< "elements must be in [0;1]";
> +				return -EINVAL;
> +			}
> +		}
> +
> +		meteringModes_.try_emplace(it->second, std::move(weights));
> +	}
> +
> +	if (meteringModes_.empty()) {
> +		LOG(RppX1Agc, Warning)
> +			<< "No metering modes read from tuning file; defaulting to matrix";
> +
> +		meteringModes_.try_emplace(
> +			controls::MeteringMatrix,
> +			RPPX1_HIST_WEIGHT_GRIDS_SIZE, 1.0f);
> +	}
> +
> +	std::vector<ControlValue> meteringModes;
> +	std::vector<int> meteringModeKeys = utils::map_keys(meteringModes_);
> +	std::transform(meteringModeKeys.begin(), meteringModeKeys.end(),
> +		       std::back_inserter(meteringModes),
> +		       [](int x) { return ControlValue(x); });
> +	context.ctrlMap[&controls::AeMeteringMode] = ControlInfo(meteringModes);
> +
> +	return 0;
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::init
> + */
> +int Agc::init(IPAContext &context, const ValueNode &tuningData)
> +{
> +	int ret;
> +
> +	ret = agc_.init(tuningData, context.camHelper.get(), {
> +		.sensorInfo = context.sensorInfo,
> +		.sensorControls = context.sensorControls,
> +		.ctrlMap = context.ctrlMap,
> +	});
> +	if (ret)
> +		return ret;
> +
> +	const ValueNode &meteringModes = tuningData["AeMeteringMode"];
> +	ret = parseMeteringModes(context, meteringModes);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::configure
> + */
> +int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
> +{
> +	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
> +		.sensorInfo = context.sensorInfo,
> +		.sensorControls = context.sensorControls,
> +		.ctrlMap = context.ctrlMap,
> +	});
> +	if (ret)
> +		return ret;
> +
> +	context.activeState.agc.meteringMode =
> +		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
> +
> +	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;
> +
> +	return 0;
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::queueRequest
> + */
> +void Agc::queueRequest(IPAContext &context,
> +		       [[maybe_unused]] const uint32_t frame,
> +		       IPAFrameContext &frameContext,
> +		       const ControlList &controls)
> +{
> +	auto &agc = context.activeState.agc;
> +
> +	agc_.queueRequest(context.configuration.agc, agc, frameContext.agc, controls);
> +
> +	const auto &meteringMode = controls.get(controls::AeMeteringMode);
> +	if (meteringMode) {
> +		frameContext.agc.updateMetering = agc.meteringMode != *meteringMode;
> +		agc.meteringMode =
> +			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
> +	}
> +	frameContext.agc.meteringMode = agc.meteringMode;
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::prepare
> + */
> +void Agc::prepare(IPAContext &context, const uint32_t frame,
> +		  IPAFrameContext &frameContext, RppX1Params *params)
> +{
> +	agc_.prepare(context.activeState.agc, frameContext.agc);
> +
> +	if (frame > 0 && !frameContext.agc.updateMetering)
> +		return;
> +
> +	/*
> +	 * Configure the AEC measurements. Set the window, measure
> +	 * continuously, and estimate Y as (R + G + B) x (85/256).
> +	 */
> +	auto exmConfig = params->block<BlockType::ExmPre1>();
> +	exmConfig.setEnabled(true);
> +
> +	/* Jacopo:

Oh clear leftover from early developments :)

> +	 * we decided to let the driver calculate the sub-window sizes.
> +	 *
> +	 * I'm under the impression that all the rounding that happens in the
> +	 * driver restrict the window enough to work with the limitation of
> +	 * of measuring after denoise (Note 4 page 159).

I think this can be dropped now


> +	 */
> +
> +	exmConfig->wnd = context.configuration.agc.measureWindow;
> +	exmConfig->mode = RPPX1_EXP_MEASURING_MODE_BAYER;
> +
> +	/*
> +	 * Jacopo
> +	 *


> +	 * RPPX1: set also channel (after denoise) and gains (x1.0) they were
> +	 * harcoded in the driver.
> +	 *

As RPPX1_MEAS_CHAN_SEL6 doesn't clearly tell what the tap point is I
would still mention in the comment we sample the histogram after the
denoise block

> +	 * last_line can be programmed but the driver does that.

That's a driver's choice, worth mentioning it if anyone decides to
change it later ?

> +	 */
> +	exmConfig->channel_sel = RPPX1_MEAS_CHAN_SEL6;
> +	exmConfig->coeff_r = MeasureCoeffQ(1.f).quantized();
> +	exmConfig->coeff_g_gr = MeasureCoeffQ(1.f).quantized();
> +	exmConfig->coeff_b = MeasureCoeffQ(1.f).quantized();
> +	exmConfig->coeff_gb = MeasureCoeffQ(1.f).quantized(); /* Unused in RGB mode. */
> +
> +	auto hst = params->block<BlockType::HistPost>();
> +	hst.setEnabled(true);
> +
> +	/*
> +	 * Configure the histogram measurement. Set the window, produce a
> +	 * luminance histogram, and set the weights and predivider.
> +	 *
> +	 * RPP-X1: we decided to let the driver calculate the sub-window sizes..
> +	 * Also, we can program last_line, but the driver already does that
> +	 */

This comment can be dropped.

> +	hst->wnd = context.configuration.agc.measureWindow;
> +
> +	/*
> +	 * In RGB mode, a linear combination of the RGB components is used
> +	 * for the histogram. Use the BT.601 RGB -> Y coefficients.
> +	 *
> +	 * \todo is this OK?!

I really hope so, why ?

> +	 */
> +	hst->mode = RPPX1_HIST_MODE_RGB_COMBINED;
> +	hst->coeff[0] = MeasureCoeffQ(0.299f).quantized();
> +	hst->coeff[1] = MeasureCoeffQ(0.587f).quantized();
> +	hst->coeff[2] = MeasureCoeffQ(0.114f).quantized();
> +
> +	/*
> +	 * Jacopo:
> +	 *
> +	 * RPP-X1: we also have to select post-debayer as the histogram tap

You should drop my name and all the "RPP-X1:" from comments. I used
them to highlight things that we specific to rppx1 during developments

> +	 * point as we want to operate on RGB data.
> +	 * It was hardcoded in the kernel driver.

In example this comments should become

	/*
	 * Select post-debayer as the histogram tap point as we want to operate
	 * on RGB data.
	 */

> +	 */
> +	hst->channel_sel = RPPX1_MEAS_CHAN_SEL7;
> +
> +	std::span<const float> weights = meteringModes_.at(frameContext.agc.meteringMode);
> +	ASSERT(weights.size() == std::size(hst->weights));
> +	for (size_t i = 0; i < weights.size(); i++) {
> +		WindowWeightQ w = weights[i];
> +		hst->weights[i] = w.quantized();
> +
> +		LOG(RppX1Agc, Debug)
> +			<< "weights[" << i << "]: " << w;
> +	}
> +
> +	/*
> +	 * Each bin has a 16-bit integral component. So the maximum number in it
> +	 * is 65535. The worst case is when all pixels are in the same bin.
> +	 *
> +	 * To ensure no overflow even in that case, one pixel may be sampled out
> +	 * of every `skip` pixel.
> +	 */
> +	constexpr uint32_t kBinMax = (1u << 16) - 1;
> +	const double skip = double(hst->wnd.h_size * hst->wnd.v_size) / kBinMax;
> +
> +	/*
> +	 * There is a separate vertical/horizontal step setting,
> +	 * but let's try to find something square-ish.
> +	 *
> +	 * Subsampling by `prediv` vertically and horizontally should
> +	 * ensure that no more that `kBinMax` pixels are sampled.
> +	 *
> +	 * \todo Take weights into account?
> +	 */
> +	const auto prediv = std::clamp<uint8_t>(std::ceil(std::sqrt(skip)), 1, 127);
> +	hst->v_stepsize = prediv;
> +	hst->h_step_inc = 65536 / prediv;

This has been reimplemented compared to the previous version but if
I'm not mistaken the factor of 3 used to compute prediv has been lost.

	int count = mode == RKISP1_CIF_ISP_HISTOGRAM_MODE_RGB_COMBINED ? 3 : 1;

Have you verfied the predive calculation results match the existing
code ?




> +
> +	/* RPP-X1: sample shift/offset to 0 */

You can drop this

> +	hst->sample_offs = 0;
> +	hst->sample_shift = 0;
> +
> +	LOG(RppX1Agc, Debug)
> +		<< "window: " << Rectangle(hst->wnd.h_offs, hst->wnd.v_offs, hst->wnd.h_size, hst->wnd.v_size) << ", "
> +		<< "v_stepsize: " << hst->v_stepsize << ", "
> +		<< "h_step_inc: " << hst->h_step_inc;
> +}
> +
> +namespace {
> +
> +class AgcTraits final : public AgcMeanLuminance::Traits
> +{
> +public:
> +	AgcTraits(std::span<const uint32_t> expMeans, std::span<const float> weights)
> +		: expMeans_(expMeans), weights_(weights)
> +	{
> +		ASSERT(expMeans_.size() == weights_.size());
> +	}
> +
> +	/**
> +	 * \brief Estimate the relative luminance of the frame with a given gain
> +	 * \param[in] gain The gain to apply to the frame
> +	 *
> +	 * This function estimates the average relative luminance of the frame that
> +	 * would be output by the sensor if an additional \a gain was applied.
> +	 *
> +	 * The estimation is based on the AE statistics for the current frame. Y
> +	 * averages for all cells are first multiplied by the gain, and then saturated
> +	 * to approximate the sensor behaviour at high brightness values. The
> +	 * approximation is quite rough, as it doesn't take into account non-linearities
> +	 * when approaching saturation. In this case, saturating after the conversion to
> +	 * YUV doesn't take into account the fact that the R, G and B components
> +	 * contribute differently to the relative luminance.
> +	 *
> +	 * The values are normalized to the [0.0, 1.0] range, where 1.0 corresponds to a
> +	 * theoretical perfect reflector of 100% reference white.
> +	 *
> +	 * More detailed information can be found in:
> +	 * https://en.wikipedia.org/wiki/Relative_luminance
> +	 *
> +	 * \return The relative luminance
> +	 */
> +	double estimateLuminance(double gain) const override
> +	{
> +		constexpr double kMaxExpMean = (1u << 20) - 1;
> +
> +		double ySum = 0.0;
> +		double wSum = 0.0;
> +
> +		/* Sum the averages, saturated to 255. */
> +		for (unsigned i = 0; i < expMeans_.size(); i++) {
> +			double w = weights_[i];
> +			ySum += std::min<double>(expMeans_[i] * gain, kMaxExpMean) * w;
> +			wSum += w;
> +		}
> +
> +		/* \todo Weight with the AWB gains */
> +
> +		return ySum / wSum / kMaxExpMean;
> +	}
> +
> +private:
> +	std::span<const uint32_t> expMeans_;
> +	std::span<const float> weights_;
> +};
> +
> +} /* namespace */
> +
> +/**
> + * \brief Process RppX1 statistics, and run AGC operations
> + * \param[in] context The shared IPA context
> + * \param[in] frame The frame context sequence number
> + * \param[in] frameContext The current frame context
> + * \param[in] stats The RPP-X1 statistics and ISP results
> + * \param[out] metadata Metadata for the frame, to be filled by the algorithm
> + *
> + * Identify the current image brightness, and use that to estimate the optimal
> + * new exposure and gain for the scene.
> + */
> +void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
> +		  IPAFrameContext &frameContext, const RppX1Stats *stats,
> +		  ControlList &metadata)
> +{
> +	const auto histPost = stats->block<StatsType::HistPost>();
> +	const auto exmPre1 = stats->block<StatsType::ExmPre1>();
> +
> +	if (!histPost)
> +		LOG(RppX1Agc, Error) << "HIST_POST data is missing in statistics";
> +	if (!exmPre1)
> +		LOG(RppX1Agc, Error) << "EXM_PRE1 data is missing in statistics";
> +
> +	if (histPost && exmPre1) {
> +		static_assert(RPPX1_EXM_NUM_WIN == RPPX1_HIST_WEIGHT_GRIDS_SIZE);
> +
> +		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
> +			.traits = AgcTraits{
> +				exmPre1->exp_mean,
> +				meteringModes_.at(frameContext.agc.meteringMode),
> +			},
> +			.yHist = {
> +				/* The lower 4 bits are fractional and meant to be discarded. */
> +				histPost->hist_bins,
> +				[](uint32_t x) { return x >> 4; },
> +			},
> +			.exposure = frameContext.sensor.exposure,
> +			.gain = frameContext.sensor.gain,
> +		}}, metadata);
> +	} else {
> +		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
> +	}
> +
> +	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
> +}
> +
> +REGISTER_IPA_ALGORITHM(Agc, "Agc")
> +
> +} /* namespace ipa::rppx1::algorithms */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/rppx1/algorithms/agc.h b/src/ipa/rppx1/algorithms/agc.h
> new file mode 100644
> index 0000000000..1b3c55681a
> --- /dev/null
> +++ b/src/ipa/rppx1/algorithms/agc.h
> @@ -0,0 +1,47 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Rpp-X1 AGC/AEC algorithm
> + */
> +
> +#pragma once
> +
> +#include <map>
> +#include <vector>
> +
> +#include "libipa/agc.h"
> +
> +#include "algorithm.h"
> +
> +namespace libcamera {
> +
> +namespace ipa::rppx1::algorithms {
> +
> +class Agc : public Algorithm
> +{
> +public:
> +	int init(IPAContext &context, const ValueNode &tuningData) override;
> +	int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;
> +	void queueRequest(IPAContext &context,
> +			  const uint32_t frame,
> +			  IPAFrameContext &frameContext,
> +			  const ControlList &controls) override;
> +	void prepare(IPAContext &context, const uint32_t frame,
> +		     IPAFrameContext &frameContext,
> +		     RppX1Params *params) override;
> +	void process(IPAContext &context, const uint32_t frame,
> +		     IPAFrameContext &frameContext,
> +		     const RppX1Stats *stats,
> +		     ControlList &metadata) override;
> +
> +private:
> +	int parseMeteringModes(IPAContext &context, const ValueNode &tuningData);
> +
> +	std::map<int32_t, std::vector<float>> meteringModes_;
> +	AgcAlgorithm agc_;
> +};
> +
> +} /* namespace ipa::rppx1::algorithms */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build
> index 165f0ce054..394ea9510b 100644
> --- a/src/ipa/rppx1/algorithms/meson.build
> +++ b/src/ipa/rppx1/algorithms/meson.build
> @@ -1,5 +1,6 @@
>  # SPDX-License-Identifier: CC0-1.0
>
>  rppx1_ipa_algorithms = files([
> +    'agc.cpp',
>      'blc.cpp',
>  ])
> diff --git a/src/ipa/rppx1/ipa_context.h b/src/ipa/rppx1/ipa_context.h
> index 079fcab782..f6408e73bb 100644
> --- a/src/ipa/rppx1/ipa_context.h
> +++ b/src/ipa/rppx1/ipa_context.h
> @@ -9,14 +9,14 @@
>
>  #include <memory>
>
> -#include <libcamera/base/utils.h>
> +#include <linux/media/dreamchip/rppx1-config.h>
>
>  #include <libcamera/control_ids.h>
>  #include <libcamera/controls.h>
> -#include <libcamera/geometry.h>
>
>  #include <libcamera/ipa/core_ipa_interface.h>
>
> +#include <libipa/agc.h>
>  #include <libipa/camera_sensor_helper.h>
>  #include <libipa/fc_queue.h>
>
> @@ -25,12 +25,27 @@ namespace libcamera {
>  namespace ipa::rppx1 {
>
>  struct IPASessionConfiguration {
> +	struct Agc : ipa::agc::Session {
> +		rppx1_window measureWindow;
> +	} agc;
>  };
>
>  struct IPAActiveState {
> +	struct Agc : ipa::agc::ActiveState {
> +		controls::AeMeteringModeEnum meteringMode;
> +	} agc;
>  };
>
>  struct IPAFrameContext : public FrameContext {
> +	struct {
> +		uint32_t exposure;
> +		double gain;
> +	} sensor;
> +
> +	struct Agc : ipa::agc::FrameContext {
> +		controls::AeMeteringModeEnum meteringMode;
> +		bool updateMetering;
> +	} agc;
>  };
>
>  struct IPAContext {
> diff --git a/src/ipa/rppx1/params.h b/src/ipa/rppx1/params.h
> index 05db309914..45adf43613 100644
> --- a/src/ipa/rppx1/params.h
> +++ b/src/ipa/rppx1/params.h
> @@ -17,6 +17,8 @@ namespace ipa::rppx1 {
>
>  enum class BlockType : uint16_t {
>  	BlsPre1,
> +	ExmPre1,
> +	HistPost,
>  };
>
>  namespace details {
> @@ -34,6 +36,8 @@ struct block_type {
>  	};
>
>  RPPX1_DEFINE_BLOCK_TYPE(BlsPre1, bls, BLS_PRE1)
> +RPPX1_DEFINE_BLOCK_TYPE(ExmPre1, exm, EXM_PRE1)
> +RPPX1_DEFINE_BLOCK_TYPE(HistPost, hist, HIST_POST)
>
>  struct params_traits {
>  	using id_type = BlockType;
> diff --git a/src/ipa/rppx1/rppx1.cpp b/src/ipa/rppx1/rppx1.cpp
> index 23ab8391b3..78c9370b29 100644
> --- a/src/ipa/rppx1/rppx1.cpp
> +++ b/src/ipa/rppx1/rppx1.cpp
> @@ -27,6 +27,8 @@
>  #include "libcamera/internal/mapped_framebuffer.h"
>  #include "libcamera/internal/yaml_parser.h"
>
> +#include <libipa/agc.h>
> +
>  #include "algorithms/algorithm.h"
>
>  #include "ipa_context.h"
> @@ -229,6 +231,9 @@ void IPARppX1::processStats(const uint32_t frame, const uint32_t bufferId,
>  	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
>  	ControlList metadata(controls::controls);
>
> +	std::tie(frameContext.sensor.exposure, frameContext.sensor.gain) =
> +		agc::extractControls(sensorControls, context_.camHelper.get());
> +
>  	for (auto const &a : algorithms()) {
>  		Algorithm *algo = static_cast<Algorithm *>(a.get());
>  		algo->process(context_, frame, frameContext, &stats, metadata);
> @@ -249,9 +254,19 @@ void IPARppX1::updateControls(ControlInfoMap *ipaControls)
>
>  void IPARppX1::setControls(unsigned int frame)
>  {
> -	[[maybe_unused]] IPAFrameContext &frameContext = context_.frameContexts.get(frame);
> +	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
> +
> +	uint32_t exposure = frameContext.agc.exposure;
> +	uint32_t vblank = frameContext.agc.vblank;
> +
> +	LOG(IPARppX1, Debug)
> +		<< "Set controls for frame " << frame << ": exposure " << exposure
> +		<< ", gain " << frameContext.agc.gain << ", vblank " << vblank;
>
>  	ControlList ctrls(context_.sensorControls);
> +	agc::prepareControls(ctrls, context_.camHelper.get(),
> +			     exposure, frameContext.agc.gain);
> +	ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank));
>
>  	setSensorControls.emit(frame, ctrls);
>  }
> diff --git a/src/ipa/rppx1/stats.h b/src/ipa/rppx1/stats.h
> index 39dc132bac..2925e2bbfc 100644
> --- a/src/ipa/rppx1/stats.h
> +++ b/src/ipa/rppx1/stats.h
> @@ -16,6 +16,8 @@ namespace libcamera {
>  namespace ipa::rppx1 {
>
>  enum class StatsType : uint16_t {
> +	ExmPre1,
> +	HistPost,
>  };
>
>  namespace details {
> @@ -32,6 +34,9 @@ struct stats_type {
>  			RPPX1_STATS_BLOCK_TYPE_##id;			\
>  	};
>
> +RPPX1_DEFINE_STATS_TYPE(ExmPre1, exm, EXM_PRE1)
> +RPPX1_DEFINE_STATS_TYPE(HistPost, hist, HIST_POST)
> +
>  struct stats_traits {
>  	using id_type = StatsType;
>
> --
> 2.55.0
>
Barnabás Pőcze Sept. 25, 2026, 1:02 p.m. UTC | #2
2026. 09. 24. 17:21 keltezéssel, Jacopo Mondi írta:
> Hi Barnabás
> 
> On Fri, Sep 18, 2026 at 02:09:42PM +0200, Barnabás Pőcze wrote:
>> From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>>
>> Add the algorithm to the rppx1 ipa module based on the corresponding
>> algorithm in the rkisp1 ipa module.
>>
>> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>> ---
>>   src/ipa/rppx1/algorithms/agc.cpp     | 391 +++++++++++++++++++++++++++
>>   src/ipa/rppx1/algorithms/agc.h       |  47 ++++
>>   src/ipa/rppx1/algorithms/meson.build |   1 +
>>   src/ipa/rppx1/ipa_context.h          |  19 +-
>>   src/ipa/rppx1/params.h               |   4 +
>>   src/ipa/rppx1/rppx1.cpp              |  17 +-
>>   src/ipa/rppx1/stats.h                |   5 +
>>   7 files changed, 481 insertions(+), 3 deletions(-)
>>   create mode 100644 src/ipa/rppx1/algorithms/agc.cpp
>>   create mode 100644 src/ipa/rppx1/algorithms/agc.h
>>
>> diff --git a/src/ipa/rppx1/algorithms/agc.cpp b/src/ipa/rppx1/algorithms/agc.cpp
>> new file mode 100644
>> index 0000000000..fd782cea3d
>> --- /dev/null
>> +++ b/src/ipa/rppx1/algorithms/agc.cpp
>> @@ -0,0 +1,391 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2026, Ideas On Board
>> + *
>> + * AGC/AEC mean-based control algorithm
>> + */
>> +
>> +#include "agc.h"
>> +
>> +#include <algorithm>
>> +#include <cmath>
>> +#include <iterator>
>> +#include <vector>
>> +
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/utils.h>
>> +
>> +#include <libcamera/control_ids.h>
>> +#include <libcamera/geometry.h>
>> +
>> +#include <libcamera/ipa/core_ipa_interface.h>
>> +
>> +#include "libcamera/internal/value_node.h"
>> +
>> +#include "libipa/fixedpoint.h"
>> +#include "libipa/histogram.h"
>> +
>> +/**
>> + * \file agc.h
>> + */
>> +
>> +namespace libcamera {
>> +
>> +namespace ipa::rppx1::algorithms {
>> +
>> +using WindowWeightQ = UQ<1, 4>;
>> +using MeasureCoeffQ = UQ<1, 7>;
> 
> I'm not sure these make the code any easier to read
> 
>> +
>> +LOG_DEFINE_CATEGORY(RppX1Agc)
>> +
>> +int Agc::parseMeteringModes(IPAContext &context, const ValueNode &tuningData)
>> +{
> 
> One more reason to move metering modes parsing to libipa :)
> 
>> +	if (!tuningData.isDictionary())
>> +		LOG(RppX1Agc, Warning)
>> +			<< "'AeMeteringMode' parameter not found in tuning file";
>> +
>> +	for (const auto &[key, value] : tuningData.asDict()) {
>> +		auto it = controls::AeMeteringModeNameValueMap.find(key);
>> +		if (it == controls::AeMeteringModeNameValueMap.end()) {
>> +			LOG(RppX1Agc, Warning)
>> +				<< "Skipping unknown metering mode '" << key << "'";
>> +			continue;
>> +		}
>> +
>> +		auto weights = value.get<std::vector<float>>().value_or(utils::defopt);
>> +		if (weights.size() != RPPX1_HIST_WEIGHT_GRIDS_SIZE) {
>> +			LOG(RppX1Agc, Error)
>> +				<< "Invalid 'AeMeteringMode:" << key << "': "
>> +				<< "expected " << RPPX1_HIST_WEIGHT_GRIDS_SIZE << " elements, "
>> +				<< "got " << weights.size();
>> +			return -EINVAL;
>> +		}
>> +
>> +		for (const auto &x : weights) {
>> +			if (x < 0 || x > 1) {
>> +				LOG(RppX1Agc, Error)
>> +					<< "Invalid 'AeMeteringMode:" << key << "' values: "
>> +					<< "elements must be in [0;1]";
>> +				return -EINVAL;
>> +			}
>> +		}
>> +
>> +		meteringModes_.try_emplace(it->second, std::move(weights));
>> +	}
>> +
>> +	if (meteringModes_.empty()) {
>> +		LOG(RppX1Agc, Warning)
>> +			<< "No metering modes read from tuning file; defaulting to matrix";
>> +
>> +		meteringModes_.try_emplace(
>> +			controls::MeteringMatrix,
>> +			RPPX1_HIST_WEIGHT_GRIDS_SIZE, 1.0f);
>> +	}
>> +
>> +	std::vector<ControlValue> meteringModes;
>> +	std::vector<int> meteringModeKeys = utils::map_keys(meteringModes_);
>> +	std::transform(meteringModeKeys.begin(), meteringModeKeys.end(),
>> +		       std::back_inserter(meteringModes),
>> +		       [](int x) { return ControlValue(x); });
>> +	context.ctrlMap[&controls::AeMeteringMode] = ControlInfo(meteringModes);
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * \copydoc libcamera::ipa::Algorithm::init
>> + */
>> +int Agc::init(IPAContext &context, const ValueNode &tuningData)
>> +{
>> +	int ret;
>> +
>> +	ret = agc_.init(tuningData, context.camHelper.get(), {
>> +		.sensorInfo = context.sensorInfo,
>> +		.sensorControls = context.sensorControls,
>> +		.ctrlMap = context.ctrlMap,
>> +	});
>> +	if (ret)
>> +		return ret;
>> +
>> +	const ValueNode &meteringModes = tuningData["AeMeteringMode"];
>> +	ret = parseMeteringModes(context, meteringModes);
>> +	if (ret)
>> +		return ret;
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * \copydoc libcamera::ipa::Algorithm::configure
>> + */
>> +int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
>> +{
>> +	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
>> +		.sensorInfo = context.sensorInfo,
>> +		.sensorControls = context.sensorControls,
>> +		.ctrlMap = context.ctrlMap,
>> +	});
>> +	if (ret)
>> +		return ret;
>> +
>> +	context.activeState.agc.meteringMode =
>> +		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
>> +
>> +	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;
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * \copydoc libcamera::ipa::Algorithm::queueRequest
>> + */
>> +void Agc::queueRequest(IPAContext &context,
>> +		       [[maybe_unused]] const uint32_t frame,
>> +		       IPAFrameContext &frameContext,
>> +		       const ControlList &controls)
>> +{
>> +	auto &agc = context.activeState.agc;
>> +
>> +	agc_.queueRequest(context.configuration.agc, agc, frameContext.agc, controls);
>> +
>> +	const auto &meteringMode = controls.get(controls::AeMeteringMode);
>> +	if (meteringMode) {
>> +		frameContext.agc.updateMetering = agc.meteringMode != *meteringMode;
>> +		agc.meteringMode =
>> +			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
>> +	}
>> +	frameContext.agc.meteringMode = agc.meteringMode;
>> +}
>> +
>> +/**
>> + * \copydoc libcamera::ipa::Algorithm::prepare
>> + */
>> +void Agc::prepare(IPAContext &context, const uint32_t frame,
>> +		  IPAFrameContext &frameContext, RppX1Params *params)
>> +{
>> +	agc_.prepare(context.activeState.agc, frameContext.agc);
>> +
>> +	if (frame > 0 && !frameContext.agc.updateMetering)
>> +		return;
>> +
>> +	/*
>> +	 * Configure the AEC measurements. Set the window, measure
>> +	 * continuously, and estimate Y as (R + G + B) x (85/256).
>> +	 */
>> +	auto exmConfig = params->block<BlockType::ExmPre1>();
>> +	exmConfig.setEnabled(true);
>> +
>> +	/* Jacopo:
> 
> Oh clear leftover from early developments :)
> 
>> +	 * we decided to let the driver calculate the sub-window sizes.
>> +	 *
>> +	 * I'm under the impression that all the rounding that happens in the
>> +	 * driver restrict the window enough to work with the limitation of
>> +	 * of measuring after denoise (Note 4 page 159).
> 
> I think this can be dropped now
> 
> 
>> +	 */
>> +
>> +	exmConfig->wnd = context.configuration.agc.measureWindow;
>> +	exmConfig->mode = RPPX1_EXP_MEASURING_MODE_BAYER;
>> +
>> +	/*
>> +	 * Jacopo
>> +	 *
> 
> 
>> +	 * RPPX1: set also channel (after denoise) and gains (x1.0) they were
>> +	 * harcoded in the driver.
>> +	 *
> 
> As RPPX1_MEAS_CHAN_SEL6 doesn't clearly tell what the tap point is I
> would still mention in the comment we sample the histogram after the
> denoise block

It's documented in the kernel header, so I think it's fine?


> 
>> +	 * last_line can be programmed but the driver does that.
> 
> That's a driver's choice, worth mentioning it if anyone decides to
> change it later ?
> 
>> +	 */
>> +	exmConfig->channel_sel = RPPX1_MEAS_CHAN_SEL6;
>> +	exmConfig->coeff_r = MeasureCoeffQ(1.f).quantized();
>> +	exmConfig->coeff_g_gr = MeasureCoeffQ(1.f).quantized();
>> +	exmConfig->coeff_b = MeasureCoeffQ(1.f).quantized();
>> +	exmConfig->coeff_gb = MeasureCoeffQ(1.f).quantized(); /* Unused in RGB mode. */
>> +
>> +	auto hst = params->block<BlockType::HistPost>();
>> +	hst.setEnabled(true);
>> +
>> +	/*
>> +	 * Configure the histogram measurement. Set the window, produce a
>> +	 * luminance histogram, and set the weights and predivider.
>> +	 *
>> +	 * RPP-X1: we decided to let the driver calculate the sub-window sizes..
>> +	 * Also, we can program last_line, but the driver already does that
>> +	 */
> 
> This comment can be dropped.
> 
>> +	hst->wnd = context.configuration.agc.measureWindow;
>> +
>> +	/*
>> +	 * In RGB mode, a linear combination of the RGB components is used
>> +	 * for the histogram. Use the BT.601 RGB -> Y coefficients.
>> +	 *
>> +	 * \todo is this OK?!
> 
> I really hope so, why ?

I think so too, just wanted to bring some attention to it, maybe
someone has some comments.


> 
>> +	 */
>> +	hst->mode = RPPX1_HIST_MODE_RGB_COMBINED;
>> +	hst->coeff[0] = MeasureCoeffQ(0.299f).quantized();
>> +	hst->coeff[1] = MeasureCoeffQ(0.587f).quantized();
>> +	hst->coeff[2] = MeasureCoeffQ(0.114f).quantized();
>> +
>> +	/*
>> +	 * Jacopo:
>> +	 *
>> +	 * RPP-X1: we also have to select post-debayer as the histogram tap
> 
> You should drop my name and all the "RPP-X1:" from comments. I used
> them to highlight things that we specific to rppx1 during developments
> 
>> +	 * point as we want to operate on RGB data.
>> +	 * It was hardcoded in the kernel driver.
> 
> In example this comments should become
> 
> 	/*
> 	 * Select post-debayer as the histogram tap point as we want to operate
> 	 * on RGB data.
> 	 */
> 
>> +	 */
>> +	hst->channel_sel = RPPX1_MEAS_CHAN_SEL7;
>> +
>> +	std::span<const float> weights = meteringModes_.at(frameContext.agc.meteringMode);
>> +	ASSERT(weights.size() == std::size(hst->weights));
>> +	for (size_t i = 0; i < weights.size(); i++) {
>> +		WindowWeightQ w = weights[i];
>> +		hst->weights[i] = w.quantized();
>> +
>> +		LOG(RppX1Agc, Debug)
>> +			<< "weights[" << i << "]: " << w;
>> +	}
>> +
>> +	/*
>> +	 * Each bin has a 16-bit integral component. So the maximum number in it
>> +	 * is 65535. The worst case is when all pixels are in the same bin.
>> +	 *
>> +	 * To ensure no overflow even in that case, one pixel may be sampled out
>> +	 * of every `skip` pixel.
>> +	 */
>> +	constexpr uint32_t kBinMax = (1u << 16) - 1;
>> +	const double skip = double(hst->wnd.h_size * hst->wnd.v_size) / kBinMax;
>> +
>> +	/*
>> +	 * There is a separate vertical/horizontal step setting,
>> +	 * but let's try to find something square-ish.
>> +	 *
>> +	 * Subsampling by `prediv` vertically and horizontally should
>> +	 * ensure that no more that `kBinMax` pixels are sampled.
>> +	 *
>> +	 * \todo Take weights into account?
>> +	 */
>> +	const auto prediv = std::clamp<uint8_t>(std::ceil(std::sqrt(skip)), 1, 127);
>> +	hst->v_stepsize = prediv;
>> +	hst->h_step_inc = 65536 / prediv;
> 
> This has been reimplemented compared to the previous version but if
> I'm not mistaken the factor of 3 used to compute prediv has been lost.
> 
> 	int count = mode == RKISP1_CIF_ISP_HISTOGRAM_MODE_RGB_COMBINED ? 3 : 1;
> 
> Have you verfied the predive calculation results match the existing
> code ?

I believe it not to be necessary because the coefficients sum to 1, meaning that
each rgb triplet can contribute at most 1 to a bin.


> 
> 
> 
> 
>> +
>> +	/* RPP-X1: sample shift/offset to 0 */
> 
> You can drop this
> 
>> +	hst->sample_offs = 0;
>> +	hst->sample_shift = 0;
>> +
>> +	LOG(RppX1Agc, Debug)
>> +		<< "window: " << Rectangle(hst->wnd.h_offs, hst->wnd.v_offs, hst->wnd.h_size, hst->wnd.v_size) << ", "
>> +		<< "v_stepsize: " << hst->v_stepsize << ", "
>> +		<< "h_step_inc: " << hst->h_step_inc;
>> +}
>> +
>> +namespace {
>> +
>> +class AgcTraits final : public AgcMeanLuminance::Traits
>> +{
>> +public:
>> +	AgcTraits(std::span<const uint32_t> expMeans, std::span<const float> weights)
>> +		: expMeans_(expMeans), weights_(weights)
>> +	{
>> +		ASSERT(expMeans_.size() == weights_.size());
>> +	}
>> +
>> +	/**
>> +	 * \brief Estimate the relative luminance of the frame with a given gain
>> +	 * \param[in] gain The gain to apply to the frame
>> +	 *
>> +	 * This function estimates the average relative luminance of the frame that
>> +	 * would be output by the sensor if an additional \a gain was applied.
>> +	 *
>> +	 * The estimation is based on the AE statistics for the current frame. Y
>> +	 * averages for all cells are first multiplied by the gain, and then saturated
>> +	 * to approximate the sensor behaviour at high brightness values. The
>> +	 * approximation is quite rough, as it doesn't take into account non-linearities
>> +	 * when approaching saturation. In this case, saturating after the conversion to
>> +	 * YUV doesn't take into account the fact that the R, G and B components
>> +	 * contribute differently to the relative luminance.
>> +	 *
>> +	 * The values are normalized to the [0.0, 1.0] range, where 1.0 corresponds to a
>> +	 * theoretical perfect reflector of 100% reference white.
>> +	 *
>> +	 * More detailed information can be found in:
>> +	 * https://en.wikipedia.org/wiki/Relative_luminance
>> +	 *
>> +	 * \return The relative luminance
>> +	 */
>> +	double estimateLuminance(double gain) const override
>> +	{
>> +		constexpr double kMaxExpMean = (1u << 20) - 1;
>> +
>> +		double ySum = 0.0;
>> +		double wSum = 0.0;
>> +
>> +		/* Sum the averages, saturated to 255. */
>> +		for (unsigned i = 0; i < expMeans_.size(); i++) {
>> +			double w = weights_[i];
>> +			ySum += std::min<double>(expMeans_[i] * gain, kMaxExpMean) * w;
>> +			wSum += w;
>> +		}
>> +
>> +		/* \todo Weight with the AWB gains */
>> +
>> +		return ySum / wSum / kMaxExpMean;
>> +	}
>> +
>> +private:
>> +	std::span<const uint32_t> expMeans_;
>> +	std::span<const float> weights_;
>> +};
>> +
>> +} /* namespace */
>> +
>> +/**
>> + * \brief Process RppX1 statistics, and run AGC operations
>> + * \param[in] context The shared IPA context
>> + * \param[in] frame The frame context sequence number
>> + * \param[in] frameContext The current frame context
>> + * \param[in] stats The RPP-X1 statistics and ISP results
>> + * \param[out] metadata Metadata for the frame, to be filled by the algorithm
>> + *
>> + * Identify the current image brightness, and use that to estimate the optimal
>> + * new exposure and gain for the scene.
>> + */
>> +void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>> +		  IPAFrameContext &frameContext, const RppX1Stats *stats,
>> +		  ControlList &metadata)
>> +{
>> +	const auto histPost = stats->block<StatsType::HistPost>();
>> +	const auto exmPre1 = stats->block<StatsType::ExmPre1>();
>> +
>> +	if (!histPost)
>> +		LOG(RppX1Agc, Error) << "HIST_POST data is missing in statistics";
>> +	if (!exmPre1)
>> +		LOG(RppX1Agc, Error) << "EXM_PRE1 data is missing in statistics";
>> +
>> +	if (histPost && exmPre1) {
>> +		static_assert(RPPX1_EXM_NUM_WIN == RPPX1_HIST_WEIGHT_GRIDS_SIZE);
>> +
>> +		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
>> +			.traits = AgcTraits{
>> +				exmPre1->exp_mean,
>> +				meteringModes_.at(frameContext.agc.meteringMode),
>> +			},
>> +			.yHist = {
>> +				/* The lower 4 bits are fractional and meant to be discarded. */
>> +				histPost->hist_bins,
>> +				[](uint32_t x) { return x >> 4; },
>> +			},
>> +			.exposure = frameContext.sensor.exposure,
>> +			.gain = frameContext.sensor.gain,
>> +		}}, metadata);
>> +	} else {
>> +		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
>> +	}
>> +
>> +	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
>> +}
>> +
>> +REGISTER_IPA_ALGORITHM(Agc, "Agc")
>> +
>> +} /* namespace ipa::rppx1::algorithms */
>> +
>> +} /* namespace libcamera */
> [...]

Patch
diff mbox series

diff --git a/src/ipa/rppx1/algorithms/agc.cpp b/src/ipa/rppx1/algorithms/agc.cpp
new file mode 100644
index 0000000000..fd782cea3d
--- /dev/null
+++ b/src/ipa/rppx1/algorithms/agc.cpp
@@ -0,0 +1,391 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * AGC/AEC mean-based control algorithm
+ */
+
+#include "agc.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/geometry.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "libipa/fixedpoint.h"
+#include "libipa/histogram.h"
+
+/**
+ * \file agc.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rppx1::algorithms {
+
+using WindowWeightQ = UQ<1, 4>;
+using MeasureCoeffQ = UQ<1, 7>;
+
+LOG_DEFINE_CATEGORY(RppX1Agc)
+
+int Agc::parseMeteringModes(IPAContext &context, const ValueNode &tuningData)
+{
+	if (!tuningData.isDictionary())
+		LOG(RppX1Agc, Warning)
+			<< "'AeMeteringMode' parameter not found in tuning file";
+
+	for (const auto &[key, value] : tuningData.asDict()) {
+		auto it = controls::AeMeteringModeNameValueMap.find(key);
+		if (it == controls::AeMeteringModeNameValueMap.end()) {
+			LOG(RppX1Agc, Warning)
+				<< "Skipping unknown metering mode '" << key << "'";
+			continue;
+		}
+
+		auto weights = value.get<std::vector<float>>().value_or(utils::defopt);
+		if (weights.size() != RPPX1_HIST_WEIGHT_GRIDS_SIZE) {
+			LOG(RppX1Agc, Error)
+				<< "Invalid 'AeMeteringMode:" << key << "': "
+				<< "expected " << RPPX1_HIST_WEIGHT_GRIDS_SIZE << " elements, "
+				<< "got " << weights.size();
+			return -EINVAL;
+		}
+
+		for (const auto &x : weights) {
+			if (x < 0 || x > 1) {
+				LOG(RppX1Agc, Error)
+					<< "Invalid 'AeMeteringMode:" << key << "' values: "
+					<< "elements must be in [0;1]";
+				return -EINVAL;
+			}
+		}
+
+		meteringModes_.try_emplace(it->second, std::move(weights));
+	}
+
+	if (meteringModes_.empty()) {
+		LOG(RppX1Agc, Warning)
+			<< "No metering modes read from tuning file; defaulting to matrix";
+
+		meteringModes_.try_emplace(
+			controls::MeteringMatrix,
+			RPPX1_HIST_WEIGHT_GRIDS_SIZE, 1.0f);
+	}
+
+	std::vector<ControlValue> meteringModes;
+	std::vector<int> meteringModeKeys = utils::map_keys(meteringModes_);
+	std::transform(meteringModeKeys.begin(), meteringModeKeys.end(),
+		       std::back_inserter(meteringModes),
+		       [](int x) { return ControlValue(x); });
+	context.ctrlMap[&controls::AeMeteringMode] = ControlInfo(meteringModes);
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int Agc::init(IPAContext &context, const ValueNode &tuningData)
+{
+	int ret;
+
+	ret = agc_.init(tuningData, context.camHelper.get(), {
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+	});
+	if (ret)
+		return ret;
+
+	const ValueNode &meteringModes = tuningData["AeMeteringMode"];
+	ret = parseMeteringModes(context, meteringModes);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
+{
+	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+	});
+	if (ret)
+		return ret;
+
+	context.activeState.agc.meteringMode =
+		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
+
+	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;
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void Agc::queueRequest(IPAContext &context,
+		       [[maybe_unused]] const uint32_t frame,
+		       IPAFrameContext &frameContext,
+		       const ControlList &controls)
+{
+	auto &agc = context.activeState.agc;
+
+	agc_.queueRequest(context.configuration.agc, agc, frameContext.agc, controls);
+
+	const auto &meteringMode = controls.get(controls::AeMeteringMode);
+	if (meteringMode) {
+		frameContext.agc.updateMetering = agc.meteringMode != *meteringMode;
+		agc.meteringMode =
+			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
+	}
+	frameContext.agc.meteringMode = agc.meteringMode;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void Agc::prepare(IPAContext &context, const uint32_t frame,
+		  IPAFrameContext &frameContext, RppX1Params *params)
+{
+	agc_.prepare(context.activeState.agc, frameContext.agc);
+
+	if (frame > 0 && !frameContext.agc.updateMetering)
+		return;
+
+	/*
+	 * Configure the AEC measurements. Set the window, measure
+	 * continuously, and estimate Y as (R + G + B) x (85/256).
+	 */
+	auto exmConfig = params->block<BlockType::ExmPre1>();
+	exmConfig.setEnabled(true);
+
+	/* Jacopo:
+	 * we decided to let the driver calculate the sub-window sizes.
+	 *
+	 * I'm under the impression that all the rounding that happens in the
+	 * driver restrict the window enough to work with the limitation of
+	 * of measuring after denoise (Note 4 page 159).
+	 */
+
+	exmConfig->wnd = context.configuration.agc.measureWindow;
+	exmConfig->mode = RPPX1_EXP_MEASURING_MODE_BAYER;
+
+	/*
+	 * Jacopo
+	 *
+	 * RPPX1: set also channel (after denoise) and gains (x1.0) they were
+	 * harcoded in the driver.
+	 *
+	 * last_line can be programmed but the driver does that.
+	 */
+	exmConfig->channel_sel = RPPX1_MEAS_CHAN_SEL6;
+	exmConfig->coeff_r = MeasureCoeffQ(1.f).quantized();
+	exmConfig->coeff_g_gr = MeasureCoeffQ(1.f).quantized();
+	exmConfig->coeff_b = MeasureCoeffQ(1.f).quantized();
+	exmConfig->coeff_gb = MeasureCoeffQ(1.f).quantized(); /* Unused in RGB mode. */
+
+	auto hst = params->block<BlockType::HistPost>();
+	hst.setEnabled(true);
+
+	/*
+	 * Configure the histogram measurement. Set the window, produce a
+	 * luminance histogram, and set the weights and predivider.
+	 *
+	 * RPP-X1: we decided to let the driver calculate the sub-window sizes..
+	 * Also, we can program last_line, but the driver already does that
+	 */
+	hst->wnd = context.configuration.agc.measureWindow;
+
+	/*
+	 * In RGB mode, a linear combination of the RGB components is used
+	 * for the histogram. Use the BT.601 RGB -> Y coefficients.
+	 *
+	 * \todo is this OK?!
+	 */
+	hst->mode = RPPX1_HIST_MODE_RGB_COMBINED;
+	hst->coeff[0] = MeasureCoeffQ(0.299f).quantized();
+	hst->coeff[1] = MeasureCoeffQ(0.587f).quantized();
+	hst->coeff[2] = MeasureCoeffQ(0.114f).quantized();
+
+	/*
+	 * Jacopo:
+	 *
+	 * RPP-X1: we also have to select post-debayer as the histogram tap
+	 * point as we want to operate on RGB data.
+	 * It was hardcoded in the kernel driver.
+	 */
+	hst->channel_sel = RPPX1_MEAS_CHAN_SEL7;
+
+	std::span<const float> weights = meteringModes_.at(frameContext.agc.meteringMode);
+	ASSERT(weights.size() == std::size(hst->weights));
+	for (size_t i = 0; i < weights.size(); i++) {
+		WindowWeightQ w = weights[i];
+		hst->weights[i] = w.quantized();
+
+		LOG(RppX1Agc, Debug)
+			<< "weights[" << i << "]: " << w;
+	}
+
+	/*
+	 * Each bin has a 16-bit integral component. So the maximum number in it
+	 * is 65535. The worst case is when all pixels are in the same bin.
+	 *
+	 * To ensure no overflow even in that case, one pixel may be sampled out
+	 * of every `skip` pixel.
+	 */
+	constexpr uint32_t kBinMax = (1u << 16) - 1;
+	const double skip = double(hst->wnd.h_size * hst->wnd.v_size) / kBinMax;
+
+	/*
+	 * There is a separate vertical/horizontal step setting,
+	 * but let's try to find something square-ish.
+	 *
+	 * Subsampling by `prediv` vertically and horizontally should
+	 * ensure that no more that `kBinMax` pixels are sampled.
+	 *
+	 * \todo Take weights into account?
+	 */
+	const auto prediv = std::clamp<uint8_t>(std::ceil(std::sqrt(skip)), 1, 127);
+	hst->v_stepsize = prediv;
+	hst->h_step_inc = 65536 / prediv;
+
+	/* RPP-X1: sample shift/offset to 0 */
+	hst->sample_offs = 0;
+	hst->sample_shift = 0;
+
+	LOG(RppX1Agc, Debug)
+		<< "window: " << Rectangle(hst->wnd.h_offs, hst->wnd.v_offs, hst->wnd.h_size, hst->wnd.v_size) << ", "
+		<< "v_stepsize: " << hst->v_stepsize << ", "
+		<< "h_step_inc: " << hst->h_step_inc;
+}
+
+namespace {
+
+class AgcTraits final : public AgcMeanLuminance::Traits
+{
+public:
+	AgcTraits(std::span<const uint32_t> expMeans, std::span<const float> weights)
+		: expMeans_(expMeans), weights_(weights)
+	{
+		ASSERT(expMeans_.size() == weights_.size());
+	}
+
+	/**
+	 * \brief Estimate the relative luminance of the frame with a given gain
+	 * \param[in] gain The gain to apply to the frame
+	 *
+	 * This function estimates the average relative luminance of the frame that
+	 * would be output by the sensor if an additional \a gain was applied.
+	 *
+	 * The estimation is based on the AE statistics for the current frame. Y
+	 * averages for all cells are first multiplied by the gain, and then saturated
+	 * to approximate the sensor behaviour at high brightness values. The
+	 * approximation is quite rough, as it doesn't take into account non-linearities
+	 * when approaching saturation. In this case, saturating after the conversion to
+	 * YUV doesn't take into account the fact that the R, G and B components
+	 * contribute differently to the relative luminance.
+	 *
+	 * The values are normalized to the [0.0, 1.0] range, where 1.0 corresponds to a
+	 * theoretical perfect reflector of 100% reference white.
+	 *
+	 * More detailed information can be found in:
+	 * https://en.wikipedia.org/wiki/Relative_luminance
+	 *
+	 * \return The relative luminance
+	 */
+	double estimateLuminance(double gain) const override
+	{
+		constexpr double kMaxExpMean = (1u << 20) - 1;
+
+		double ySum = 0.0;
+		double wSum = 0.0;
+
+		/* Sum the averages, saturated to 255. */
+		for (unsigned i = 0; i < expMeans_.size(); i++) {
+			double w = weights_[i];
+			ySum += std::min<double>(expMeans_[i] * gain, kMaxExpMean) * w;
+			wSum += w;
+		}
+
+		/* \todo Weight with the AWB gains */
+
+		return ySum / wSum / kMaxExpMean;
+	}
+
+private:
+	std::span<const uint32_t> expMeans_;
+	std::span<const float> weights_;
+};
+
+} /* namespace */
+
+/**
+ * \brief Process RppX1 statistics, and run AGC operations
+ * \param[in] context The shared IPA context
+ * \param[in] frame The frame context sequence number
+ * \param[in] frameContext The current frame context
+ * \param[in] stats The RPP-X1 statistics and ISP results
+ * \param[out] metadata Metadata for the frame, to be filled by the algorithm
+ *
+ * Identify the current image brightness, and use that to estimate the optimal
+ * new exposure and gain for the scene.
+ */
+void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext, const RppX1Stats *stats,
+		  ControlList &metadata)
+{
+	const auto histPost = stats->block<StatsType::HistPost>();
+	const auto exmPre1 = stats->block<StatsType::ExmPre1>();
+
+	if (!histPost)
+		LOG(RppX1Agc, Error) << "HIST_POST data is missing in statistics";
+	if (!exmPre1)
+		LOG(RppX1Agc, Error) << "EXM_PRE1 data is missing in statistics";
+
+	if (histPost && exmPre1) {
+		static_assert(RPPX1_EXM_NUM_WIN == RPPX1_HIST_WEIGHT_GRIDS_SIZE);
+
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
+			.traits = AgcTraits{
+				exmPre1->exp_mean,
+				meteringModes_.at(frameContext.agc.meteringMode),
+			},
+			.yHist = {
+				/* The lower 4 bits are fractional and meant to be discarded. */
+				histPost->hist_bins,
+				[](uint32_t x) { return x >> 4; },
+			},
+			.exposure = frameContext.sensor.exposure,
+			.gain = frameContext.sensor.gain,
+		}}, metadata);
+	} else {
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
+	}
+
+	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
+}
+
+REGISTER_IPA_ALGORITHM(Agc, "Agc")
+
+} /* namespace ipa::rppx1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rppx1/algorithms/agc.h b/src/ipa/rppx1/algorithms/agc.h
new file mode 100644
index 0000000000..1b3c55681a
--- /dev/null
+++ b/src/ipa/rppx1/algorithms/agc.h
@@ -0,0 +1,47 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * Rpp-X1 AGC/AEC algorithm
+ */
+
+#pragma once
+
+#include <map>
+#include <vector>
+
+#include "libipa/agc.h"
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rppx1::algorithms {
+
+class Agc : public Algorithm
+{
+public:
+	int init(IPAContext &context, const ValueNode &tuningData) override;
+	int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;
+	void queueRequest(IPAContext &context,
+			  const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     RppX1Params *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const RppX1Stats *stats,
+		     ControlList &metadata) override;
+
+private:
+	int parseMeteringModes(IPAContext &context, const ValueNode &tuningData);
+
+	std::map<int32_t, std::vector<float>> meteringModes_;
+	AgcAlgorithm agc_;
+};
+
+} /* namespace ipa::rppx1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build
index 165f0ce054..394ea9510b 100644
--- a/src/ipa/rppx1/algorithms/meson.build
+++ b/src/ipa/rppx1/algorithms/meson.build
@@ -1,5 +1,6 @@ 
 # SPDX-License-Identifier: CC0-1.0
 
 rppx1_ipa_algorithms = files([
+    'agc.cpp',
     'blc.cpp',
 ])
diff --git a/src/ipa/rppx1/ipa_context.h b/src/ipa/rppx1/ipa_context.h
index 079fcab782..f6408e73bb 100644
--- a/src/ipa/rppx1/ipa_context.h
+++ b/src/ipa/rppx1/ipa_context.h
@@ -9,14 +9,14 @@ 
 
 #include <memory>
 
-#include <libcamera/base/utils.h>
+#include <linux/media/dreamchip/rppx1-config.h>
 
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
-#include <libcamera/geometry.h>
 
 #include <libcamera/ipa/core_ipa_interface.h>
 
+#include <libipa/agc.h>
 #include <libipa/camera_sensor_helper.h>
 #include <libipa/fc_queue.h>
 
@@ -25,12 +25,27 @@  namespace libcamera {
 namespace ipa::rppx1 {
 
 struct IPASessionConfiguration {
+	struct Agc : ipa::agc::Session {
+		rppx1_window measureWindow;
+	} agc;
 };
 
 struct IPAActiveState {
+	struct Agc : ipa::agc::ActiveState {
+		controls::AeMeteringModeEnum meteringMode;
+	} agc;
 };
 
 struct IPAFrameContext : public FrameContext {
+	struct {
+		uint32_t exposure;
+		double gain;
+	} sensor;
+
+	struct Agc : ipa::agc::FrameContext {
+		controls::AeMeteringModeEnum meteringMode;
+		bool updateMetering;
+	} agc;
 };
 
 struct IPAContext {
diff --git a/src/ipa/rppx1/params.h b/src/ipa/rppx1/params.h
index 05db309914..45adf43613 100644
--- a/src/ipa/rppx1/params.h
+++ b/src/ipa/rppx1/params.h
@@ -17,6 +17,8 @@  namespace ipa::rppx1 {
 
 enum class BlockType : uint16_t {
 	BlsPre1,
+	ExmPre1,
+	HistPost,
 };
 
 namespace details {
@@ -34,6 +36,8 @@  struct block_type {
 	};
 
 RPPX1_DEFINE_BLOCK_TYPE(BlsPre1, bls, BLS_PRE1)
+RPPX1_DEFINE_BLOCK_TYPE(ExmPre1, exm, EXM_PRE1)
+RPPX1_DEFINE_BLOCK_TYPE(HistPost, hist, HIST_POST)
 
 struct params_traits {
 	using id_type = BlockType;
diff --git a/src/ipa/rppx1/rppx1.cpp b/src/ipa/rppx1/rppx1.cpp
index 23ab8391b3..78c9370b29 100644
--- a/src/ipa/rppx1/rppx1.cpp
+++ b/src/ipa/rppx1/rppx1.cpp
@@ -27,6 +27,8 @@ 
 #include "libcamera/internal/mapped_framebuffer.h"
 #include "libcamera/internal/yaml_parser.h"
 
+#include <libipa/agc.h>
+
 #include "algorithms/algorithm.h"
 
 #include "ipa_context.h"
@@ -229,6 +231,9 @@  void IPARppX1::processStats(const uint32_t frame, const uint32_t bufferId,
 	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
 	ControlList metadata(controls::controls);
 
+	std::tie(frameContext.sensor.exposure, frameContext.sensor.gain) =
+		agc::extractControls(sensorControls, context_.camHelper.get());
+
 	for (auto const &a : algorithms()) {
 		Algorithm *algo = static_cast<Algorithm *>(a.get());
 		algo->process(context_, frame, frameContext, &stats, metadata);
@@ -249,9 +254,19 @@  void IPARppX1::updateControls(ControlInfoMap *ipaControls)
 
 void IPARppX1::setControls(unsigned int frame)
 {
-	[[maybe_unused]] IPAFrameContext &frameContext = context_.frameContexts.get(frame);
+	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
+
+	uint32_t exposure = frameContext.agc.exposure;
+	uint32_t vblank = frameContext.agc.vblank;
+
+	LOG(IPARppX1, Debug)
+		<< "Set controls for frame " << frame << ": exposure " << exposure
+		<< ", gain " << frameContext.agc.gain << ", vblank " << vblank;
 
 	ControlList ctrls(context_.sensorControls);
+	agc::prepareControls(ctrls, context_.camHelper.get(),
+			     exposure, frameContext.agc.gain);
+	ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank));
 
 	setSensorControls.emit(frame, ctrls);
 }
diff --git a/src/ipa/rppx1/stats.h b/src/ipa/rppx1/stats.h
index 39dc132bac..2925e2bbfc 100644
--- a/src/ipa/rppx1/stats.h
+++ b/src/ipa/rppx1/stats.h
@@ -16,6 +16,8 @@  namespace libcamera {
 namespace ipa::rppx1 {
 
 enum class StatsType : uint16_t {
+	ExmPre1,
+	HistPost,
 };
 
 namespace details {
@@ -32,6 +34,9 @@  struct stats_type {
 			RPPX1_STATS_BLOCK_TYPE_##id;			\
 	};
 
+RPPX1_DEFINE_STATS_TYPE(ExmPre1, exm, EXM_PRE1)
+RPPX1_DEFINE_STATS_TYPE(HistPost, hist, HIST_POST)
+
 struct stats_traits {
 	using id_type = StatsType;