{"id":27469,"url":"https://patchwork.libcamera.org/api/patches/27469/?format=json","web_url":"https://patchwork.libcamera.org/patch/27469/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260723154327.1357866-22-barnabas.pocze@ideasonboard.com>","date":"2026-07-23T15:43:04","name":"[RFC,v2,21/43] ipa: libipa: Add `AgcAlgorithm`","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"1a796b58c78d3b47b0c890943cbb948aae170d17","submitter":{"id":216,"url":"https://patchwork.libcamera.org/api/people/216/?format=json","name":"Barnabás Pőcze","email":"barnabas.pocze@ideasonboard.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/27469/mbox/","series":[{"id":6075,"url":"https://patchwork.libcamera.org/api/series/6075/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=6075","date":"2026-07-23T15:42:43","name":"ipa: libipa: agc rework","version":2,"mbox":"https://patchwork.libcamera.org/series/6075/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/27469/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/27469/checks/","tags":{},"headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id C9793BE080\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 23 Jul 2026 15:44:02 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 1E6EF67F2C;\n\tThu, 23 Jul 2026 17:44:02 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B20CB67EE4\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 23 Jul 2026 17:43:35 +0200 (CEST)","from pb-laptop.local (185.182.215.156.nat.pool.zt.hu\n\t[185.182.215.156])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id B89F92C09\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 23 Jul 2026 17:42:34 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"QSur8nlO\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1784821354;\n\tbh=EZFWPuML0YM7g/SywwmeTfYry8mjSRCAcmXzWCF01n8=;\n\th=From:To:Subject:Date:In-Reply-To:References:From;\n\tb=QSur8nlO0CRYdzQqtOfrDgvM/gJeTIIl32P55Mlu9XNE+jV4LTnVX9shBfCcinrH1\n\tp5LC5h4/8KkaiARUt+WTLC7FiYMtcFJgp+4KJXI+f9opG7oW8v4QOi/4xnB2Hl8ebf\n\tzGxx7mJCa3mQtJH6FYVuFj4f6Y9YIURL2TqdSea0=","From":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Subject":"[RFC PATCH v2 21/43] ipa: libipa: Add `AgcAlgorithm`","Date":"Thu, 23 Jul 2026 17:43:04 +0200","Message-ID":"<20260723154327.1357866-22-barnabas.pocze@ideasonboard.com>","X-Mailer":"git-send-email 2.55.0","In-Reply-To":"<20260723154327.1357866-1-barnabas.pocze@ideasonboard.com>","References":"<20260723154327.1357866-1-barnabas.pocze@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"},"content":"Add a class that implements the `Algorithm` interface using `AgcMeanLuminance`\nbased on the rkisp1 `Agc` algorithm, with slight adjustments.\n\nSigned-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>\n---\n src/ipa/libipa/agc.cpp     | 639 +++++++++++++++++++++++++++++++++++++\n src/ipa/libipa/agc.h       |  98 ++++++\n src/ipa/libipa/meson.build |   1 +\n 3 files changed, 738 insertions(+)\n create mode 100644 src/ipa/libipa/agc.cpp","diff":"diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp\nnew file mode 100644\nindex 0000000000..e16a02fdde\n--- /dev/null\n+++ b/src/ipa/libipa/agc.cpp\n@@ -0,0 +1,639 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2021-2022, 2026 Ideas On Board\n+ *\n+ * Generic AGC algorithm\n+ */\n+\n+#include \"agc.h\"\n+\n+#include <algorithm>\n+#include <array>\n+#include <chrono>\n+#include <optional>\n+\n+#include <linux/v4l2-controls.h>\n+\n+#include <libcamera/base/log.h>\n+\n+#include <libcamera/control_ids.h>\n+#include <libcamera/controls.h>\n+\n+namespace libcamera {\n+\n+namespace ipa {\n+\n+using namespace std::chrono_literals;\n+\n+LOG_DEFINE_CATEGORY(Agc)\n+\n+/**\n+ * \\class AgcAlgorithm\n+ * \\brief AgcMeanLuminance wrapper for implementing the Algorithm interface\n+ *\n+ * \\todo DigitalGain, DigitalGainMode\n+ */\n+\n+/**\n+ * \\struct agc::Session\n+ * \\brief Session configuration for AgcAlgorithm\n+ *\n+ * \\var agc::Session::minExposureTime\n+ * \\brief Minimum exposure time supported with the configured sensor\n+ *\n+ * \\var agc::Session::maxExposureTime\n+ * \\brief Maximum exposure time supported with the configured sensor\n+ *\n+ * \\var agc::Session::minAnalogueGain\n+ * \\brief Minimum analogue gain supported with the configured sensor\n+ *\n+ * \\var agc::Session::maxAnalogueGain\n+ * \\brief Maximum analogue gain supported with the configured sensor\n+ *\n+ * \\var agc::Session::minFrameDuration\n+ * \\brief Minimum frame duration supported with the configured sensor\n+ *\n+ * \\var agc::Session::maxFrameDuration\n+ * \\brief Maximum frame duration supported with the configured sensor\n+ *\n+ * \\var agc::Session::lineDuration\n+ * \\brief Line duration with the configured sensor and output size\n+ *\n+ * \\var agc::Session::sensor\n+ * \\brief Details of the sensor configuration\n+ *\n+ * \\var agc::Session::sensor.outputSize\n+ * \\brief Configured output size of the sensor\n+ *\n+ * \\var agc::Session::autoAllowed\n+ * \\brief Whether automatic controls are allowed\n+ */\n+\n+/**\n+ * \\struct agc::ActiveState\n+ * \\brief Active state for AgcAlgorithm\n+ *\n+ * The \\a automatic variables track the latest values computed by algorithm\n+ * based on the latest processed statistics. All other variables track the\n+ * consolidated controls requested in queued requests.\n+ *\n+ * \\var agc::ActiveState::manual\n+ * \\brief Manual exposure time and analog gain (set through requests)\n+ *\n+ * \\var agc::ActiveState::manual.exposure\n+ * \\brief Manual exposure time expressed as a number of lines as set by the\n+ * ExposureTime control\n+ *\n+ * \\var agc::ActiveState::manual.gain\n+ * \\brief Manual analogue gain as set by the AnalogueGain control\n+ *\n+ * \\var agc::ActiveState::automatic\n+ * \\brief Automatic exposure time and analog gain (computed by the algorithm)\n+ *\n+ * \\var agc::ActiveState::automatic.exposure\n+ * \\brief Automatic exposure time expressed as a number of lines\n+ *\n+ * \\var agc::ActiveState::automatic.gain\n+ * \\brief Automatic analogue gain multiplier\n+ *\n+ * \\var agc::ActiveState::automatic.quantizationGain\n+ * \\brief Automatic quantization gain multiplier\n+ *\n+ * \\var agc::ActiveState::automatic.yTarget\n+ * \\brief Automatically determined luminance target\n+ *\n+ * \\var agc::ActiveState::autoExposureEnabled\n+ * \\brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control\n+ *\n+ * \\var agc::ActiveState::autoGainEnabled\n+ * \\brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control\n+ *\n+ * \\var agc::ActiveState::exposureValue\n+ * \\brief Exposure value as set by the ExposureValue control\n+ *\n+ * \\var agc::ActiveState::constraintMode\n+ * \\brief Constraint mode as set by the AeConstraintMode control\n+ *\n+ * \\var agc::ActiveState::exposureMode\n+ * \\brief Exposure mode as set by the AeExposureMode control\n+ *\n+ * \\var agc::ActiveState::minFrameDuration\n+ * \\brief Minimum frame duration as set by the FrameDurationLimits control\n+ *\n+ * \\var agc::ActiveState::maxFrameDuration\n+ * \\brief Maximum frame duration as set by the FrameDurationLimits control\n+ */\n+\n+/**\n+ * \\struct agc::FrameContext\n+ * \\brief Per-frame context for AgcAlgorithm\n+ *\n+ * \\var agc::FrameContext::exposure\n+ * \\brief Exposure time expressed as a number of lines computed by the algorithm\n+ *\n+ * \\var agc::FrameContext::gain\n+ * \\brief Analogue gain multiplier computed by the algorithm\n+ *\n+ * The gain should be adapted to the sensor specific gain code before applying.\n+ *\n+ * \\var agc::FrameContext::quantizationGain\n+ * \\brief Quantization gain multiplier computed by the algorithm\n+ *\n+ * \\var agc::FrameContext::exposureValue\n+ * \\brief Exposure value as set by the ExposureValue control\n+ *\n+ * \\var agc::FrameContext::yTarget\n+ * \\brief Luminance target computed by the algorithm\n+ *\n+ * \\var agc::FrameContext::vblank\n+ * \\brief Vertical blanking parameter computed by the algorithm\n+ *\n+ * \\var agc::FrameContext::autoExposureEnabled\n+ * \\brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control\n+ *\n+ * \\var agc::FrameContext::autoGainEnabled\n+ * \\brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control\n+ *\n+ * \\var agc::FrameContext::constraintMode\n+ * \\brief Constraint mode as set by the AeConstraintMode control\n+ *\n+ * \\var agc::FrameContext::exposureMode\n+ * \\brief Exposure mode as set by the AeExposureMode control\n+ *\n+ * \\var agc::FrameContext::minFrameDuration\n+ * \\brief Minimum frame duration as set by the FrameDurationLimits control\n+ *\n+ * \\var agc::FrameContext::maxFrameDuration\n+ * \\brief Maximum frame duration as set by the FrameDurationLimits control\n+ *\n+ * \\var agc::FrameContext::frameDuration\n+ * \\brief The actual FrameDuration used by the algorithm for the frame\n+ *\n+ * \\var agc::FrameContext::autoExposureModeChange\n+ * \\brief Indicate if autoExposureEnabled has changed from true in the previous\n+ * frame to false in the current frame, and no manual exposure value has been\n+ * supplied in the current frame.\n+ *\n+ * \\var agc::FrameContext::autoGainModeChange\n+ * \\brief Indicate if autoGainEnabled has changed from true in the previous\n+ * frame to false in the current frame, and no manual gain value has been\n+ * supplied in the current frame.\n+ */\n+\n+/**\n+ * \\struct AgcAlgorithm::ConfigurationParams\n+ * \\brief Parameters for AgcAlgorithm::configure()\n+ *\n+ * \\var AgcAlgorithm::ConfigurationParams::sensor\n+ * \\brief CameraSensorHelper for the sensor\n+ *\n+ * \\var AgcAlgorithm::ConfigurationParams::sensorInfo\n+ * \\brief Details of the sensor\n+ *\n+ * \\var AgcAlgorithm::ConfigurationParams::sensorControls\n+ * \\brief ControlInfoMap of the sensor\n+ *\n+ * \\var AgcAlgorithm::ConfigurationParams::ctrlMap\n+ * \\brief ControlMap to update with controls\n+ *\n+ * \\var AgcAlgorithm::ConfigurationParams::autoAllowed\n+ * \\brief Whether to enable auto controls\n+ */\n+\n+/**\n+ * \\struct AgcAlgorithm::ProcessParams\n+ * \\brief Parameters for AgcAlgorithm::process()\n+ *\n+ * \\var AgcAlgorithm::ProcessParams::traits\n+ * \\brief Implementation of AgcMeanLuminance::Traits\n+ *\n+ * \\var AgcAlgorithm::ProcessParams::yHist\n+ * \\brief Luminance histogram of the frame\n+ *\n+ * \\var AgcAlgorithm::ProcessParams::exposure\n+ * \\brief Effective exposure of the frame\n+ *\n+ * \\var AgcAlgorithm::ProcessParams::gain\n+ * \\brief Effective gain of the frame\n+ *\n+ * \\var AgcAlgorithm::ProcessParams::additionalConstraints\n+ * \\brief Additional AgcMeanLuminance::AgcConstraints to apply\n+ *\n+ * \\var AgcAlgorithm::ProcessParams::lux\n+ * \\brief Effective lux value of the frame\n+ */\n+\n+/**\n+ * \\brief Load tuning data\n+ */\n+int AgcAlgorithm::init(const ValueNode &tuningData)\n+{\n+\tint ret = impl_.parseTuningData(tuningData);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief Initialize the session configuration and active state\n+ */\n+int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, const ConfigurationParams &config)\n+{\n+\tsession = {};\n+\tsession.lineDuration = config.sensorInfo.minLineLength * 1.0s\n+\t\t/ config.sensorInfo.pixelRate;\n+\tsession.sensor.outputSize = config.sensorInfo.outputSize;\n+\tsession.autoAllowed = config.autoAllowed;\n+\n+\tconst double lineDurationUs = session.lineDuration.get<std::micro>();\n+\n+\t/*\n+\t * Compute exposure time limits from the V4L2_CID_EXPOSURE control\n+\t * limits and the line duration.\n+\t */\n+\n+\tconst ControlInfo &v4l2Exposure = config.sensorControls.find(V4L2_CID_EXPOSURE)->second;\n+\tint32_t minExposure = v4l2Exposure.min().get<int32_t>();\n+\tint32_t maxExposure = v4l2Exposure.max().get<int32_t>();\n+\tint32_t defExposure = v4l2Exposure.def().get<int32_t>();\n+\tconfig.ctrlMap[&controls::ExposureTime] = ControlInfo{\n+\t\tstatic_cast<int32_t>(minExposure * lineDurationUs),\n+\t\tstatic_cast<int32_t>(maxExposure * lineDurationUs),\n+\t\tstatic_cast<int32_t>(defExposure * lineDurationUs),\n+\t};\n+\n+\t/* Compute the analogue gain limits. */\n+\tconst ControlInfo &v4l2Gain = config.sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;\n+\tfloat minGain = config.sensor->gain(v4l2Gain.min().get<int32_t>());\n+\tfloat maxGain = config.sensor->gain(v4l2Gain.max().get<int32_t>());\n+\tfloat defGain = config.sensor->gain(v4l2Gain.def().get<int32_t>());\n+\tconfig.ctrlMap[&controls::AnalogueGain] = ControlInfo{\n+\t\tminGain,\n+\t\tmaxGain,\n+\t\tdefGain,\n+\t};\n+\n+\tLOG(Agc, Debug)\n+\t\t<< \"Exposure: [\" << minExposure << \", \" << maxExposure\n+\t\t<< \"], gain: [\" << minGain << \", \" << maxGain << \"]\";\n+\n+\t/*\n+\t * Compute the frame duration limits.\n+\t *\n+\t * The frame length is computed assuming a fixed line length combined\n+\t * with the vertical frame sizes.\n+\t */\n+\tconst ControlInfo &v4l2HBlank = config.sensorControls.find(V4L2_CID_HBLANK)->second;\n+\tuint32_t hblank = v4l2HBlank.def().get<int32_t>();\n+\tuint32_t lineLength = config.sensorInfo.outputSize.width + hblank;\n+\n+\tconst ControlInfo &v4l2VBlank = config.sensorControls.find(V4L2_CID_VBLANK)->second;\n+\tstd::array<uint32_t, 3> frameHeights{\n+\t\tv4l2VBlank.min().get<int32_t>() + config.sensorInfo.outputSize.height,\n+\t\tv4l2VBlank.max().get<int32_t>() + config.sensorInfo.outputSize.height,\n+\t\tv4l2VBlank.def().get<int32_t>() + config.sensorInfo.outputSize.height,\n+\t};\n+\n+\tstd::array<int64_t, 3> frameDurations;\n+\tfor (unsigned int i = 0; i < frameHeights.size(); ++i) {\n+\t\tuint64_t frameSize = lineLength * frameHeights[i];\n+\t\tframeDurations[i] = frameSize / (config.sensorInfo.pixelRate / 1000000U);\n+\t}\n+\n+\tconfig.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{\n+\t\tframeDurations[0],\n+\t\tframeDurations[1],\n+\t\tSpan<const int64_t, 2>{ { frameDurations[2], frameDurations[2] } },\n+\t};\n+\n+\tsession.minFrameDuration = std::chrono::microseconds(frameDurations[0]);\n+\tsession.maxFrameDuration = std::chrono::microseconds(frameDurations[1]);\n+\n+\t/*\n+\t * When the AGC computes the new exposure values for a frame, it needs\n+\t * to know the limits for exposure time and analogue gain. As it depends\n+\t * on the sensor, update it with the controls.\n+\t *\n+\t * \\todo take VBLANK into account for maximum exposure time\n+\t */\n+\tsession.minExposureTime = minExposure * session.lineDuration;\n+\tsession.maxExposureTime = maxExposure * session.lineDuration;\n+\tsession.minAnalogueGain = minGain;\n+\tsession.maxAnalogueGain = maxGain;\n+\n+\timpl_.configure(session.lineDuration, config.sensor);\n+\timpl_.setLimits(session.minExposureTime, session.maxExposureTime,\n+\t\t\tsession.minAnalogueGain, session.maxAnalogueGain,\n+\t\t\t{});\n+\timpl_.resetFrameCount();\n+\n+\t/* Configure the default exposure and gain. */\n+\tstate = {};\n+\tstate.automatic.gain = session.minAnalogueGain;\n+\tstate.automatic.exposure = 10ms / session.lineDuration;\n+\tstate.automatic.quantizationGain = 1;\n+\tstate.automatic.yTarget = impl_.effectiveYTarget(0, 1);\n+\tstate.manual.gain = state.automatic.gain;\n+\tstate.manual.exposure = state.automatic.exposure;\n+\tstate.autoExposureEnabled = session.autoAllowed;\n+\tstate.autoGainEnabled = session.autoAllowed;\n+\tstate.exposureValue = 0;\n+\n+\tstate.constraintMode =\n+\t\tstatic_cast<controls::AeConstraintModeEnum>(impl_.constraintModes().begin()->first);\n+\tstate.exposureMode =\n+\t\tstatic_cast<controls::AeExposureModeEnum>(impl_.exposureModeHelpers().begin()->first);\n+\n+\tstate.minFrameDuration = session.minFrameDuration;\n+\tstate.maxFrameDuration = session.maxFrameDuration;\n+\n+\tconst auto add = [&](const ControlId &cid, const auto &automatic, const auto &manual) {\n+\t\tstd::array<ControlValue, 2> values;\n+\t\tsize_t count = 0;\n+\n+\t\tif (session.autoAllowed)\n+\t\t\tvalues[count++] = ControlValue(automatic);\n+\n+\t\tvalues[count++] = ControlValue(manual);\n+\n+\t\tconfig.ctrlMap[&cid] = ControlInfo{\n+\t\t\t{ values.data(), count },\n+\t\t\tControlValue(session.autoAllowed ? automatic : manual),\n+\t\t};\n+\t};\n+\n+\tadd(controls::ExposureTimeMode, controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual);\n+\tadd(controls::AnalogueGainMode, controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual);\n+\n+\t/* \\todo Move this to the `Camera` class. */\n+\tconfig.ctrlMap[&controls::AeEnable] = ControlInfo{\n+\t\tfalse,\n+\t\tsession.autoAllowed,\n+\t\tsession.autoAllowed,\n+\t};\n+\n+\tif (session.autoAllowed) {\n+\t\tconfig.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);\n+\n+\t\tfor (const auto &[id, info] : impl_.controls())\n+\t\t\tconfig.ctrlMap[id] = info;\n+\t} else {\n+\t\tconfig.ctrlMap.erase(&controls::ExposureValue);\n+\n+\t\tfor (const auto &[id, info] : impl_.controls())\n+\t\t\tconfig.ctrlMap.erase(id);\n+\t}\n+\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief Handle a \\a queueRequest operation\n+ */\n+void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &state,\n+\t\t\t\tagc::FrameContext &frameContext, const ControlList &controls)\n+{\n+\tif (session.autoAllowed) {\n+\t\tconst auto &aeEnable = controls.get(controls::ExposureTimeMode);\n+\t\tif (aeEnable &&\n+\t\t    (*aeEnable == controls::ExposureTimeModeAuto) != state.autoExposureEnabled) {\n+\t\t\tstate.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto);\n+\n+\t\t\tLOG(Agc, Debug)\n+\t\t\t\t<< (state.autoExposureEnabled ? \"Enabling\" : \"Disabling\")\n+\t\t\t\t<< \" AGC (exposure)\";\n+\n+\t\t\t/*\n+\t\t\t * If we go from auto -> manual with no manual control\n+\t\t\t * set, use the last computed value, which we don't\n+\t\t\t * know until prepare() so save this information.\n+\t\t\t *\n+\t\t\t * \\todo Check the previous frame at prepare() time\n+\t\t\t * instead of saving a flag here\n+\t\t\t */\n+\t\t\tif (!state.autoExposureEnabled && !controls.get(controls::ExposureTime))\n+\t\t\t\tframeContext.autoExposureModeChange = true;\n+\t\t}\n+\n+\t\tconst auto &agEnable = controls.get(controls::AnalogueGainMode);\n+\t\tif (agEnable &&\n+\t\t    (*agEnable == controls::AnalogueGainModeAuto) != state.autoGainEnabled) {\n+\t\t\tstate.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto);\n+\n+\t\t\tLOG(Agc, Debug)\n+\t\t\t\t<< (state.autoGainEnabled ? \"Enabling\" : \"Disabling\")\n+\t\t\t\t<< \" AGC (gain)\";\n+\t\t\t/*\n+\t\t\t * If we go from auto -> manual with no manual control\n+\t\t\t * set, use the last computed value, which we don't\n+\t\t\t * know until prepare() so save this information.\n+\t\t\t */\n+\t\t\tif (!state.autoGainEnabled && !controls.get(controls::AnalogueGain))\n+\t\t\t\tframeContext.autoGainModeChange = true;\n+\t\t}\n+\t}\n+\n+\tconst auto &exposure = controls.get(controls::ExposureTime);\n+\tif (exposure && !state.autoExposureEnabled) {\n+\t\tstate.manual.exposure = *exposure * 1.0us / session.lineDuration;\n+\n+\t\tLOG(Agc, Debug)\n+\t\t\t<< \"Set exposure to \" << state.manual.exposure;\n+\t}\n+\n+\tconst auto &gain = controls.get(controls::AnalogueGain);\n+\tif (gain && !state.autoGainEnabled) {\n+\t\tstate.manual.gain = *gain;\n+\n+\t\tLOG(Agc, Debug) << \"Set gain to \" << state.manual.gain;\n+\t}\n+\n+\tframeContext.autoExposureEnabled = state.autoExposureEnabled;\n+\tframeContext.autoGainEnabled = state.autoGainEnabled;\n+\n+\tif (!frameContext.autoExposureEnabled)\n+\t\tframeContext.exposure = state.manual.exposure;\n+\tif (!frameContext.autoGainEnabled)\n+\t\tframeContext.gain = state.manual.gain;\n+\n+\tif (!frameContext.autoExposureEnabled &&\n+\t    !frameContext.autoGainEnabled)\n+\t\tframeContext.quantizationGain = 1.0;\n+\n+\tconst auto &exposureMode = controls.get(controls::AeExposureMode);\n+\tif (exposureMode)\n+\t\tstate.exposureMode =\n+\t\t\tstatic_cast<controls::AeExposureModeEnum>(*exposureMode);\n+\tframeContext.exposureMode = state.exposureMode;\n+\n+\tconst auto &constraintMode = controls.get(controls::AeConstraintMode);\n+\tif (constraintMode)\n+\t\tstate.constraintMode =\n+\t\t\tstatic_cast<controls::AeConstraintModeEnum>(*constraintMode);\n+\tframeContext.constraintMode = state.constraintMode;\n+\n+\tconst auto &exposureValue = controls.get(controls::ExposureValue);\n+\tif (exposureValue)\n+\t\tstate.exposureValue = *exposureValue;\n+\tframeContext.exposureValue = state.exposureValue;\n+\n+\tconst auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);\n+\tif (frameDurationLimits) {\n+\t\t/* Limit the control value to the limits in ControlInfo */\n+\t\tstate.minFrameDuration = std::clamp<utils::Duration>(\n+\t\t\tstd::chrono::microseconds((*frameDurationLimits).front()),\n+\t\t\tsession.minFrameDuration,\n+\t\t\tsession.maxFrameDuration\n+\t\t);\n+\n+\t\tstate.maxFrameDuration = std::clamp<utils::Duration>(\n+\t\t\tstd::chrono::microseconds((*frameDurationLimits).back()),\n+\t\t\tsession.minFrameDuration,\n+\t\t\tsession.maxFrameDuration\n+\t\t);\n+\t}\n+\tframeContext.minFrameDuration = state.minFrameDuration;\n+\tframeContext.maxFrameDuration = state.maxFrameDuration;\n+}\n+\n+/**\n+ * \\brief Handle a \\a prepare operation\n+ */\n+void AgcAlgorithm::prepare(agc::ActiveState &state, agc::FrameContext &frameContext)\n+{\n+\tuint32_t activeAutoExposure = state.automatic.exposure;\n+\tdouble activeAutoGain = state.automatic.gain;\n+\tdouble activeAutoQGain = state.automatic.quantizationGain;\n+\n+\t/* Populate exposure and gain in auto mode */\n+\tif (frameContext.autoExposureEnabled) {\n+\t\tframeContext.exposure = activeAutoExposure;\n+\t\tframeContext.quantizationGain = activeAutoQGain;\n+\t}\n+\tif (frameContext.autoGainEnabled) {\n+\t\tframeContext.gain = activeAutoGain;\n+\t\tframeContext.quantizationGain = activeAutoQGain;\n+\t}\n+\n+\t/*\n+\t * Populate manual exposure and gain from the active auto values when\n+\t * transitioning from auto to manual\n+\t */\n+\tif (!frameContext.autoExposureEnabled && frameContext.autoExposureModeChange) {\n+\t\tstate.manual.exposure = activeAutoExposure;\n+\t\tframeContext.exposure = activeAutoExposure;\n+\t}\n+\tif (!frameContext.autoGainEnabled && frameContext.autoGainModeChange) {\n+\t\tstate.manual.gain = activeAutoGain;\n+\t\tframeContext.gain = activeAutoGain;\n+\t\tframeContext.quantizationGain = activeAutoQGain;\n+\t}\n+\n+\tframeContext.yTarget = state.automatic.yTarget;\n+}\n+\n+/**\n+ * \\brief Handle a \\a process operation\n+ */\n+void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state,\n+\t\t\t   agc::FrameContext &frameContext, std::optional<ProcessParams> &&params,\n+\t\t\t   ControlList &metadata)\n+{\n+\tconst utils::Duration &lineDuration = session.lineDuration;\n+\tutils::Duration newExposureTime = {};\n+\n+\tif (params) {\n+\t\tASSERT(session.autoAllowed);\n+\n+\t\t/*\n+\t\t* Set the AGC limits using the fixed exposure time and/or gain in\n+\t\t* manual mode, or the sensor limits in auto mode.\n+\t\t*/\n+\t\tutils::Duration minExposureTime;\n+\t\tutils::Duration maxExposureTime;\n+\t\tdouble minAnalogueGain;\n+\t\tdouble maxAnalogueGain;\n+\n+\t\tif (frameContext.autoExposureEnabled) {\n+\t\t\tminExposureTime = session.minExposureTime;\n+\t\t\tmaxExposureTime = std::clamp(frameContext.maxFrameDuration, session.minExposureTime, session.maxExposureTime);\n+\t\t} else {\n+\t\t\tminExposureTime = lineDuration * frameContext.exposure;\n+\t\t\tmaxExposureTime = minExposureTime;\n+\t\t}\n+\n+\t\tif (frameContext.autoGainEnabled) {\n+\t\t\tminAnalogueGain = session.minAnalogueGain;\n+\t\t\tmaxAnalogueGain = session.maxAnalogueGain;\n+\t\t} else {\n+\t\t\tminAnalogueGain = frameContext.gain;\n+\t\t\tmaxAnalogueGain = frameContext.gain;\n+\t\t}\n+\n+\t\t/*\n+\t\t* The Agc algorithm needs to know the effective exposure value that was\n+\t\t* applied to the sensor when the statistics were collected.\n+\t\t*/\n+\t\tutils::Duration effectiveExposureValue =\n+\t\t\tlineDuration * params->exposure * params->gain;\n+\n+\t\timpl_.setLimits(minExposureTime, maxExposureTime,\n+\t\t\t\tminAnalogueGain, maxAnalogueGain,\n+\t\t\t\tstd::move(params->additionalConstraints));\n+\n+\t\tconst auto &newEv = impl_.calculateNewEv({\n+\t\t\t.traits = params->traits,\n+\t\t\t.yHist = params->yHist,\n+\t\t\t.effectiveExposureValue = effectiveExposureValue,\n+\t\t\t.constraintModeIndex = frameContext.constraintMode,\n+\t\t\t.exposureModeIndex = frameContext.exposureMode,\n+\t\t\t.lux = params->lux,\n+\t\t\t.exposureCompensation = pow(2.0, frameContext.exposureValue),\n+\t\t});\n+\n+\t\tLOG(Agc, Debug)\n+\t\t\t<< \"Divided up exposure time, analogue gain, quantization gain\"\n+\t\t\t<< \" and digital gain are \" << newEv.exposureTime << \", \" << newEv.analogueGain\n+\t\t\t<< \", \" << newEv.quantizationGain << \" and \" << newEv.digitalGain;\n+\n+\t\t/* Update the estimated exposure and gain. */\n+\t\tstate.automatic.exposure = newEv.exposureTime / lineDuration;\n+\t\tstate.automatic.gain = newEv.analogueGain;\n+\t\tstate.automatic.quantizationGain = newEv.quantizationGain;\n+\t\tstate.automatic.yTarget = newEv.yTarget;\n+\n+\t\tnewExposureTime = newEv.exposureTime;\n+\t}\n+\n+\t/*\n+\t * Expand the target frame duration so that we do not run faster than\n+\t * the minimum frame duration when we have short exposures.\n+\t */\n+\tconst auto frameDuration = std::max(frameContext.minFrameDuration, newExposureTime);\n+\tframeContext.vblank = (frameDuration / lineDuration) - session.sensor.outputSize.height;\n+\n+\t/* Update frame duration accounting for line length quantization. */\n+\tframeContext.frameDuration = (session.sensor.outputSize.height + frameContext.vblank) * lineDuration;\n+\n+\tmetadata.set(controls::AnalogueGain, frameContext.gain);\n+\tmetadata.set(controls::ExposureTime, utils::Duration(lineDuration * frameContext.exposure).get<std::micro>());\n+\tmetadata.set(controls::FrameDuration, frameContext.frameDuration.get<std::micro>());\n+\tmetadata.set(controls::ExposureTimeMode,\n+\t\t     frameContext.autoExposureEnabled\n+\t\t     ? controls::ExposureTimeModeAuto\n+\t\t     : controls::ExposureTimeModeManual);\n+\tmetadata.set(controls::AnalogueGainMode,\n+\t\t     frameContext.autoGainEnabled\n+\t\t     ? controls::AnalogueGainModeAuto\n+\t\t     : controls::AnalogueGainModeManual);\n+\n+\tmetadata.set(controls::AeExposureMode, frameContext.exposureMode);\n+\tmetadata.set(controls::AeConstraintMode, frameContext.constraintMode);\n+\tmetadata.set(controls::ExposureValue, frameContext.exposureValue);\n+}\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h\nindex 5247425952..1eace12908 100644\n--- a/src/ipa/libipa/agc.h\n+++ b/src/ipa/libipa/agc.h\n@@ -7,13 +7,19 @@\n \n #pragma once\n \n+#include <optional>\n #include <utility>\n \n #include <linux/v4l2-controls.h>\n \n+#include <libcamera/control_ids.h>\n #include <libcamera/controls.h>\n \n+#include <libcamera/ipa/core_ipa_interface.h>\n+\n+#include \"agc_mean_luminance.h\"\n #include \"camera_sensor_helper.h\"\n+#include \"histogram.h\"\n \n namespace libcamera {\n \n@@ -42,8 +48,100 @@ prepareControls(ControlList &controls, const CameraSensorHelper *sensor,\n \tcontrols.set(V4L2_CID_ANALOGUE_GAIN, int32_t(sensor ? sensor->gainCode(gain) : gain));\n }\n \n+struct Session {\n+\tutils::Duration minExposureTime;\n+\tutils::Duration maxExposureTime;\n+\tdouble minAnalogueGain;\n+\tdouble maxAnalogueGain;\n+\tutils::Duration minFrameDuration;\n+\tutils::Duration maxFrameDuration;\n+\n+\tutils::Duration lineDuration;\n+\n+\tstruct {\n+\t\tSize outputSize;\n+\t} sensor;\n+\n+\tbool autoAllowed;\n+};\n+\n+struct ActiveState {\n+\tstruct {\n+\t\tuint32_t exposure;\n+\t\tdouble gain;\n+\t} manual;\n+\tstruct {\n+\t\tuint32_t exposure;\n+\t\tdouble gain;\n+\t\tdouble quantizationGain;\n+\t\tdouble yTarget;\n+\t} automatic;\n+\n+\tbool autoExposureEnabled;\n+\tbool autoGainEnabled;\n+\tdouble exposureValue;\n+\tcontrols::AeConstraintModeEnum constraintMode;\n+\tcontrols::AeExposureModeEnum exposureMode;\n+\tutils::Duration minFrameDuration;\n+\tutils::Duration maxFrameDuration;\n+};\n+\n+struct FrameContext {\n+\tuint32_t exposure;\n+\tdouble gain;\n+\tdouble quantizationGain;\n+\tdouble exposureValue;\n+\tdouble yTarget;\n+\tuint32_t vblank;\n+\tbool autoExposureEnabled;\n+\tbool autoGainEnabled;\n+\tcontrols::AeConstraintModeEnum constraintMode;\n+\tcontrols::AeExposureModeEnum exposureMode;\n+\tutils::Duration minFrameDuration;\n+\tutils::Duration maxFrameDuration;\n+\tutils::Duration frameDuration;\n+\tbool autoExposureModeChange;\n+\tbool autoGainModeChange;\n+};\n+\n } /* namespace agc */\n \n+class AgcAlgorithm\n+{\n+public:\n+\tstruct ConfigurationParams {\n+\t\tconst CameraSensorHelper *sensor;\n+\t\tconst IPACameraSensorInfo &sensorInfo;\n+\t\tconst ControlInfoMap &sensorControls;\n+\t\tControlInfoMap::Map &ctrlMap;\n+\t\tbool autoAllowed = true;\n+\t};\n+\n+\tint init(const ValueNode &tuningData);\n+\n+\tint configure(agc::Session &session, agc::ActiveState &state, const ConfigurationParams &config);\n+\n+\tvoid queueRequest(const agc::Session &session, agc::ActiveState &state,\n+\t\t\t  agc::FrameContext &frameContext, const ControlList &controls);\n+\n+\tvoid prepare(agc::ActiveState &state, agc::FrameContext &frameContext);\n+\n+\tstruct ProcessParams {\n+\t\tconst AgcMeanLuminance::Traits &traits;\n+\t\tconst Histogram &yHist;\n+\t\tuint32_t exposure;\n+\t\tdouble gain;\n+\t\tstd::vector<AgcMeanLuminance::AgcConstraint> &&additionalConstraints = {};\n+\t\tdouble lux = 0;\n+\t};\n+\n+\tvoid process(const agc::Session &session, agc::ActiveState &state, agc::FrameContext &frameContext,\n+\t\t     std::optional<ProcessParams> &&params, ControlList &metadata);\n+\n+private:\n+\tAgcMeanLuminance impl_;\n+};\n+\n } /* namespace ipa */\n \n } /* namespace libcamera */\ndiff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build\nindex 05f1a8749c..ca681fa5af 100644\n--- a/src/ipa/libipa/meson.build\n+++ b/src/ipa/libipa/meson.build\n@@ -23,6 +23,7 @@ libipa_headers = files([\n ])\n \n libipa_sources = files([\n+    'agc.cpp',\n     'agc_mean_luminance.cpp',\n     'algorithm.cpp',\n     'awb_bayes.cpp',\n","prefixes":["RFC","v2","21/43"]}