{"id":27492,"url":"https://patchwork.libcamera.org/api/patches/27492/?format=json","web_url":"https://patchwork.libcamera.org/patch/27492/","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-44-barnabas.pocze@ideasonboard.com>","date":"2026-07-23T15:43:26","name":"[RFC,v2,43/43] ipa: simple: agc: Port to `AgcAlgorithm`","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"27a441b57d62324dcd0f3ef46650d755d823beea","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/27492/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/27492/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/27492/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 46CCAC333C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 23 Jul 2026 15:44:29 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id BE6D267F75;\n\tThu, 23 Jul 2026 17:44:28 +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 5C26B67ED0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 23 Jul 2026 17:43:41 +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 7D92923B9\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 23 Jul 2026 17:42: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=\"SpFERWt0\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1784821360;\n\tbh=/stMkR+OSyGOyMTcTHMBqzUc6Ain7zBYox2nE68AigY=;\n\th=From:To:Subject:Date:In-Reply-To:References:From;\n\tb=SpFERWt0DctzXOgV7w/njLDwAjUylSb389em/L0y08yqpVyqlXpjfrWcLKChipTSD\n\tTmQ9YkEfgQ/GdP69z56K8oZNBhuSe3J/stBACUeOluKUy4++kWoyZy6vgRUhZBWdz6\n\tQWTN9Y5BVhv8nPsrIOlELqsniKnxLxXnizAtfjH8=","From":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Subject":"[RFC PATCH v2 43/43] ipa: simple: agc: Port to `AgcAlgorithm`","Date":"Thu, 23 Jul 2026 17:43:26 +0200","Message-ID":"<20260723154327.1357866-44-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":"Use the `AgcAlgorithm` class to implement the agc algorithm.\n\nSigned-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>\n---\n src/ipa/simple/algorithms/agc.cpp | 134 +++++++++++++++++-------------\n src/ipa/simple/algorithms/agc.h   |  12 ++-\n src/ipa/simple/ipa_context.h      |  15 +---\n src/ipa/simple/soft_simple.cpp    |  55 +-----------\n 4 files changed, 93 insertions(+), 123 deletions(-)","diff":"diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp\nindex 7b980c7382..927bc6ab22 100644\n--- a/src/ipa/simple/algorithms/agc.cpp\n+++ b/src/ipa/simple/algorithms/agc.cpp\n@@ -13,30 +13,76 @@\n \n #include <libipa/histogram.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+namespace {\n+\n+class AgcTraits : public AgcMeanLuminance::Traits\n+{\n+public:\n+        AgcTraits(const Histogram &yHist)\n+                : yHist_(yHist)\n+        {\n+        }\n+\n+        double estimateLuminance(double gain) const override\n+        {\n+\t\t/*\n+\t\t * TODO: Improve by asking the weight of saturating and non-saturating\n+\t\t * bins directly from the histogram\n+\t\t */\n+\t\tdouble sum = 0;\n+\n+\t\tfor (size_t i = 0; i < yHist_.bins(); i++)\n+\t\t\tsum += std::min<double>(yHist_.bins(), i * gain) * yHist_[i];\n+\n+\t\treturn sum / yHist_.total() / yHist_.bins();\n+        }\n+\n+private:\n+        const Histogram &yHist_;\n+};\n+\n+} /* namespace */\n+\n+int Agc::init(IPAContext &context, const ValueNode &tuningData)\n+{\n+\tint ret = agc_.init(tuningData, context.camHelper.get());\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn agc_.configure(context.configuration.agc, context.activeState.agc, {\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 int Agc::configure(IPAContext &context, [[maybe_unused]] const IPAConfigInfo &configInfo)\n {\n-\tagc_.setLimits({\n-\t\t.exposure = {\n-\t\t\tcontext.configuration.agc.exposureMin,\n-\t\t\tcontext.configuration.agc.exposureMax,\n-\t\t},\n-\t\t.gain = {\n-\t\t\tcontext.configuration.agc.againMin,\n-\t\t\tcontext.configuration.agc.againMax,\n-\t\t},\n-\t\t.gainMinStep = context.configuration.agc.againMinStep,\n-\t\t.gain1 = context.configuration.agc.again10,\n+\treturn agc_.configure(context.configuration.agc, context.activeState.agc, {\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::queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n+\t\t       IPAFrameContext &frameContext, const ControlList &controls)\n+{\n+\tagc_.queueRequest(context.configuration.agc, context.activeState.agc, frameContext.agc, controls);\n+}\n \n-\treturn 0;\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, frameContext.agc);\n }\n \n void Agc::process(IPAContext &context,\n@@ -45,49 +91,25 @@ 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+\tif (stats->valid) {\n+\t\tauto histogram = stats->yHistogram;\n+\n+\t\tconst unsigned int blackLevelHistIdx =\n+\t\t\tcontext.activeState.blc.level * std::size(histogram) / 256;\n+\t\tfor (size_t i = 1; i < blackLevelHistIdx; i++)\n+\t\t\thistogram[0] += std::exchange(histogram[i], 0);\n+\n+\t\tHistogram yHist(histogram);\n+\n+\t\tagc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{\n+\t\t\t.traits = AgcTraits(yHist),\n+\t\t\t.yHist = yHist,\n+\t\t\t.exposure = frameContext.sensor.exposure,\n+\t\t\t.gain = frameContext.sensor.gain,\n+\t\t}}, metadata);\n+\t} else {\n+\t\tagc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);\n \t}\n-\n-\tauto histogram = stats->yHistogram;\n-\tconst unsigned int blackLevelHistIdx =\n-\t\tcontext.activeState.blc.level * std::size(histogram) / 256;\n-\n-\tfor (unsigned int i = 1; i < blackLevelHistIdx; i++)\n-\t\thistogram[0] += std::exchange(histogram[i], 0);\n-\n-\tconst auto &newEv = agc_.calculateNewEv({\n-\t\t.yHist = { histogram },\n-\t\t.exposure = frameContext.sensor.exposure,\n-\t\t.gain = frameContext.sensor.gain,\n-\t});\n-\n-\tframeContext.agc.exposure = newEv.exposure;\n-\tframeContext.agc.gain = newEv.analogueGain;\n-\n-\tcontext.activeState.agc.exposure = frameContext.agc.exposure;\n-\tcontext.activeState.agc.again = frameContext.agc.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 2e156e135c..6e37ce6eff 100644\n--- a/src/ipa/simple/algorithms/agc.h\n+++ b/src/ipa/simple/algorithms/agc.h\n@@ -9,7 +9,7 @@\n \n #include \"algorithm.h\"\n \n-#include <libipa/agc_msv.h>\n+#include <libipa/agc.h>\n \n namespace libcamera {\n \n@@ -18,15 +18,23 @@ namespace ipa::soft::algorithms {\n class Agc : public Algorithm\n {\n public:\n+\tint init(IPAContext &context, const ValueNode &tuningData) override;\n+\n \tint configure(IPAContext &context, const IPAConfigInfo &configInfo) override;\n \n+\tvoid queueRequest(IPAContext &context, const uint32_t frame,\n+\t\t\t  IPAFrameContext &frameContext, const ControlList &controls) override;\n+\n+\tvoid prepare(IPAContext &context, const uint32_t frame,\n+\t\t     IPAFrameContext &frameContext, DebayerParams *params) override;\n+\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-\tAgcMSV agc_;\n+\tAgcAlgorithm 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 96b3c5b21d..63889cd8f6 100644\n--- a/src/ipa/simple/ipa_context.h\n+++ b/src/ipa/simple/ipa_context.h\n@@ -17,6 +17,7 @@\n #include \"libcamera/internal/matrix.h\"\n #include \"libcamera/internal/vector.h\"\n \n+#include <libipa/agc.h>\n #include <libipa/camera_sensor_helper.h>\n #include <libipa/fc_queue.h>\n \n@@ -27,10 +28,7 @@ namespace libcamera {\n namespace ipa::soft {\n \n struct IPASessionConfiguration {\n-\tstruct {\n-\t\tuint32_t exposureMin, exposureMax;\n-\t\tdouble againMin, againMax, again10, againMinStep;\n-\t\tutils::Duration lineDuration;\n+\tstruct Agc : agc::Session {\n \t} agc;\n \tstruct {\n \t\tstd::optional<uint8_t> level;\n@@ -38,10 +36,7 @@ struct IPASessionConfiguration {\n };\n \n struct IPAActiveState {\n-\tstruct {\n-\t\tuint32_t exposure;\n-\t\tdouble again;\n-\t\tbool valid;\n+\tstruct Agc : agc::ActiveState {\n \t} agc;\n \n \tstruct {\n@@ -68,9 +63,7 @@ struct IPAActiveState {\n struct IPAFrameContext : public FrameContext {\n \tMatrix<float, 3, 3> ccm;\n \n-\tstruct {\n-\t\tuint32_t exposure;\n-\t\tdouble gain;\n+\tstruct Agc : agc::FrameContext {\n \t} agc;\n \n \tstruct {\ndiff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp\nindex 8b5df884d4..f35e269deb 100644\n--- a/src/ipa/simple/soft_simple.cpp\n+++ b/src/ipa/simple/soft_simple.cpp\n@@ -107,6 +107,7 @@ int IPASoftSimple::init(const IPASettings &settings,\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@@ -180,22 +181,6 @@ int IPASoftSimple::init(const IPASettings &settings,\n \tControlInfoMap::Map ctrlMap = context_.ctrlMap;\n \t*ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls);\n \n-\t/*\n-\t * Check if the sensor driver supports the controls required by the\n-\t * Soft IPA.\n-\t * Don't save the min and max control values yet, as e.g. the limits\n-\t * for V4L2_CID_EXPOSURE depend on the configured sensor resolution.\n-\t */\n-\tif (sensorControls.find(V4L2_CID_EXPOSURE) == sensorControls.end()) {\n-\t\tLOG(IPASoft, Error) << \"Don't have exposure control\";\n-\t\treturn -EINVAL;\n-\t}\n-\n-\tif (sensorControls.find(V4L2_CID_ANALOGUE_GAIN) == sensorControls.end()) {\n-\t\tLOG(IPASoft, Error) << \"Don't have gain control\";\n-\t\treturn -EINVAL;\n-\t}\n-\n \treturn 0;\n }\n \n@@ -203,55 +188,17 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo, ControlInfoMap *ip\n {\n \tcontext_.sensorControls = configInfo.sensorControls;\n \n-\tconst ControlInfo &exposureInfo = context_.sensorControls.find(V4L2_CID_EXPOSURE)->second;\n-\tconst ControlInfo &gainInfo = context_.sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;\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 (context_.camHelper) {\n-\t\tcontext_.configuration.agc.againMin = context_.camHelper->gain(againMin);\n-\t\tcontext_.configuration.agc.againMax = context_.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} 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 \t\tint ret = algo->configure(context_, configInfo);\n \t\tif (ret)\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 \t*ipaControls = { ControlInfoMap::Map(context_.ctrlMap), controls::controls };\n \n \treturn 0;\n","prefixes":["RFC","v2","43/43"]}