Patch Detail
Show a patch.
GET /api/patches/27194/?format=api
{ "id": 27194, "url": "https://patchwork.libcamera.org/api/patches/27194/?format=api", "web_url": "https://patchwork.libcamera.org/patch/27194/", "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": "<20260703153819.1088752-17-barnabas.pocze@ideasonboard.com>", "date": "2026-07-03T15:38:18", "name": "[RFC,v1,16/17] ipa: simple: agc: Port to `AgcAlgorithm`", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "821d97ae3c4c014d0b7cef6eb8c66657530088ab", "submitter": { "id": 216, "url": "https://patchwork.libcamera.org/api/people/216/?format=api", "name": "Barnabás Pőcze", "email": "barnabas.pocze@ideasonboard.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/27194/mbox/", "series": [ { "id": 6036, "url": "https://patchwork.libcamera.org/api/series/6036/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=6036", "date": "2026-07-03T15:38:02", "name": "ipa: libipa: agc rework", "version": 1, "mbox": "https://patchwork.libcamera.org/series/6036/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/27194/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/27194/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 C1C17C3303\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 3 Jul 2026 15:38:45 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 1583D66019;\n\tFri, 3 Jul 2026 17:38:43 +0200 (CEST)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 68EB965FED\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 3 Jul 2026 17:38:26 +0200 (CEST)", "from pb-laptop.local (185.221.140.128.nat.pool.zt.hu\n\t[185.221.140.128])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 50F671121\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 3 Jul 2026 17:37:40 +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=\"BHLtqs7Y\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1783093060;\n\tbh=r4esyH1S/bN/Om3T3n+FhiOHUVvyWbjje7vOg/227eg=;\n\th=From:To:Subject:Date:In-Reply-To:References:From;\n\tb=BHLtqs7Y7rBPHSBg1+RfzwQxfTXTnWs5NAdzsyKPy8bmqCxgzhVqlZxZIjnjqiTZW\n\tvIs7hT5bzW0Tn9ErR0LYJjujOY7HYugxL7ZjrZrs7nLHvszVXDgZJLRyWkc8Rw2KBw\n\tpX0C9IgUP3+5Ynm/6GpOY6AsSMu/iLaTII6flg0Q=", "From": "=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>", "To": "libcamera-devel@lists.libcamera.org", "Subject": "[RFC PATCH v1 16/17] ipa: simple: agc: Port to `AgcAlgorithm`", "Date": "Fri, 3 Jul 2026 17:38:18 +0200", "Message-ID": "<20260703153819.1088752-17-barnabas.pocze@ideasonboard.com>", "X-Mailer": "git-send-email 2.54.0", "In-Reply-To": "<20260703153819.1088752-1-barnabas.pocze@ideasonboard.com>", "References": "<20260703153819.1088752-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": "Move the agc algorithm into a separate class and use `AgcAlgorithm`.\n\nSigned-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>\n---\n src/ipa/simple/agc_simple.cpp | 197 +++++++++++++++++++++++++++++\n src/ipa/simple/agc_simple.h | 59 +++++++++\n src/ipa/simple/algorithms/agc.cpp | 200 ++++++------------------------\n src/ipa/simple/algorithms/agc.h | 9 +-\n src/ipa/simple/ipa_context.h | 16 +--\n src/ipa/simple/meson.build | 1 +\n src/ipa/simple/soft_simple.cpp | 61 ++-------\n 7 files changed, 322 insertions(+), 221 deletions(-)\n create mode 100644 src/ipa/simple/agc_simple.cpp\n create mode 100644 src/ipa/simple/agc_simple.h", "diff": "diff --git a/src/ipa/simple/agc_simple.cpp b/src/ipa/simple/agc_simple.cpp\nnew file mode 100644\nindex 0000000000..0734600105\n--- /dev/null\n+++ b/src/ipa/simple/agc_simple.cpp\n@@ -0,0 +1,197 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Red Hat Inc.\n+ *\n+ * Exposure and gain\n+ */\n+\n+#include \"agc_simple.h\"\n+\n+#include <algorithm>\n+#include <cmath>\n+\n+#include <linux/v4l2-controls.h>\n+\n+#include <libcamera/base/log.h>\n+\n+namespace libcamera {\n+\n+namespace ipa::soft {\n+\n+LOG_DEFINE_CATEGORY(IPASoftAgcSimple)\n+\n+\n+/*\n+ * The number of bins to use for the optimal exposure calculations.\n+ */\n+static constexpr unsigned int kExposureBinsCount = 5;\n+\n+/*\n+ * The exposure is optimal when the mean sample value of the histogram is\n+ * in the middle of the range.\n+ */\n+static constexpr float kExposureOptimal = kExposureBinsCount / 2.0;\n+\n+/*\n+ * This implements the hysteresis for the exposure adjustment.\n+ * It is small enough to have the exposure close to the optimal, and is big\n+ * enough to prevent the exposure from wobbling around the optimal value.\n+ */\n+static constexpr float kExposureSatisfactory = 0.2;\n+\n+/*\n+ * Proportional gain for exposure/gain adjustment. Maps the MSV error to a\n+ * multiplicative correction factor:\n+ *\n+ * factor = 1.0 + kExpProportionalGain * error\n+ *\n+ * With kExpProportionalGain = 0.04:\n+ * - max error ~2.5 -> factor 1.10 (~10% step, same as before)\n+ * - error 1.0 -> factor 1.04 (~4% step)\n+ * - error 0.3 -> factor 1.012 (~1.2% step)\n+ *\n+ * This replaces the fixed 10% bang-bang step with a proportional correction\n+ * that converges smoothly and avoids overshooting near the target.\n+ */\n+static constexpr float kExpProportionalGain = 0.04;\n+\n+/*\n+ * Maximum multiplicative step per frame, to bound the correction when the\n+ * scene changes dramatically.\n+ */\n+static constexpr float kExpMaxStep = 0.15;\n+\n+void AgcSimpleAlgorithm::updateExposure(const Session &session, ActiveState &state, FrameContext &frameContext,\n+\t\t\t\t\tconst ProcessParams ¶ms, double exposureMSV)\n+{\n+\tdouble error = kExposureOptimal - exposureMSV;\n+\tif (std::abs(error) <= kExposureSatisfactory)\n+\t\treturn;\n+\n+\tutils::Duration exposureDuration = params.exposure * session.lineDuration;\n+\tint32_t exposure = params.exposure;\n+\tdouble again = params.gain;\n+\n+\t/*\n+\t * Compute a proportional correction factor. The sign of the error\n+\t * determines the direction: positive error means too dark (increase),\n+\t * negative means too bright (decrease).\n+\t */\n+\tfloat step = std::clamp(static_cast<float>(error) * kExpProportionalGain,\n+\t\t\t\t-kExpMaxStep, kExpMaxStep);\n+\tfloat factor = 1.0f + step;\n+\n+\tconst auto limits = AgcAlgorithm::calculateLimits(session, frameContext);\n+\n+\tif (factor > 1.0f) {\n+\t\t/* Scene too dark: increase exposure first, then gain. */\n+\t\tif (exposureDuration < limits.exposure.second) {\n+\t\t\tint32_t next = static_cast<int32_t>(exposure * factor);\n+\t\t\texposure = std::max(next, exposure + 1);\n+\t\t} else {\n+\t\t\tdouble next = again * factor;\n+\t\t\tif (next - again < session.gainMinStep)\n+\t\t\t\tagain += session.gainMinStep;\n+\t\t\telse\n+\t\t\t\tagain = next;\n+\t\t}\n+\t} else {\n+\t\t/* Scene too bright: decrease gain first, then exposure. */\n+\t\tif (again > std::max(session.gain10, limits.gain.first)) {\n+\t\t\tdouble next = again * factor;\n+\t\t\tif (again - next < session.gainMinStep)\n+\t\t\t\tagain -= session.gainMinStep;\n+\t\t\telse\n+\t\t\t\tagain = next;\n+\t\t} else {\n+\t\t\tint32_t next = static_cast<int32_t>(exposure * factor);\n+\t\t\texposure = std::min(next, exposure - 1);\n+\t\t}\n+\t}\n+\n+\texposureDuration = std::clamp<utils::Duration>(\n+\t\texposure * session.lineDuration,\n+\t\tlimits.exposure.first, limits.exposure.second);\n+\texposure = exposureDuration / session.lineDuration;\n+\tagain = std::clamp(again, limits.gain.first, limits.gain.second);\n+\n+\tstate.automatic.exposure = exposure;\n+\tstate.automatic.gain = again;\n+\n+\tLOG(IPASoftAgcSimple, Debug)\n+\t\t<< \"exposureMSV \" << exposureMSV\n+\t\t<< \" error \" << error << \" factor \" << factor\n+\t\t<< \" exp \" << exposure << \" again \" << again;\n+}\n+\n+int AgcSimpleAlgorithm::configure(Session &session, ActiveState &state, const ConfigurationParams &config)\n+{\n+\tint ret = AgcAlgorithm::configure(session, state, config);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tconst ControlInfo &v4l2Gain = config.sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;\n+\tauto defGain = v4l2Gain.def().get<int32_t>();\n+\n+\tif (config.sensor) {\n+\t\tsession.gain10 = std::max(session.minAnalogueGain, 1.0);\n+\t\tsession.gainMinStep = (session.maxAnalogueGain - session.minAnalogueGain) / 100.0;\n+\t} else {\n+\t\tsession.gain10 = defGain;\n+\t\tsession.gainMinStep = 1.0;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+void AgcSimpleAlgorithm::process(const Session &session, ActiveState &state,\n+\t\t\t\t FrameContext &frameContext, std::optional<ProcessParams> &¶ms,\n+\t\t\t\t ControlList &metadata)\n+{\n+\tutils::Duration newExposureTime = {};\n+\n+\tif (params) {\n+\t\t/*\n+\t\t* Calculate Mean Sample Value (MSV) according to formula from:\n+\t\t* https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf\n+\t\t*/\n+\t\tconst auto &histogram = params->stats.yHistogram;\n+\t\tconst unsigned int blackLevelHistIdx = params->blackLevel / (256 / SwIspStats::kYHistogramSize);\n+\t\tconst unsigned int histogramSize =\n+\t\t\tSwIspStats::kYHistogramSize - blackLevelHistIdx;\n+\t\tconst unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount;\n+\t\tconst unsigned int yHistValsPerBinMod =\n+\t\t\thistogramSize / (histogramSize % kExposureBinsCount + 1);\n+\t\tint exposureBins[kExposureBinsCount] = {};\n+\t\tunsigned int denom = 0;\n+\t\tunsigned int num = 0;\n+\n+\t\tif (yHistValsPerBin == 0) {\n+\t\t\tLOG(IPASoftAgcSimple, Debug)\n+\t\t\t\t<< \"Not adjusting exposure due to insufficient histogram data\";\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tfor (unsigned int i = 0; i < histogramSize; i++) {\n+\t\t\tunsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin;\n+\t\t\texposureBins[idx] += histogram[blackLevelHistIdx + i];\n+\t\t}\n+\n+\t\tfor (unsigned int i = 0; i < kExposureBinsCount; i++) {\n+\t\t\tLOG(IPASoftAgcSimple, Debug) << i << \": \" << exposureBins[i];\n+\t\t\tdenom += exposureBins[i];\n+\t\t\tnum += exposureBins[i] * (i + 1);\n+\t\t}\n+\n+\t\tfloat exposureMSV = (denom == 0 ? 0 : static_cast<float>(num) / denom);\n+\t\tupdateExposure(session, state, frameContext, *params, exposureMSV);\n+\t\tnewExposureTime = state.automatic.exposure * session.lineDuration;\n+\t}\n+\n+\tAgcAlgorithm::process(session, frameContext, newExposureTime, metadata);\n+}\n+\n+\n+} /* namespace ipa::soft::algorithms */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/simple/agc_simple.h b/src/ipa/simple/agc_simple.h\nnew file mode 100644\nindex 0000000000..28e414a291\n--- /dev/null\n+++ b/src/ipa/simple/agc_simple.h\n@@ -0,0 +1,59 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Red Hat Inc.\n+ *\n+ * Exposure and gain\n+ */\n+\n+#pragma once\n+\n+#include <optional>\n+\n+#include <libipa/agc.h>\n+\n+#include \"libcamera/internal/software_isp/swisp_stats.h\"\n+\n+namespace libcamera {\n+\n+namespace ipa::soft {\n+\n+class AgcSimpleAlgorithm : public AgcAlgorithm\n+{\n+public:\n+\tstruct Session : AgcAlgorithm::Session {\n+\t\tdouble gain10;\n+\t\tdouble gainMinStep;\n+\t};\n+\n+\tstruct ProcessParams {\n+\t\tint32_t exposure;\n+\t\tdouble gain;\n+\t\tconst SwIspStats &stats;\n+\t\tunsigned int blackLevel;\n+\t};\n+\n+\tint configure(Session &session, ActiveState &state, const ConfigurationParams &config);\n+\n+\tvoid prepare(ActiveState &state, FrameContext &frameContext)\n+\t{\n+\t\treturn AgcAlgorithm::prepare(state, frameContext);\n+\t}\n+\n+\tvoid queueRequest(const Session &session, ActiveState &state,\n+\t\t\t FrameContext &frameContext, const ControlList &controls)\n+\t{\n+\t\treturn AgcAlgorithm::queueRequest(session, state, frameContext, controls);\n+\t}\n+\n+\tvoid process(const Session &session, ActiveState &state,\n+\t\t FrameContext &frameContext, std::optional<ProcessParams> &¶ms,\n+\t\t ControlList &metadata);\n+\n+private:\n+\tvoid updateExposure(const Session &session, ActiveState &state, FrameContext &frameContext,\n+\t\t\t const ProcessParams ¶ms, double exposureMSV);\n+};\n+\n+} /* namepsace ipa::soft */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp\nindex e9bcb2c032..db4f63eba5 100644\n--- a/src/ipa/simple/algorithms/agc.cpp\n+++ b/src/ipa/simple/algorithms/agc.cpp\n@@ -7,124 +7,46 @@\n \n #include \"agc.h\"\n \n-#include <algorithm>\n-#include <cmath>\n-#include <stdint.h>\n-\n #include <libcamera/base/log.h>\n \n-#include \"control_ids.h\"\n-\n namespace libcamera {\n \n LOG_DEFINE_CATEGORY(IPASoftExposure)\n \n namespace ipa::soft::algorithms {\n \n-/*\n- * The number of bins to use for the optimal exposure calculations.\n- */\n-static constexpr unsigned int kExposureBinsCount = 5;\n-\n-/*\n- * The exposure is optimal when the mean sample value of the histogram is\n- * in the middle of the range.\n- */\n-static constexpr float kExposureOptimal = kExposureBinsCount / 2.0;\n-\n-/*\n- * This implements the hysteresis for the exposure adjustment.\n- * It is small enough to have the exposure close to the optimal, and is big\n- * enough to prevent the exposure from wobbling around the optimal value.\n- */\n-static constexpr float kExposureSatisfactory = 0.2;\n-\n-/*\n- * Proportional gain for exposure/gain adjustment. Maps the MSV error to a\n- * multiplicative correction factor:\n- *\n- * factor = 1.0 + kExpProportionalGain * error\n- *\n- * With kExpProportionalGain = 0.04:\n- * - max error ~2.5 -> factor 1.10 (~10% step, same as before)\n- * - error 1.0 -> factor 1.04 (~4% step)\n- * - error 0.3 -> factor 1.012 (~1.2% step)\n- *\n- * This replaces the fixed 10% bang-bang step with a proportional correction\n- * that converges smoothly and avoids overshooting near the target.\n- */\n-static constexpr float kExpProportionalGain = 0.04;\n-\n-/*\n- * Maximum multiplicative step per frame, to bound the correction when the\n- * scene changes dramatically.\n- */\n-static constexpr float kExpMaxStep = 0.15;\n-\n-Agc::Agc()\n+int Agc::init(IPAContext &context, [[maybe_unused]] const ValueNode &tuningData)\n {\n+\treturn agc_.configure(context.configuration.agc.simple, context.activeState.agc.simple, {\n+\t\t.sensor = context.camHelper.get(),\n+\t\t.sensorInfo = context.sensorInfo,\n+\t\t.sensorControls = context.sensorControls,\n+\t\t.ctrlMap = context.ctrlMap,\n+\t\t.autoAllowed = true,\n+\t});\n }\n \n-void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, double exposureMSV)\n+int Agc::configure(IPAContext &context, [[maybe_unused]] const IPAConfigInfo &configInfo)\n {\n-\tint32_t exposure = frameContext.sensor.exposure;\n-\tdouble again = frameContext.sensor.gain;\n-\n-\tdouble error = kExposureOptimal - exposureMSV;\n-\n-\tif (std::abs(error) <= kExposureSatisfactory)\n-\t\treturn;\n-\n-\t/*\n-\t * Compute a proportional correction factor. The sign of the error\n-\t * determines the direction: positive error means too dark (increase),\n-\t * negative means too bright (decrease).\n-\t */\n-\tfloat step = std::clamp(static_cast<float>(error) * kExpProportionalGain,\n-\t\t\t\t-kExpMaxStep, kExpMaxStep);\n-\tfloat factor = 1.0f + step;\n-\n-\tif (factor > 1.0f) {\n-\t\t/* Scene too dark: increase exposure first, then gain. */\n-\t\tif (exposure < context.configuration.agc.exposureMax) {\n-\t\t\tint32_t next = static_cast<int32_t>(exposure * factor);\n-\t\t\texposure = std::max(next, exposure + 1);\n-\t\t} else {\n-\t\t\tdouble next = again * factor;\n-\t\t\tif (next - again < context.configuration.agc.againMinStep)\n-\t\t\t\tagain += context.configuration.agc.againMinStep;\n-\t\t\telse\n-\t\t\t\tagain = next;\n-\t\t}\n-\t} else {\n-\t\t/* Scene too bright: decrease gain first, then exposure. */\n-\t\tif (again > context.configuration.agc.again10) {\n-\t\t\tdouble next = again * factor;\n-\t\t\tif (again - next < context.configuration.agc.againMinStep)\n-\t\t\t\tagain -= context.configuration.agc.againMinStep;\n-\t\t\telse\n-\t\t\t\tagain = next;\n-\t\t} else {\n-\t\t\tint32_t next = static_cast<int32_t>(exposure * factor);\n-\t\t\texposure = std::min(next, exposure - 1);\n-\t\t}\n-\t}\n-\n-\texposure = std::clamp(exposure, context.configuration.agc.exposureMin,\n-\t\t\t context.configuration.agc.exposureMax);\n-\tagain = std::clamp(again, context.configuration.agc.againMin,\n-\t\t\t context.configuration.agc.againMax);\n-\n-\tframeContext.agc.exposure = exposure;\n-\tframeContext.agc.gain = again;\n+\treturn agc_.configure(context.configuration.agc.simple, context.activeState.agc.simple, {\n+\t\t.sensor = context.camHelper.get(),\n+\t\t.sensorInfo = context.sensorInfo,\n+\t\t.sensorControls = context.sensorControls,\n+\t\t.ctrlMap = context.ctrlMap,\n+\t\t.autoAllowed = true, // \\todo not if raw?\n+\t});\n+}\n \n-\tcontext.activeState.agc.exposure = exposure;\n-\tcontext.activeState.agc.again = again;\n+void Agc::queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t frame, IPAFrameContext &frameContext, const ControlList &controls)\n+{\n+\tagc_.queueRequest(context.configuration.agc.simple, context.activeState.agc.simple,\n+\t\t\t frameContext.agc.simple, controls);\n+}\n \n-\tLOG(IPASoftExposure, Debug)\n-\t\t<< \"exposureMSV \" << exposureMSV\n-\t\t<< \" error \" << error << \" factor \" << factor\n-\t\t<< \" exp \" << exposure << \" again \" << again;\n+void Agc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n+\t\t IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)\n+{\n+\tagc_.prepare(context.activeState.agc.simple, frameContext.agc.simple);\n }\n \n void Agc::process(IPAContext &context,\n@@ -133,66 +55,20 @@ void Agc::process(IPAContext &context,\n \t\t const SwIspStats *stats,\n \t\t ControlList &metadata)\n {\n-\tutils::Duration exposureTime =\n-\t\tcontext.configuration.agc.lineDuration * frameContext.sensor.exposure;\n-\tmetadata.set(controls::ExposureTime, exposureTime.get<std::micro>());\n-\tmetadata.set(controls::AnalogueGain, frameContext.sensor.gain);\n-\n-\tif (!context.activeState.agc.valid) {\n-\t\t/*\n-\t\t * Init active-state from sensor values in case updateExposure()\n-\t\t * does not run for the first frame.\n-\t\t */\n-\t\tcontext.activeState.agc.exposure = frameContext.sensor.exposure;\n-\t\tcontext.activeState.agc.again = frameContext.sensor.gain;\n-\t\tcontext.activeState.agc.valid = true;\n-\t}\n-\n-\tif (!stats->valid) {\n-\t\t/*\n-\t\t * Use the new exposure and gain values calculated the last time\n-\t\t * there were valid stats.\n-\t\t */\n-\t\tframeContext.agc.exposure = context.activeState.agc.exposure;\n-\t\tframeContext.agc.gain = context.activeState.agc.again;\n-\t\treturn;\n-\t}\n-\n-\t/*\n-\t * Calculate Mean Sample Value (MSV) according to formula from:\n-\t * https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf\n-\t */\n-\tconst auto &histogram = stats->yHistogram;\n-\tconst unsigned int blackLevelHistIdx =\n-\t\tcontext.activeState.blc.level / (256 / SwIspStats::kYHistogramSize);\n-\tconst unsigned int histogramSize =\n-\t\tSwIspStats::kYHistogramSize - blackLevelHistIdx;\n-\tconst unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount;\n-\tconst unsigned int yHistValsPerBinMod =\n-\t\thistogramSize / (histogramSize % kExposureBinsCount + 1);\n-\tint exposureBins[kExposureBinsCount] = {};\n-\tunsigned int denom = 0;\n-\tunsigned int num = 0;\n-\n-\tif (yHistValsPerBin == 0) {\n-\t\tLOG(IPASoftExposure, Debug)\n-\t\t\t<< \"Not adjusting exposure due to insufficient histogram data\";\n-\t\treturn;\n-\t}\n-\n-\tfor (unsigned int i = 0; i < histogramSize; i++) {\n-\t\tunsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin;\n-\t\texposureBins[idx] += histogram[blackLevelHistIdx + i];\n-\t}\n-\n-\tfor (unsigned int i = 0; i < kExposureBinsCount; i++) {\n-\t\tLOG(IPASoftExposure, Debug) << i << \": \" << exposureBins[i];\n-\t\tdenom += exposureBins[i];\n-\t\tnum += exposureBins[i] * (i + 1);\n+\tif (stats->valid) {\n+\t\tagc_.process(context.configuration.agc.simple, context.activeState.agc.simple, frameContext.agc.simple, {{\n+\t\t\t.exposure = frameContext.sensor.exposure,\n+\t\t\t.gain = frameContext.sensor.gain,\n+\t\t\t.stats = *stats,\n+\t\t\t.blackLevel = context.activeState.blc.level,\n+\t\t}}, metadata);\n+\t} else {\n+\t\tagc_.process(context.configuration.agc.simple, context.activeState.agc.simple,\n+\t\t\t frameContext.agc.simple, {}, metadata);\n \t}\n \n-\tfloat exposureMSV = (denom == 0 ? 0 : static_cast<float>(num) / denom);\n-\tupdateExposure(context, frameContext, exposureMSV);\n+\tframeContext.agc.exposure = frameContext.agc.simple.exposure;\n+\tframeContext.agc.gain = frameContext.agc.simple.gain;\n }\n \n REGISTER_IPA_ALGORITHM(Agc, \"Agc\")\ndiff --git a/src/ipa/simple/algorithms/agc.h b/src/ipa/simple/algorithms/agc.h\nindex 112d9f5a19..869817fc95 100644\n--- a/src/ipa/simple/algorithms/agc.h\n+++ b/src/ipa/simple/algorithms/agc.h\n@@ -8,6 +8,7 @@\n #pragma once\n \n #include \"algorithm.h\"\n+#include \"agc_simple.h\"\n \n namespace libcamera {\n \n@@ -16,16 +17,20 @@ namespace ipa::soft::algorithms {\n class Agc : public Algorithm\n {\n public:\n-\tAgc();\n+\tAgc() = default;\n \t~Agc() = default;\n \n+\tint init(IPAContext &context, const ValueNode &tuningData) override;\n+\tint configure(IPAContext &context, const IPAConfigInfo &configInfo) override;\n+\tvoid queueRequest(IPAContext &context, const uint32_t frame, IPAFrameContext &frameContext, const ControlList &controls) override;\n+\tvoid prepare(IPAContext &context, const uint32_t frame, IPAFrameContext &frameContext, DebayerParams *params) override;\n \tvoid process(IPAContext &context, const uint32_t frame,\n \t\t IPAFrameContext &frameContext,\n \t\t const SwIspStats *stats,\n \t\t ControlList &metadata) override;\n \n private:\n-\tvoid updateExposure(IPAContext &context, IPAFrameContext &frameContext, double exposureMSV);\n+\tAgcSimpleAlgorithm agc_;\n };\n \n } /* namespace ipa::soft::algorithms */\ndiff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h\nindex d35fb1e91d..36e1d8bba8 100644\n--- a/src/ipa/simple/ipa_context.h\n+++ b/src/ipa/simple/ipa_context.h\n@@ -7,7 +7,6 @@\n \n #pragma once\n \n-#include <array>\n #include <optional>\n #include <stdint.h>\n \n@@ -16,19 +15,21 @@\n #include \"libcamera/internal/matrix.h\"\n #include \"libcamera/internal/vector.h\"\n \n+#include <libipa/camera_sensor_helper.h>\n #include <libipa/fc_queue.h>\n \n #include \"core_ipa_interface.h\"\n \n+#include \"agc_simple.h\"\n+\n namespace libcamera {\n \n namespace ipa::soft {\n \n struct IPASessionConfiguration {\n \tstruct {\n-\t\tint32_t exposureMin, exposureMax;\n-\t\tdouble againMin, againMax, again10, againMinStep;\n-\t\tutils::Duration lineDuration;\n+\t\tAgcSimpleAlgorithm::Session simple;\n+\t\tdouble again10, againMinStep;\n \t} agc;\n \tstruct {\n \t\tstd::optional<uint8_t> level;\n@@ -37,9 +38,7 @@ struct IPASessionConfiguration {\n \n struct IPAActiveState {\n \tstruct {\n-\t\tint32_t exposure;\n-\t\tdouble again;\n-\t\tbool valid;\n+\t\tAgcSimpleAlgorithm::ActiveState simple;\n \t} agc;\n \n \tstruct {\n@@ -67,6 +66,7 @@ struct IPAFrameContext : public FrameContext {\n \tMatrix<float, 3, 3> ccm;\n \n \tstruct {\n+\t\tAgcSimpleAlgorithm::FrameContext simple;\n \t\tint32_t exposure;\n \t\tdouble gain;\n \t} agc;\n@@ -90,10 +90,12 @@ struct IPAContext {\n \t}\n \n \tIPACameraSensorInfo sensorInfo;\n+\tControlInfoMap sensorControls;\n \tIPASessionConfiguration configuration;\n \tIPAActiveState activeState;\n \tFCQueue<IPAFrameContext> frameContexts;\n \tControlInfoMap::Map ctrlMap;\n+\tstd::unique_ptr<CameraSensorHelper> camHelper;\n \tbool ccmEnabled = false;\n };\n \ndiff --git a/src/ipa/simple/meson.build b/src/ipa/simple/meson.build\nindex 2f9f15f4aa..3033e91763 100644\n--- a/src/ipa/simple/meson.build\n+++ b/src/ipa/simple/meson.build\n@@ -6,6 +6,7 @@ subdir('data')\n ipa_name = 'ipa_soft_simple'\n \n soft_simple_sources = files([\n+ 'agc_simple.cpp',\n 'ipa_context.cpp',\n 'soft_simple.cpp',\n ])\ndiff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp\nindex 22702638bb..5c85b3986f 100644\n--- a/src/ipa/simple/soft_simple.cpp\n+++ b/src/ipa/simple/soft_simple.cpp\n@@ -5,7 +5,6 @@\n * Simple Software Image Processing Algorithm module\n */\n \n-#include <chrono>\n #include <stdint.h>\n #include <sys/mman.h>\n \n@@ -34,8 +33,6 @@\n namespace libcamera {\n LOG_DEFINE_CATEGORY(IPASoft)\n \n-using namespace std::literals::chrono_literals;\n-\n namespace ipa::soft {\n \n /* Maximum number of frame contexts to be held */\n@@ -76,8 +73,6 @@ private:\n \n \tDebayerParams *params_;\n \tSwIspStats *stats_;\n-\tstd::unique_ptr<CameraSensorHelper> camHelper_;\n-\tControlInfoMap sensorInfoMap_;\n \n \t/* Local parameter storage */\n \tstruct IPAContext context_;\n@@ -99,14 +94,15 @@ int IPASoftSimple::init(const IPASettings &settings,\n \t\t\tControlInfoMap *ipaControls,\n \t\t\tbool *ccmEnabled)\n {\n-\tcamHelper_ = CameraSensorHelperFactoryBase::create(settings.sensorModel);\n-\tif (!camHelper_) {\n+\tcontext_.camHelper = CameraSensorHelperFactoryBase::create(settings.sensorModel);\n+\tif (!context_.camHelper) {\n \t\tLOG(IPASoft, Warning)\n \t\t\t<< \"Failed to create camera sensor helper for \"\n \t\t\t<< settings.sensorModel;\n \t}\n \n \tcontext_.sensorInfo = sensorInfo;\n+\tcontext_.sensorControls = sensorControls;\n \n \t/* Load the tuning data file */\n \tFile file(settings.configurationFile);\n@@ -201,38 +197,15 @@ int IPASoftSimple::init(const IPASettings &settings,\n \n int IPASoftSimple::configure(const IPAConfigInfo &configInfo)\n {\n-\tsensorInfoMap_ = configInfo.sensorControls;\n-\n-\tconst ControlInfo &exposureInfo = sensorInfoMap_.find(V4L2_CID_EXPOSURE)->second;\n-\tconst ControlInfo &gainInfo = sensorInfoMap_.find(V4L2_CID_ANALOGUE_GAIN)->second;\n+\tcontext_.sensorControls = configInfo.sensorControls;\n \n \t/* Clear the IPA context before the streaming session. */\n \tcontext_.configuration = {};\n \tcontext_.activeState = {};\n \tcontext_.frameContexts.clear();\n \n-\tcontext_.configuration.agc.lineDuration =\n-\t\tcontext_.sensorInfo.minLineLength * 1.0s / context_.sensorInfo.pixelRate;\n-\tcontext_.configuration.agc.exposureMin = exposureInfo.min().get<int32_t>();\n-\tcontext_.configuration.agc.exposureMax = exposureInfo.max().get<int32_t>();\n-\tif (!context_.configuration.agc.exposureMin) {\n-\t\tLOG(IPASoft, Warning) << \"Minimum exposure is zero, that can't be linear\";\n-\t\tcontext_.configuration.agc.exposureMin = 1;\n-\t}\n-\n-\tint32_t againMin = gainInfo.min().get<int32_t>();\n-\tint32_t againMax = gainInfo.max().get<int32_t>();\n-\tint32_t againDef = gainInfo.def().get<int32_t>();\n-\n-\tif (camHelper_) {\n-\t\tcontext_.configuration.agc.againMin = camHelper_->gain(againMin);\n-\t\tcontext_.configuration.agc.againMax = camHelper_->gain(againMax);\n-\t\tcontext_.configuration.agc.again10 = std::max(context_.configuration.agc.againMin, 1.0);\n-\t\tcontext_.configuration.agc.againMinStep =\n-\t\t\t(context_.configuration.agc.againMax -\n-\t\t\t context_.configuration.agc.againMin) /\n-\t\t\t100.0;\n-\t\tif (camHelper_->blackLevel().has_value()) {\n+\tif (context_.camHelper) {\n+\t\tif (context_.camHelper->blackLevel().has_value()) {\n \t\t\t/*\n \t\t\t * The black level from camHelper_ is a 16 bit value, software ISP\n \t\t\t * works with 8 bit pixel values, both regardless of the actual\n@@ -240,13 +213,8 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo)\n \t\t\t * by dividing the value from the helper by 256.\n \t\t\t */\n \t\t\tcontext_.configuration.black.level =\n-\t\t\t\tcamHelper_->blackLevel().value() / 256;\n+\t\t\t\tcontext_.camHelper->blackLevel().value() / 256;\n \t\t}\n-\t} else {\n-\t\tcontext_.configuration.agc.againMax = againMax;\n-\t\tcontext_.configuration.agc.again10 = againDef;\n-\t\tcontext_.configuration.agc.againMin = againMin;\n-\t\tcontext_.configuration.agc.againMinStep = 1.0;\n \t}\n \n \tfor (const auto &algo : algorithms()) {\n@@ -255,13 +223,6 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo)\n \t\t\treturn ret;\n \t}\n \n-\tLOG(IPASoft, Info)\n-\t\t<< \"Exposure \" << context_.configuration.agc.exposureMin << \"-\"\n-\t\t<< context_.configuration.agc.exposureMax\n-\t\t<< \", gain \" << context_.configuration.agc.againMin << \"-\"\n-\t\t<< context_.configuration.agc.againMax\n-\t\t<< \" (\" << context_.configuration.agc.againMinStep << \")\";\n-\n \treturn 0;\n }\n \n@@ -311,17 +272,17 @@ void IPASoftSimple::processStats(const uint32_t frame,\n \tframeContext.sensor.exposure =\n \t\tsensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();\n \tint32_t again = sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();\n-\tframeContext.sensor.gain = camHelper_ ? camHelper_->gain(again) : again;\n+\tframeContext.sensor.gain = context_.camHelper ? context_.camHelper->gain(again) : again;\n \n \tControlList metadata(controls::controls);\n \tfor (const auto &algo : algorithms())\n \t\talgo->process(context_, frame, frameContext, stats_, metadata);\n \tmetadataReady.emit(frame, metadata);\n \n-\tControlList ctrls(sensorInfoMap_);\n+\tControlList ctrls(context_.sensorControls);\n \n-\tint32_t againNew = camHelper_\n-\t\t? camHelper_->gainCode(frameContext.agc.gain)\n+\tint32_t againNew = context_.camHelper\n+\t\t? context_.camHelper->gainCode(frameContext.agc.gain)\n \t\t: static_cast<int32_t>(frameContext.agc.gain);\n \tctrls.set(V4L2_CID_EXPOSURE, frameContext.agc.exposure);\n \tctrls.set(V4L2_CID_ANALOGUE_GAIN, againNew);\n", "prefixes": [ "RFC", "v1", "16/17" ] }