Show a patch.

GET /api/patches/27169/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 27169,
    "url": "https://patchwork.libcamera.org/api/patches/27169/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/27169/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20260703122543.1991189-11-paul.elder@ideasonboard.com>",
    "date": "2026-07-03T12:25:16",
    "name": "[RFC,10/19] ipa: rkisp2: algo: agc: Implement automatic gain control",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "9b81ceaa83b1ad59e2cafec056c619ae4d1caa45",
    "submitter": {
        "id": 17,
        "url": "https://patchwork.libcamera.org/api/people/17/?format=api",
        "name": "Paul Elder",
        "email": "paul.elder@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/27169/mbox/",
    "series": [
        {
            "id": 6035,
            "url": "https://patchwork.libcamera.org/api/series/6035/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=6035",
            "date": "2026-07-03T12:25:06",
            "name": "Add support for rkisp2",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/6035/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/27169/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/27169/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 923E2C328C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  3 Jul 2026 12:26:37 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 4275B65FE2;\n\tFri,  3 Jul 2026 14:26:37 +0200 (CEST)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id D4ED865FDF\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  3 Jul 2026 14:26:35 +0200 (CEST)",
            "from neptunite.hamster-moth.ts.net (unknown\n\t[IPv6:2404:7a81:160:2100:a2cc:2f45:3bd7:2589])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 004E71121;\n\tFri,  3 Jul 2026 14:25:46 +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=\"EA5hVS/R\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1783081549;\n\tbh=aXas7ZXnEXY6006S1EayfAlN8KPowCE2a94tRzIXrxw=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=EA5hVS/Re1nhZw07ojscZ4ZK0uegJhYX4T2tCH/bzIRGkZYdXrI8Ixnrnauq8e8e2\n\tuFmBHUXZyl1HBwX5Sx2DhBOkBuIN2FUgL+VAqpOTTydz5gEZgs9iLDWr3/K/Hbc8Oq\n\tDlJUnVki9lS0g7ea3mc0v0lJunOgaIZq9/mKuCb4=",
        "From": "Paul Elder <paul.elder@ideasonboard.com>",
        "To": "laurent.pinchart@ideasonboard.com",
        "Cc": "Paul Elder <paul.elder@ideasonboard.com>, michael.riesch@collabora.com, \n\txuhf@rock-chips.com, stefan.klug@ideasonboard.com,\n\tkieran.bingham@ideasonboard.com, dan.scally@ideasonboard.com,\n\tjacopo.mondi@ideasonboard.com, nicolas.dufresne@collabora.com,\n\tlibcamera-devel@lists.libcamera.org",
        "Subject": "[RFC PATCH 10/19] ipa: rkisp2: algo: agc: Implement automatic gain\n\tcontrol",
        "Date": "Fri,  3 Jul 2026 21:25:16 +0900",
        "Message-ID": "<20260703122543.1991189-11-paul.elder@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.47.2",
        "In-Reply-To": "<20260703122543.1991189-1-paul.elder@ideasonboard.com>",
        "References": "<20260703122543.1991189-1-paul.elder@ideasonboard.com>",
        "MIME-Version": "1.0",
        "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": "Implement auto gain control algorithm for the rkisp2 IPA. This leverages\nthe libipa agc, and supports manual gain and exposure control as well.\nIt currently does not function in raw mode, meaning that there is no\nmanual gain and exposure control in raw mode, as no IPA is created when\nrunning in raw mode for the rkisp2.\n\nSigned-off-by: Paul Elder <paul.elder@ideasonboard.com>\n\n---\ntodo: support manual gain and exposure in raw mode\n---\n src/ipa/rkisp2/algorithms/agc.cpp     | 472 ++++++++++++++++++++++++++\n src/ipa/rkisp2/algorithms/agc.h       |  58 ++++\n src/ipa/rkisp2/algorithms/meson.build |   1 +\n 3 files changed, 531 insertions(+)\n create mode 100644 src/ipa/rkisp2/algorithms/agc.cpp\n create mode 100644 src/ipa/rkisp2/algorithms/agc.h",
    "diff": "diff --git a/src/ipa/rkisp2/algorithms/agc.cpp b/src/ipa/rkisp2/algorithms/agc.cpp\nnew file mode 100644\nindex 000000000000..00300ac7c0a4\n--- /dev/null\n+++ b/src/ipa/rkisp2/algorithms/agc.cpp\n@@ -0,0 +1,472 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026, Ideas On Board Oy.\n+ *\n+ * AGC/AEC mean-based control algorithm\n+ */\n+\n+#include \"agc.h\"\n+\n+#include <algorithm>\n+#include <array>\n+#include <chrono>\n+#include <cmath>\n+#include <tuple>\n+#include <vector>\n+\n+#include <libcamera/base/log.h>\n+#include <libcamera/base/span.h>\n+#include <libcamera/base/utils.h>\n+\n+#include <libcamera/control_ids.h>\n+#include <libcamera/controls.h>\n+#include <libcamera/ipa/core_ipa_interface.h>\n+\n+#include \"libipa/histogram.h\"\n+\n+/**\n+ * \\file agc.h\n+ */\n+\n+namespace libcamera {\n+\n+using namespace std::literals::chrono_literals;\n+\n+namespace ipa::rkisp2::algorithms {\n+\n+/**\n+ * \\class Agc\n+ * \\brief A mean-based auto-exposure algorithm\n+ */\n+\n+LOG_DEFINE_CATEGORY(RkISP2Agc)\n+\n+int Agc::init(IPAContext &context, const ValueNode &tuningData)\n+{\n+\tint ret = parseTuningData(tuningData);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tcontext.ctrlMap[&controls::ExposureTimeMode] =\n+\t\tControlInfo({ { ControlValue(controls::ExposureTimeModeAuto),\n+\t\t\t\tControlValue(controls::ExposureTimeModeManual) } },\n+\t\t\t    ControlValue(controls::ExposureTimeModeAuto));\n+\tcontext.ctrlMap[&controls::AnalogueGainMode] =\n+\t\tControlInfo({ { ControlValue(controls::AnalogueGainModeAuto),\n+\t\t\t\tControlValue(controls::AnalogueGainModeManual) } },\n+\t\t\t    ControlValue(controls::AnalogueGainModeAuto));\n+\tcontext.ctrlMap[&controls::ExposureValue] = ControlInfo(-8.0f, 8.0f, 0.0f);\n+\t/* \\todo Support AnalogueGain and ExposureTime in raw mode */\n+\tcontext.ctrlMap.merge(controls());\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief Configure the AGC given a configInfo\n+ * \\param[in] context The shared IPA context\n+ * \\param[in] configInfo The IPA configuration data\n+ *\n+ * \\return 0\n+ */\n+int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)\n+{\n+\tcontext.configuration.agc.measureWindow.h_offs = 0;\n+\tcontext.configuration.agc.measureWindow.v_offs = 0;\n+\tcontext.configuration.agc.measureWindow.h_size = (configInfo.outputSize.width / 5);\n+\t/*\n+\t * ae lite needs the -2 because the total window height must be\n+\t * divisible by 2, and it cannot be equal to or greater than the frame\n+\t * size, or else the hardware hangs\n+\t *\n+\t * \\todo Move this to the kernel?\n+\t * \\todo Check if hist lite also needs this\n+\t */\n+\tcontext.configuration.agc.measureWindow.v_size = (configInfo.outputSize.height / 5) - 2;\n+\n+\tcontext.configuration.agc.measureWindow15.h_size = (configInfo.outputSize.width / 15);\n+\tcontext.configuration.agc.measureWindow15.v_size = (configInfo.outputSize.height / 15) - 2;\n+\n+\t/* Configure the default exposure and gain. */\n+\tcontext.activeState.agc.automatic.gain = context.configuration.sensor.minAnalogueGain;\n+\tcontext.activeState.agc.automatic.exposure =\n+\t\t10ms / context.configuration.sensor.lineDuration;\n+\tcontext.activeState.agc.automatic.quantizationGain = 1.0;\n+\tcontext.activeState.agc.manual.gain = context.activeState.agc.automatic.gain;\n+\tcontext.activeState.agc.manual.exposure = context.activeState.agc.automatic.exposure;\n+\tcontext.activeState.agc.autoExposureEnabled = true;\n+\tcontext.activeState.agc.autoGainEnabled = true;\n+\tcontext.activeState.agc.exposureValue = 0.0;\n+\n+\t/* Limit the frame duration to match current initialisation */\n+\tControlInfo &frameDurationLimits = context.ctrlMap[&controls::FrameDurationLimits];\n+\tcontext.activeState.agc.minFrameDuration = std::chrono::microseconds(frameDurationLimits.min().get<int64_t>());\n+\tcontext.activeState.agc.maxFrameDuration = std::chrono::microseconds(frameDurationLimits.max().get<int64_t>());\n+\n+\tAgcMeanLuminance::configure(context.configuration.sensor.lineDuration,\n+\t\t\t\t    context.camHelper.get());\n+\n+\tsetLimits(context.configuration.sensor.minExposureTime,\n+\t\t  context.configuration.sensor.maxExposureTime,\n+\t\t  context.configuration.sensor.minAnalogueGain,\n+\t\t  context.configuration.sensor.maxAnalogueGain, {});\n+\n+\tcontext.activeState.agc.automatic.yTarget = effectiveYTarget();\n+\n+\tresetFrameCount();\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\copydoc libcamera::ipa::Algorithm::queueRequest\n+ */\n+void Agc::queueRequest(IPAContext &context,\n+\t\t       [[maybe_unused]] const uint32_t frame,\n+\t\t       IPAFrameContext &frameContext,\n+\t\t       const ControlList &controls)\n+{\n+\tauto &agc = context.activeState.agc;\n+\n+\tconst auto &aeEnable = controls.get(controls::ExposureTimeMode);\n+\tif (aeEnable &&\n+\t    (*aeEnable == controls::ExposureTimeModeAuto) != agc.autoExposureEnabled) {\n+\t\tagc.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto);\n+\n+\t\tLOG(RkISP2Agc, Debug)\n+\t\t\t<< (agc.autoExposureEnabled ? \"Enabling\" : \"Disabling\")\n+\t\t\t<< \" AGC (exposure)\";\n+\n+\t\t/*\n+\t\t * If we go from auto -> manual with no manual control\n+\t\t * set, use the last computed value, which we don't\n+\t\t * know until prepare() so save this information.\n+\t\t *\n+\t\t * \\todo Check the previous frame at prepare() time\n+\t\t * instead of saving a flag here\n+\t\t */\n+\t\tif (!agc.autoExposureEnabled && !controls.get(controls::ExposureTime))\n+\t\t\tframeContext.agc.autoExposureModeChange = true;\n+\t}\n+\n+\tconst auto &agEnable = controls.get(controls::AnalogueGainMode);\n+\tif (agEnable &&\n+\t    (*agEnable == controls::AnalogueGainModeAuto) != agc.autoGainEnabled) {\n+\t\tagc.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto);\n+\n+\t\tLOG(RkISP2Agc, Debug)\n+\t\t\t<< (agc.autoGainEnabled ? \"Enabling\" : \"Disabling\")\n+\t\t\t<< \" AGC (gain)\";\n+\t\t/*\n+\t\t * If we go from auto -> manual with no manual control\n+\t\t * set, use the last computed value, which we don't\n+\t\t * know until prepare() so save this information.\n+\t\t */\n+\t\tif (!agc.autoGainEnabled && !controls.get(controls::AnalogueGain))\n+\t\t\tframeContext.agc.autoGainModeChange = true;\n+\t}\n+\n+\tconst auto &exposure = controls.get(controls::ExposureTime);\n+\tif (exposure && !agc.autoExposureEnabled) {\n+\t\tagc.manual.exposure = *exposure * 1.0us\n+\t\t\t\t    / context.configuration.sensor.lineDuration;\n+\n+\t\tLOG(RkISP2Agc, Debug)\n+\t\t\t<< \"Set exposure to \" << agc.manual.exposure;\n+\t}\n+\n+\tconst auto &gain = controls.get(controls::AnalogueGain);\n+\tif (gain && !agc.autoGainEnabled) {\n+\t\tagc.manual.gain = *gain;\n+\n+\t\tLOG(RkISP2Agc, Debug) << \"Set gain to \" << agc.manual.gain;\n+\t}\n+\n+\tframeContext.agc.autoExposureEnabled = agc.autoExposureEnabled;\n+\tframeContext.agc.autoGainEnabled = agc.autoGainEnabled;\n+\n+\tif (!frameContext.agc.autoExposureEnabled)\n+\t\tframeContext.agc.exposure = agc.manual.exposure;\n+\tif (!frameContext.agc.autoGainEnabled)\n+\t\tframeContext.agc.gain = agc.manual.gain;\n+\n+\tif (!frameContext.agc.autoExposureEnabled &&\n+\t    !frameContext.agc.autoGainEnabled)\n+\t\tframeContext.agc.quantizationGain = 1.0;\n+\n+\tconst auto &exposureValue = controls.get(controls::ExposureValue);\n+\tif (exposureValue)\n+\t\tagc.exposureValue = *exposureValue;\n+\tframeContext.agc.exposureValue = agc.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\tControlInfo &limits = context.ctrlMap[&controls::FrameDurationLimits];\n+\t\tint64_t minFrameDuration =\n+\t\t\tstd::clamp((*frameDurationLimits).front(),\n+\t\t\t\t   limits.min().get<int64_t>(),\n+\t\t\t\t   limits.max().get<int64_t>());\n+\t\tint64_t maxFrameDuration =\n+\t\t\tstd::clamp((*frameDurationLimits).back(),\n+\t\t\t\t   limits.min().get<int64_t>(),\n+\t\t\t\t   limits.max().get<int64_t>());\n+\n+\t\tagc.minFrameDuration = std::chrono::microseconds(minFrameDuration);\n+\t\tagc.maxFrameDuration = std::chrono::microseconds(maxFrameDuration);\n+\t}\n+\tframeContext.agc.minFrameDuration = agc.minFrameDuration;\n+\tframeContext.agc.maxFrameDuration = agc.maxFrameDuration;\n+}\n+\n+/**\n+ * \\copydoc libcamera::ipa::Algorithm::prepare\n+ */\n+void Agc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n+\t\t  [[maybe_unused]] IPAFrameContext &frameContext, RkISP2Params *params)\n+{\n+\tuint32_t activeAutoExposure = context.activeState.agc.automatic.exposure;\n+\tdouble activeAutoGain = context.activeState.agc.automatic.gain;\n+\tdouble activeAutoQGain = context.activeState.agc.automatic.quantizationGain;\n+\n+\t/* Populate exposure and gain in auto mode */\n+\tif (frameContext.agc.autoExposureEnabled) {\n+\t\tframeContext.agc.exposure = activeAutoExposure;\n+\t\tframeContext.agc.quantizationGain = activeAutoQGain;\n+\t}\n+\tif (frameContext.agc.autoGainEnabled) {\n+\t\tframeContext.agc.gain = activeAutoGain;\n+\t\tframeContext.agc.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.agc.autoExposureEnabled && frameContext.agc.autoExposureModeChange) {\n+\t\tcontext.activeState.agc.manual.exposure = activeAutoExposure;\n+\t\tframeContext.agc.exposure = activeAutoExposure;\n+\t}\n+\tif (!frameContext.agc.autoGainEnabled && frameContext.agc.autoGainModeChange) {\n+\t\tcontext.activeState.agc.manual.gain = activeAutoGain;\n+\t\tframeContext.agc.gain = activeAutoGain;\n+\t\tframeContext.agc.quantizationGain = activeAutoQGain;\n+\t}\n+\n+\tframeContext.agc.yTarget = context.activeState.agc.automatic.yTarget;\n+\n+\tif (frame > 1)\n+\t\treturn;\n+\n+\t/*\n+\t * Configure the AEC measurements. Set the window, measure\n+\t * continuously, and estimate Y as (R + G + B) x (85/256).\n+\t */\n+\tauto aeLiteConfig = params->block<RkISP2Blocks::AeLite>();\n+\taeLiteConfig.setEnabled(true);\n+\n+\taeLiteConfig->window_num = 1;\n+\taeLiteConfig->meas_window = context.configuration.agc.measureWindow;\n+\n+\tauto hstConfig = params->block<RkISP2Blocks::HistBig0>();\n+\thstConfig.setEnabled(true);\n+\n+\thstConfig->window_num = 0;\n+\t/* \\todo choose this based on the bitdepth */\n+\thstConfig->data_sel = RKISP2_ISP_HISTOGRAM_DATA_SEL_9_2;\n+\thstConfig->mode = RKISP2_ISP_HISTOGRAM_MODE_Y_HISTOGRAM;\n+\t/* waterline means to exclude everything above this value */\n+\thstConfig->waterline = 0x0;\n+\thstConfig->stepsize = 0;\n+\thstConfig->coeffs.r = 0x20;\n+\thstConfig->coeffs.g = 0x20;\n+\thstConfig->coeffs.b = 0x20;\n+\n+\thstConfig->meas_window = context.configuration.agc.measureWindow15;\n+\n+\t/* \\todo Support configuring the weights */\n+\tfor (size_t i = 0; i < RKISP2_ISP_HIST_WEIGHT_GRIDS_SIZE_BIG; i++)\n+\t\thstConfig->weights[i] = 0x20;\n+\n+\tauto hstConfigLite = params->block<RkISP2Blocks::HistLite>();\n+\thstConfigLite.setEnabled(false);\n+}\n+\n+void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,\n+\t\t       ControlList &metadata, [[maybe_unused]] const rkisp2_stats_buffer *stats)\n+{\n+\tutils::Duration exposureTime = context.configuration.sensor.lineDuration\n+\t\t\t\t     * frameContext.sensor.exposure;\n+\tmetadata.set(controls::AnalogueGain, frameContext.sensor.gain);\n+\tmetadata.set(controls::ExposureTime, exposureTime.get<std::micro>());\n+\tmetadata.set(controls::FrameDuration, frameContext.agc.frameDuration.get<std::micro>());\n+\tmetadata.set(controls::ExposureTimeMode,\n+\t\t     frameContext.agc.autoExposureEnabled\n+\t\t     ? controls::ExposureTimeModeAuto\n+\t\t     : controls::ExposureTimeModeManual);\n+\tmetadata.set(controls::AnalogueGainMode,\n+\t\t     frameContext.agc.autoGainEnabled\n+\t\t     ? controls::AnalogueGainModeAuto\n+\t\t     : controls::AnalogueGainModeManual);\n+\n+\tmetadata.set(controls::ExposureValue, frameContext.agc.exposureValue);\n+}\n+\n+double Agc::estimateLuminance(double gain) const\n+{\n+\t/*\n+\t * \\todo Enforce this check, since lite-like is 5x5 while big is 15x15,\n+\t * but I haven't yet figured out how to use all 15x15 weights. At the\n+\t * moment we're running everything lite-like\n+\t */\n+\tASSERT(expMeans_.size() == weights_.size());\n+\tdouble ySum = 0.0;\n+\tdouble wSum = 0.0;\n+\n+\t/* Sum the averages, saturated to 4095. */\n+\tfor (unsigned i = 0; i < expMeans_.size(); i++) {\n+\t\tdouble w = weights_[i] / 0x10;\n+\t\tySum += std::min(expMeans_[i] * gain, 4095.0) * w;\n+\t\twSum += w;\n+\t}\n+\n+\t/* \\todo Weight with the AWB gains */\n+\n+\treturn ySum / wSum / 4095;\n+}\n+\n+void Agc::processFrameDuration(IPAContext &context,\n+\t\t\t       IPAFrameContext &frameContext,\n+\t\t\t       utils::Duration frameDuration)\n+{\n+\tIPACameraSensorInfo &sensorInfo = context.sensorInfo;\n+\tutils::Duration lineDuration = context.configuration.sensor.lineDuration;\n+\n+\tframeContext.agc.vblank = (frameDuration / lineDuration) - sensorInfo.outputSize.height;\n+\n+\t/* Update frame duration accounting for line length quantization. */\n+\tframeContext.agc.frameDuration = (sensorInfo.outputSize.height + frameContext.agc.vblank) * lineDuration;\n+}\n+\n+/**\n+ * \\brief Process RkISP2 statistics, and run AGC operations\n+ * \\param[in] context The shared IPA context\n+ * \\param[in] frame The frame context sequence number\n+ * \\param[in] frameContext The current frame context\n+ * \\param[in] stats The RKISP2 statistics and ISP results\n+ * \\param[out] metadata Metadata for the frame, to be filled by the algorithm\n+ *\n+ * Identify the current image brightness, and use that to estimate the optimal\n+ * new exposure and gain for the scene.\n+ */\n+void Agc::process([[maybe_unused]] IPAContext &context,\n+\t\t  [[maybe_unused]] const uint32_t frame,\n+\t\t  [[maybe_unused]] IPAFrameContext &frameContext,\n+\t\t  const rkisp2_stats_buffer *stats,\n+\t\t  ControlList &metadata)\n+{\n+\tconst utils::Duration &lineDuration = context.configuration.sensor.lineDuration;\n+\n+\t/*\n+\t * \\todo Verify that the exposure and gain applied by the sensor for\n+\t * this frame match what has been requested. This isn't a hard\n+\t * requirement for stability of the AGC (the guarantee we need in\n+\t * automatic mode is a perfect match between the frame and the values\n+\t * we receive), but is important in manual mode.\n+\t */\n+\n+\t/* The lower 5 bits are fractional and meant to be discarded. */\n+\tHistogram hist({ stats->hist_big0.hist_bins, RKISP2_ISP_HIST_BIN_N_MAX },\n+\t\t       [](uint32_t x) { return x >> 5; });\n+\n+\texpMeans_.resize(RKISP2_ISP_AE_MEAN_MAX_LITE);\n+\tfor (size_t i = 0; i < RKISP2_ISP_AE_MEAN_MAX_LITE; i++) {\n+\t\t/* r and b are 0~1023; g is 255~4095 so multiply r and b to match g */\n+\t\tuint16_t r = stats->ae_lite.exp_mean_r[i] * 4;\n+\t\tuint16_t g = stats->ae_lite.exp_mean_g[i];\n+\t\tuint16_t b = stats->ae_lite.exp_mean_b[i] * 4;\n+\t\texpMeans_[i] = 0.2126 * r + 0.7152 * g + 0.0722 * b;\n+\t}\n+\n+\t/* \\todo Support configuring the weights */\n+\tstd::vector<uint8_t> modeWeights(25, 0x10);\n+\tweights_ = { modeWeights.data(), modeWeights.size() };\n+\n+\t/*\n+\t * Set the AGC limits using the fixed exposure time and/or gain in\n+\t * manual mode, or the sensor limits in auto mode.\n+\t */\n+\tutils::Duration minExposureTime;\n+\tutils::Duration maxExposureTime;\n+\tdouble minAnalogueGain;\n+\tdouble maxAnalogueGain;\n+\n+\tif (frameContext.agc.autoExposureEnabled) {\n+\t\tminExposureTime = context.configuration.sensor.minExposureTime;\n+\t\tmaxExposureTime = std::clamp(frameContext.agc.maxFrameDuration,\n+\t\t\t\t\t     context.configuration.sensor.minExposureTime,\n+\t\t\t\t\t     context.configuration.sensor.maxExposureTime);\n+\t} else {\n+\t\tminExposureTime = context.configuration.sensor.lineDuration\n+\t\t\t\t* frameContext.agc.exposure;\n+\t\tmaxExposureTime = minExposureTime;\n+\t}\n+\n+\tif (frameContext.agc.autoGainEnabled) {\n+\t\tminAnalogueGain = context.configuration.sensor.minAnalogueGain;\n+\t\tmaxAnalogueGain = context.configuration.sensor.maxAnalogueGain;\n+\t} else {\n+\t\tminAnalogueGain = frameContext.agc.gain;\n+\t\tmaxAnalogueGain = frameContext.agc.gain;\n+\t}\n+\n+\tsetLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain, {});\n+\n+\t/*\n+\t * The Agc algorithm needs to know the effective exposure value that was\n+\t * applied to the sensor when the statistics were collected.\n+\t */\n+\tutils::Duration exposureTime = lineDuration * frameContext.sensor.exposure;\n+\tdouble analogueGain = frameContext.sensor.gain;\n+\tutils::Duration effectiveExposureValue = exposureTime * analogueGain;\n+\tif (effectiveExposureValue == 0ms) {\n+\t\tLOG(RkISP2Agc, Warning)\n+\t\t\t<< \"frame \" << frame << \": Effective exposure value is 0: sensor exposure: \"\n+\t\t\t<< exposureTime << \", analogue gain: \" << analogueGain;\n+\t}\n+\n+\t/* \\todo Support lux estimation */\n+\n+\t/* \\todo Support setting constraint and exposure modes */\n+\tutils::Duration newExposureTime;\n+\tdouble aGain, qGain, dGain;\n+\tstd::tie(newExposureTime, aGain, qGain, dGain) =\n+\t\tcalculateNewEv(0, 0, hist, effectiveExposureValue);\n+\n+\tLOG(RkISP2Agc, Debug)\n+\t\t<< \"Divided up exposure time, analogue gain, quantization gain\"\n+\t\t<< \" and digital gain are \" << newExposureTime << \", \" << aGain\n+\t\t<< \", \" << qGain << \" and \" << dGain;\n+\n+\tIPAActiveState &activeState = context.activeState;\n+\t/* Update the estimated exposure and gain. */\n+\tactiveState.agc.automatic.exposure = newExposureTime / lineDuration;\n+\tactiveState.agc.automatic.gain = aGain;\n+\tactiveState.agc.automatic.quantizationGain = qGain;\n+\tactiveState.agc.automatic.yTarget = effectiveYTarget();\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+\tprocessFrameDuration(context, frameContext,\n+\t\t\t     std::max(frameContext.agc.minFrameDuration, newExposureTime));\n+\n+\tfillMetadata(context, frameContext, metadata, stats);\n+\texpMeans_ = {};\n+}\n+\n+REGISTER_IPA_ALGORITHM(Agc, \"Agc\")\n+\n+} /* namespace ipa::rkisp2::algorithms */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/rkisp2/algorithms/agc.h b/src/ipa/rkisp2/algorithms/agc.h\nnew file mode 100644\nindex 000000000000..18627c32834a\n--- /dev/null\n+++ b/src/ipa/rkisp2/algorithms/agc.h\n@@ -0,0 +1,58 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026, Ideas On Board Oy.\n+ *\n+ * RkISP2 AGC/AEC mean-based control algorithm\n+ */\n+\n+#pragma once\n+\n+#include <vector>\n+\n+#include <linux/rkisp2-config.h>\n+\n+#include <libcamera/base/span.h>\n+#include <libcamera/base/utils.h>\n+\n+#include \"libipa/agc_mean_luminance.h\"\n+\n+#include \"algorithm.h\"\n+\n+namespace libcamera {\n+\n+namespace ipa::rkisp2::algorithms {\n+\n+class Agc : public Algorithm, public AgcMeanLuminance\n+{\n+public:\n+\tAgc() = default;\n+\t~Agc() = default;\n+\n+\tint init(IPAContext &context, [[maybe_unused]] const ValueNode &tuningData) override;\n+\tint configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;\n+\tvoid queueRequest(IPAContext &context,\n+\t\t\t  const uint32_t frame,\n+\t\t\t  IPAFrameContext &frameContext,\n+\t\t\t  const ControlList &controls) override;\n+\tvoid prepare(IPAContext &context, const uint32_t frame,\n+\t\t     IPAFrameContext &frameContext,\n+\t\t     RkISP2Params *params) override;\n+\tvoid process(IPAContext &context, const uint32_t frame,\n+\t\t     IPAFrameContext &frameContext,\n+\t\t     const rkisp2_stats_buffer *stats,\n+\t\t     ControlList &metadata) override;\n+\n+private:\n+\tvoid fillMetadata(IPAContext &context, IPAFrameContext &frameContext,\n+\t\t\t  ControlList &metadata, const rkisp2_stats_buffer *stats);\n+\tdouble estimateLuminance(double gain) const override;\n+\tvoid processFrameDuration(IPAContext &context,\n+\t\t\t\t  IPAFrameContext &frameContext,\n+\t\t\t\t  utils::Duration frameDuration);\n+\n+\tstd::vector<uint16_t> expMeans_;\n+\tSpan<const uint8_t> weights_;\n+};\n+\n+} /* namespace ipa::rkisp2::algorithms */\n+} /* namespace libcamera */\ndiff --git a/src/ipa/rkisp2/algorithms/meson.build b/src/ipa/rkisp2/algorithms/meson.build\nindex 30133ac4fa68..027e40dd0346 100644\n--- a/src/ipa/rkisp2/algorithms/meson.build\n+++ b/src/ipa/rkisp2/algorithms/meson.build\n@@ -1,6 +1,7 @@\n # SPDX-License-Identifier: CC0-1.0\n \n rkisp2_ipa_algorithms = files([\n+    'agc.cpp',\n     'awb.cpp',\n     'bls.cpp',\n ])\n",
    "prefixes": [
        "RFC",
        "10/19"
    ]
}