From patchwork Mon Nov 9 09:49:36 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28799 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 43B2CC3307 for ; Mon, 9 Nov 2026 09:50:08 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 321D2691DD; Mon, 9 Nov 2026 10:50:01 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="NlD2Ytgz"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 7545D690F0 for ; Mon, 9 Nov 2026 10:49:58 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id E0109981; Mon, 9 Nov 2026 10:47:36 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217657; bh=Fn337qqjgerBHCHoSE9nbm8n76qMFjXFXI9MR1A++wc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=NlD2YtgzzJjQSgTK4Tr7JmixqdCiVCo+9avYGz9MXHPl8zWspHNi4xwg5wD9Rd3Wz GnejEdmI3JxDmy6SixOB4br/RtrzMsWrOr5eP9bG28psPLBGuCnUEjp+L33F2Njj5F QlikTPBQie2NbjXkUM7HbZ8bzn4pfsdSTS9cwN3k= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham Subject: [PATCH v7 01/19] ipa: libipa: exposure_mode_helper: Remove defaulted destructor Date: Mon, 9 Nov 2026 10:49:36 +0100 Message-ID: <20261109094954.341480-2-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" It does not have any effect, the destructor will be defaulted implicitly, so remove it. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham --- src/ipa/libipa/exposure_mode_helper.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ipa/libipa/exposure_mode_helper.h b/src/ipa/libipa/exposure_mode_helper.h index a56e19061b..d0d468a6a2 100644 --- a/src/ipa/libipa/exposure_mode_helper.h +++ b/src/ipa/libipa/exposure_mode_helper.h @@ -23,7 +23,6 @@ class ExposureModeHelper { public: ExposureModeHelper(std::span> stages); - ~ExposureModeHelper() = default; void configure(utils::Duration lineLength, const CameraSensorHelper *sensorHelper); void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime, From patchwork Mon Nov 9 09:49:37 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28800 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id B39B2C3307 for ; Mon, 9 Nov 2026 09:50:12 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 6E25569211; Mon, 9 Nov 2026 10:50:02 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="IbxOlfc9"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id BF93A68FD7 for ; Mon, 9 Nov 2026 10:49:58 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 390B9802; Mon, 9 Nov 2026 10:47:37 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217657; bh=clkJiGgh/FuVSK4PLxTFbJvxY9SKo4aS+No2DySBV6A=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=IbxOlfc9NdhDuz+WMjYHLhyJ28zDy0s4UyjA8KPguuMhscpF0xO/xR1fKorPsYX63 vsm/sWgT1jonsONBagrn2bGlVHoxlsQaZLKMjRBtabNGMh09Uk4XfCgmqtWuFQq/wr YntlCzcSnCJ4//CvMP+YV3qsdRy/5hgzlIcO2syc= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Stefan Klug Subject: [PATCH v7 02/19] ipa: libipa: exposure_mode_helper: Keep stage params together Date: Mon, 9 Nov 2026 10:49:37 +0100 Message-ID: <20261109094954.341480-3-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a new type `Stage` that contains both the exposure time and gain in one place, and have a single vector of `Stage`s instead of the two same size exposure time / gain vectors. Also clarify in the documentation that the gain of a stage only constrains the analogue gain at the moment. Link: https://gitlab.freedesktop.org/camera/libcamera/-/work_items/373 Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc_mean_luminance.cpp | 12 ++++----- src/ipa/libipa/exposure_mode_helper.cpp | 33 +++++++++++++++---------- src/ipa/libipa/exposure_mode_helper.h | 12 +++++---- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp index 5b0a6e2594..a763a3b94d 100644 --- a/src/ipa/libipa/agc_mean_luminance.cpp +++ b/src/ipa/libipa/agc_mean_luminance.cpp @@ -364,15 +364,15 @@ int AgcMeanLuminance::parseExposureModes(const ValueNode &tuningData) return -EINVAL; } - std::vector> stages; - for (unsigned int i = 0; i < exposureTimes.size(); i++) { + std::vector stages; + for (size_t i = 0; i < exposureTimes.size(); i++) { stages.push_back({ - std::chrono::microseconds(exposureTimes[i]), - gains[i] + .exposureTime = std::chrono::microseconds(exposureTimes[i]), + .gain = gains[i], }); } - exposureModeHelpers_.try_emplace(*modeValue, stages); + exposureModeHelpers_.try_emplace(*modeValue, std::move(stages)); } } @@ -384,7 +384,7 @@ int AgcMeanLuminance::parseExposureModes(const ValueNode &tuningData) */ if (exposureModeHelpers_.empty()) exposureModeHelpers_.try_emplace(controls::AeExposureModeNormal, - std::span>{}); + std::vector{}); return 0; } diff --git a/src/ipa/libipa/exposure_mode_helper.cpp b/src/ipa/libipa/exposure_mode_helper.cpp index b3e75f6358..059792af28 100644 --- a/src/ipa/libipa/exposure_mode_helper.cpp +++ b/src/ipa/libipa/exposure_mode_helper.cpp @@ -59,24 +59,31 @@ namespace ipa { * used. */ +/** + * \class ExposureModeHelper::Stage + * \brief Details of an exposure stage + * + * This type contains the parameters of a single exposure stage as used by the + * algorithm. + * + * \var ExposureModeHelper::Stage::exposureTime + * \brief The maximum exposure time of the stage + * + * \var ExposureModeHelper::Stage::gain + * \brief The maximum analogue gain of the stage + */ + /** * \brief Construct an ExposureModeHelper instance * \param[in] stages The vector of paired exposure time and gain limits * - * The input stages are exposure time and _total_ gain pairs; the gain - * encompasses both analogue and digital gain. - * * The vector of stages may be empty. In that case, the helper will simply use * the runtime limits set through setLimits() instead. */ -ExposureModeHelper::ExposureModeHelper(std::span> stages) - : lineDuration_(1us), minExposureTime_(0us), maxExposureTime_(0us), - minGain_(0), maxGain_(0), sensorHelper_(nullptr) +ExposureModeHelper::ExposureModeHelper(std::vector &&stages) + : stages_(std::move(stages)), lineDuration_(1us), minExposureTime_(0us), + maxExposureTime_(0us), minGain_(0), maxGain_(0), sensorHelper_(nullptr) { - for (const auto &[s, g] : stages) { - exposureTimes_.push_back(s); - gains_.push_back(g); - } } /** @@ -236,10 +243,10 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const double stageGain = clampGain(1.0); double lastStageGain = stageGain; - for (unsigned int stage = 0; stage < gains_.size(); stage++) { - utils::Duration stageExposureTime = clampExposureTime(exposureTimes_[stage], + for (const auto &stage : stages_) { + utils::Duration stageExposureTime = clampExposureTime(stage.exposureTime, &quantGain); - stageGain = clampGain(gains_[stage]); + stageGain = clampGain(stage.gain); /* * We perform the clamping on both exposure time and gain in diff --git a/src/ipa/libipa/exposure_mode_helper.h b/src/ipa/libipa/exposure_mode_helper.h index d0d468a6a2..1a77bf4da8 100644 --- a/src/ipa/libipa/exposure_mode_helper.h +++ b/src/ipa/libipa/exposure_mode_helper.h @@ -7,8 +7,6 @@ #pragma once -#include -#include #include #include @@ -22,7 +20,12 @@ namespace ipa { class ExposureModeHelper { public: - ExposureModeHelper(std::span> stages); + struct Stage { + utils::Duration exposureTime; + double gain; + }; + + ExposureModeHelper(std::vector &&stages); void configure(utils::Duration lineLength, const CameraSensorHelper *sensorHelper); void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime, @@ -47,8 +50,7 @@ private: double *quantizationGain = nullptr) const; double clampGain(double gain, double *quantizationGain = nullptr) const; - std::vector exposureTimes_; - std::vector gains_; + std::vector stages_; utils::Duration lineDuration_; utils::Duration minExposureTime_; From patchwork Mon Nov 9 09:49:38 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28801 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 9C46FC3307 for ; Mon, 9 Nov 2026 09:50:15 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A40D3691E3; Mon, 9 Nov 2026 10:50:03 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="QXPpFlLF"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 0F00F690F0 for ; Mon, 9 Nov 2026 10:49:59 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 7E07BC23; Mon, 9 Nov 2026 10:47:37 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217657; bh=BIpYFm8nA2wckzbLdUxOZrqBY/uiKzwYn2ams6doWYU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=QXPpFlLFGyKxXUwlQwF6ArPr076BaZCTdQSGFHFhJvk7TevCYCLosHtdZRXvs3VB0 O0uuTCJtb7VGs9RVdJQ2YvlOM1bzqZBgG+yRMYXst+hboi2F2wQsugYDf1UaiBaEiH fkgB5ODSIQytxD6yx0nfU1nV3B301N/4pRb9FtVE= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham Subject: [PATCH v7 03/19] ipa: libipa: exposure_mode_helper: Remove limit getters Date: Mon, 9 Nov 2026 10:49:38 +0100 Message-ID: <20261109094954.341480-4-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" These getters have not been used since their introduction, so remove them for now. They can be easily restored if needed. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham --- src/ipa/libipa/exposure_mode_helper.cpp | 25 ------------------------- src/ipa/libipa/exposure_mode_helper.h | 5 ----- 2 files changed, 30 deletions(-) diff --git a/src/ipa/libipa/exposure_mode_helper.cpp b/src/ipa/libipa/exposure_mode_helper.cpp index 059792af28..4ec31f54c3 100644 --- a/src/ipa/libipa/exposure_mode_helper.cpp +++ b/src/ipa/libipa/exposure_mode_helper.cpp @@ -294,31 +294,6 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const exposure / (exposureTime * gain * quantGain) }; } -/** - * \fn ExposureModeHelper::minExposureTime() - * \brief Retrieve the configured minimum exposure time limit set through - * setLimits() - * \return The minExposureTime_ value - */ - -/** - * \fn ExposureModeHelper::maxExposureTime() - * \brief Retrieve the configured maximum exposure time set through setLimits() - * \return The maxExposureTime_ value - */ - -/** - * \fn ExposureModeHelper::minGain() - * \brief Retrieve the configured minimum gain set through setLimits() - * \return The minGain_ value - */ - -/** - * \fn ExposureModeHelper::maxGain() - * \brief Retrieve the configured maximum gain set through setLimits() - * \return The maxGain_ value - */ - } /* namespace ipa */ } /* namespace libcamera */ diff --git a/src/ipa/libipa/exposure_mode_helper.h b/src/ipa/libipa/exposure_mode_helper.h index 1a77bf4da8..d33a0fd822 100644 --- a/src/ipa/libipa/exposure_mode_helper.h +++ b/src/ipa/libipa/exposure_mode_helper.h @@ -40,11 +40,6 @@ public: [[nodiscard]] Result splitExposure(utils::Duration exposure) const; - utils::Duration minExposureTime() const { return minExposureTime_; } - utils::Duration maxExposureTime() const { return maxExposureTime_; } - double minGain() const { return minGain_; } - double maxGain() const { return maxGain_; } - private: utils::Duration clampExposureTime(utils::Duration exposureTime, double *quantizationGain = nullptr) const; From patchwork Mon Nov 9 09:49:39 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28802 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 7763EC3307 for ; Mon, 9 Nov 2026 09:50:17 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 845D46A7B2; Mon, 9 Nov 2026 10:50:06 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="fxpzRD3O"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 670A1691D6 for ; Mon, 9 Nov 2026 10:49:59 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id C8990DA8; Mon, 9 Nov 2026 10:47:37 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217658; bh=983etOA7LW5SOvTBABYa7kFzNUjBv8F7W3PdSSmA1ps=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=fxpzRD3OqVlPd7M3aHBY+jLD159e8UN8tezHFe22S9kTnON8bAhq9dhxBJPGB2ucX BED2KrC9fv62vgjnAr341CxRSK/vA3pVon8pRUQi20g6rcxwBLB4ph4Om3qu6Fgp7I bdhBH2qoZoT6Hu8BrA5YqzXsnchbHAfaZbafmjpY= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [PATCH v7 04/19] ipa: libipa: agc: Move tuning file parsing Date: Mon, 9 Nov 2026 10:49:39 +0100 Message-ID: <20261109094954.341480-5-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Move the tuning file parsing out of `AgcMeanLuminance`, and provide the configuration via the constructor. This allows `AgcAlgorithm` to inspect the configuration, specifically it can check if any exposure modes have been defined. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi --- src/ipa/libipa/agc.cpp | 305 +++++++++++++++++++++++- src/ipa/libipa/agc_mean_luminance.cpp | 318 ++------------------------ src/ipa/libipa/agc_mean_luminance.h | 22 +- 3 files changed, 326 insertions(+), 319 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 182dd7380e..38bf90879d 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include @@ -26,6 +28,8 @@ #include +#include "exposure_mode_helper.h" + /** * \file agc.h * \brief libipa AGC algorithm @@ -222,6 +226,245 @@ namespace agc { } /* namespace agc */ +namespace { + +/* + * Default relative luminance target + * + * This value should be chosen so that when the camera points at a grey target, + * the resulting image brightness looks "right". Custom values can be passed + * as the relativeLuminanceTarget value in sensor tuning files. + */ +static constexpr double kDefaultRelativeLuminanceTarget = 0.16; + +int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +{ + auto &target = tuningData["relativeLuminanceTarget"]; + if (!target) { + config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } }; + return 0; + } + + auto pwl = target.get(); + if (!pwl) { + LOG(Agc, Error) + << "Failed to load relative luminance target."; + return -EINVAL; + } + + config.relativeLuminanceTarget = std::move(*pwl); + return 0; +} + +int parseConstraint(AgcMeanLuminance::Configuration &config, const ValueNode &modeDict, int32_t id) +{ + for (const auto &[boundName, content] : modeDict.asDict()) { + if (boundName != "upper" && boundName != "lower") { + LOG(Agc, Warning) + << "Ignoring unknown constraint bound '" << boundName << "'"; + continue; + } + + unsigned int idx = static_cast(boundName == "upper"); + auto bound = static_cast(idx); + double qLo = content["qLo"].get().value_or(0.98); + double qHi = content["qHi"].get().value_or(1.0); + auto yTarget = content["yTarget"].get(); + if (!yTarget) { + LOG(Agc, Error) + << "Failed to parse yTarget"; + return -EINVAL; + } + + AgcMeanLuminance::AgcConstraint constraint = { + bound, + qLo, + qHi, + std::move(*yTarget), + }; + auto &constraints = config.constraintModes[id]; + + if (idx) + constraints.push_back(std::move(constraint)); + else + constraints.insert(constraints.begin(), std::move(constraint)); + } + + return 0; +} + +int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +{ + static constexpr std::tuple compat[] = { + { "ConstraintNormal", "Normal", controls::AeConstraintModeNormal }, + { "ConstraintHighlight", "Highlight", controls::AeConstraintModeHighlight }, + { "ConstraintShadows", "Shadows", controls::AeConstraintModeShadows }, + { "ConstraintCustom", "Custom", controls::AeConstraintModeCustom }, + }; + + const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()]; + if (constraintModes.isDictionary()) { + for (const auto &[modeName, modeDict] : constraintModes.asDict()) { + std::optional modeValue; + + auto it = controls::AeConstraintModeNameValueMap.find(modeName); + if (it == controls::AeConstraintModeNameValueMap.end()) { + for (const auto &[oldName, newName, v] : compat) { + if (modeName != oldName) + continue; + + modeValue = v; + + LOG(Agc, Warning) + << "Constraint mode '" << oldName << "' has been renamed to '" + << newName << "', please fix the tuning file"; + break; + } + } else { + modeValue = static_cast(it->second); + } + + if (!modeValue) { + LOG(Agc, Warning) + << "Skipping unknown constraint mode '" << modeName << "'"; + continue; + } + + if (!modeDict.isDictionary()) { + LOG(Agc, Error) + << "Invalid constraint mode '" << modeName << "'"; + return -EINVAL; + } + + int ret = parseConstraint(config, modeDict, *modeValue); + if (ret) + return ret; + } + } + + /* + * If the tuning data file contains no constraints then we use the + * default constraint that the IPU3/RkISP1 Agc algorithms were adhering + * to anyway before centralisation; this constraint forces the top 2% of + * the histogram to be at least 0.5. + */ + if (config.constraintModes.empty()) { + AgcMeanLuminance::AgcConstraint constraint = { + AgcMeanLuminance::AgcConstraint::Bound::Lower, + 0.98, + 1.0, + Pwl({ { { 0.0, 0.5 } } }), + }; + + config.constraintModes[controls::AeConstraintModeNormal].push_back(std::move(constraint)); + } + + return 0; +} + +int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +{ + static constexpr std::tuple compat[] = { + { "ExposureNormal", "Normal", controls::AeExposureModeNormal }, + { "ExposureShort", "Short", controls::AeExposureModeShort }, + { "ExposureLong", "Long", controls::AeExposureModeLong }, + { "ExposureCustom", "Custom", controls::AeExposureModeCustom }, + }; + + const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()]; + if (exposureModes.isDictionary()) { + for (const auto &[modeName, modeValues] : exposureModes.asDict()) { + std::optional modeValue; + + auto it = controls::AeExposureModeNameValueMap.find(modeName); + if (it == controls::AeExposureModeNameValueMap.end()) { + for (const auto &[oldName, newName, v] : compat) { + if (modeName != oldName) + continue; + + modeValue = v; + + LOG(Agc, Warning) + << "Exposure mode '" << oldName << "' has been renamed to '" + << newName << "', please fix the tuning file"; + break; + } + } else { + modeValue = static_cast(it->second); + } + + if (!modeValue) { + LOG(Agc, Warning) + << "Skipping unknown exposure mode '" << modeName << "'"; + continue; + } + + if (!modeValues.isDictionary()) { + LOG(Agc, Error) + << "Invalid exposure mode '" << modeName << "'"; + return -EINVAL; + } + + std::vector exposureTimes = + modeValues["exposureTime"].get>().value_or(utils::defopt); + std::vector gains = + modeValues["gain"].get>().value_or(utils::defopt); + + if (exposureTimes.size() != gains.size()) { + LOG(Agc, Error) + << "Exposure time and gain array sizes unequal"; + return -EINVAL; + } + + if (exposureTimes.empty()) { + LOG(Agc, Error) + << "Exposure time and gain arrays are empty"; + return -EINVAL; + } + + std::vector stages; + for (size_t i = 0; i < exposureTimes.size(); i++) { + stages.push_back({ + .exposureTime = std::chrono::microseconds(exposureTimes[i]), + .gain = gains[i], + }); + } + + config.exposureModeHelpers.try_emplace(*modeValue, std::move(stages)); + } + } + + /* + * If we don't have any exposure modes in the tuning data we create an + * ExposureModeHelper using an empty vector of stages. This will result + * in the ExposureModeHelper simply driving the exposure time as high as + * possible before touching gain. + */ + if (config.exposureModeHelpers.empty()) + config.exposureModeHelpers.try_emplace( + controls::AeExposureModeNormal, + std::vector{}); + + return 0; +} + +int parseTuningData(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +{ + int ret; + + ret = parseRelativeLuminanceTarget(config, tuningData); + if (ret) + return ret; + + ret = parseConstraintModes(config, tuningData); + if (ret) + return ret; + + return parseExposureModes(config, tuningData); +} + +} /* namespace */ + /** * \class AgcAlgorithm * \brief libIPA LSC algorithm algorithm @@ -238,6 +481,57 @@ namespace agc { * virtual function, they should simply call the identically named member * function of AgcAlgorithm. * + * When a CameraSensorHelper is provided, AgcMeanLuminance is used and support for + * exposure (controls::AeExposureMode) and constraint (controls::AeConstraintMode) + * modes, and specifying the default luminance target is available via the tuning data. + * + * For controls::AeConstraintMode, the dictionary under the key "AeConstraintMode" + * is parsed: per-mode setting dictionaries with the key being a value from + * controls::AeConstraintModeNameValueMap. The yTarget can either be provided + * as single value or as array in which case it is interpreted as a PWL mapping + * lux levels to yTarget values. Each mode dict may contain either a "lower" or + * "upper" key or both, for example: + * + * \code{.unparsed} + * algorithms: + * - Agc: + * AeConstraintMode: + * Normal: + * lower: + * qLo: 0.98 + * qHi: 1.0 + * yTarget: 0.5 + * Highlight: + * lower: + * qLo: 0.98 + * qHi: 1.0 + * yTarget: 0.5 + * upper: + * qLo: 0.98 + * qHi: 1.0 + * yTarget: [ 100, 0.8, 20000, 0.5 ] + * + * \endcode + * + * For controls::AeExposureMode, the dictionary under the key AeExposureMode + * is parsed: per-mode setting dictionaries with the key being a value from + * controls::AeExposureModeNameValueMap. Each mode dict should contain + * an array of exposure times with the key "exposureTime" and an array of gain + * values with the key "gain", in this format: + * + * \code{.unparsed} + * algorithms: + * - Agc: + * AeExposureMode: + * Normal: + * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] + * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] + * Short: + * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] + * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] + * + * \endcode + * * \todo DigitalGain, DigitalGainMode * \todo Expand documentation */ @@ -298,9 +592,8 @@ namespace agc { * in ConfigurationParams::ctrlMap. * * If a CameraSensorHelper is provided in \a sensor, an instance of - * AgcMeanLuminance is instantiated and the tuning data is loaded by - * calling AgcMeanLuminance::parseTuningData(). Otherwise an instance - * of AgcMSV is instantiated, and in this case the tuning data is ignored. + * AgcMeanLuminance is instantiated. Otherwise an instance of AgcMSV + * is instantiated, and in this case the tuning data is ignored. * * \return 0 on success, or a negative error code * @@ -310,10 +603,12 @@ int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor, const ConfigurationParams &config) { if (sensor) { - auto &impl = impl_.emplace(); - int ret = impl.parseTuningData(tuningData); + AgcMeanLuminance::Configuration agcConfig; + int ret = parseTuningData(agcConfig, tuningData); if (ret) return ret; + + impl_.emplace(std::move(agcConfig)); } else { impl_.emplace(); } diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp index a763a3b94d..5b700e73d9 100644 --- a/src/ipa/libipa/agc_mean_luminance.cpp +++ b/src/ipa/libipa/agc_mean_luminance.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -18,8 +17,6 @@ #include "exposure_mode_helper.h" -using namespace libcamera::controls; - /** * \file agc_mean_luminance.h * \brief Class implementing mean luminance AEGC @@ -39,15 +36,6 @@ namespace ipa { */ static constexpr uint32_t kNumStartupFrames = 10; -/* - * Default relative luminance target - * - * This value should be chosen so that when the camera points at a grey target, - * the resulting image brightness looks "right". Custom values can be passed - * as the relativeLuminanceTarget value in sensor tuning files. - */ -static constexpr double kDefaultRelativeLuminanceTarget = 0.16; - /* * Maximum relative luminance target * @@ -148,17 +136,13 @@ static constexpr unsigned int kDefaultLuxLevel = 500; * section of a histogram to a target value, where the target and the boundaries * of the section of the histogram used in the calculation are taken from the * values defined for the currently configured AeConstraintMode within the - * tuning data. This class provides a helper function to parse those tuning data - * to discover the constraints, and so requires a specific format for those - * data which is described in \ref parseTuningData(). The gain from the first - * stage is then clamped to the gain from this stage. + * tuning data. The gain from the first stage is then clamped to the gain from + * this stage. * * The final gain is used to adjust the effective exposure value of the image, * and that new exposure value is divided into exposure time, analogue gain and * digital gain according to the selected AeExposureMode. This class uses the - * \ref ExposureModeHelper class to assist in that division, and expects the - * data needed to initialise that class to be present in tuning data in a - * format described in \ref parseTuningData(). + * \ref ExposureModeHelper class to assist in that division. * * In order to be able to use this algorithm an IPA module needs to be able to * do the following: @@ -170,225 +154,23 @@ static constexpr unsigned int kDefaultLuxLevel = 500; * * IPA modules that want to use this class to implement their AEGC algorithm * should derive AgcMeanLuminance::Traits and override the necessary functions. - * The users must call parseTuningData() in init(), and must also call configure() - * and setLimits() in configure(). They may then use calculateNewEv() in process(). - * If the limits passed to setLimits() change for any reason (for example, in - * response to a FrameDurationLimit control being passed in queueRequest()) then - * setLimits() must be called again with the new values. + * The users must call configure() and setLimits() in configure(). They may + * then use calculateNewEv() in process(). If the limits passed to setLimits() + * change for any reason (for example, in response to a FrameDurationLimit control + * being passed in queueRequest()) then setLimits() must be called again with the + * new values. */ -AgcMeanLuminance::AgcMeanLuminance() - : filteredExposure_(0s), luxWarningEnabled_(true), frameCount_(0) +AgcMeanLuminance::AgcMeanLuminance(Configuration &&config) + : filteredExposure_(0s), luxWarningEnabled_(true), frameCount_(0), + relativeLuminanceTarget_(std::move(config.relativeLuminanceTarget)), + constraintModes_(std::move(config.constraintModes)), + exposureModeHelpers_(std::move(config.exposureModeHelpers)) { } AgcMeanLuminance::~AgcMeanLuminance() = default; -int AgcMeanLuminance::parseRelativeLuminanceTarget(const ValueNode &tuningData) -{ - auto &target = tuningData["relativeLuminanceTarget"]; - if (!target) { - relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } }; - return 0; - } - - auto pwl = target.get(); - if (!pwl) { - LOG(AgcMeanLuminance, Error) - << "Failed to load relative luminance target."; - return -EINVAL; - } - - relativeLuminanceTarget_ = std::move(*pwl); - return 0; -} - -int AgcMeanLuminance::parseConstraint(const ValueNode &modeDict, int32_t id) -{ - for (const auto &[boundName, content] : modeDict.asDict()) { - if (boundName != "upper" && boundName != "lower") { - LOG(AgcMeanLuminance, Warning) - << "Ignoring unknown constraint bound '" << boundName << "'"; - continue; - } - - unsigned int idx = static_cast(boundName == "upper"); - AgcConstraint::Bound bound = static_cast(idx); - double qLo = content["qLo"].get().value_or(0.98); - double qHi = content["qHi"].get().value_or(1.0); - auto yTarget = content["yTarget"].get(); - if (!yTarget) { - LOG(AgcMeanLuminance, Error) - << "Failed to parse yTarget"; - return -EINVAL; - } - - AgcConstraint constraint = { bound, qLo, qHi, std::move(*yTarget) }; - auto &constraints = constraintModes_[id]; - - if (idx) - constraints.push_back(std::move(constraint)); - else - constraints.insert(constraints.begin(), std::move(constraint)); - } - - return 0; -} - -int AgcMeanLuminance::parseConstraintModes(const ValueNode &tuningData) -{ - static constexpr std::tuple compat[] = { - { "ConstraintNormal", "Normal", controls::AeConstraintModeNormal }, - { "ConstraintHighlight", "Highlight", controls::AeConstraintModeHighlight }, - { "ConstraintShadows", "Shadows", controls::AeConstraintModeShadows }, - { "ConstraintCustom", "Custom", controls::AeConstraintModeCustom }, - }; - - const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()]; - if (constraintModes.isDictionary()) { - for (const auto &[modeName, modeDict] : constraintModes.asDict()) { - std::optional modeValue; - - auto it = AeConstraintModeNameValueMap.find(modeName); - if (it == AeConstraintModeNameValueMap.end()) { - for (const auto &[oldName, newName, v] : compat) { - if (modeName != oldName) - continue; - - modeValue = v; - - LOG(AgcMeanLuminance, Warning) - << "Constraint mode '" << oldName << "' has been renamed to '" - << newName << "', please fix the tuning file"; - break; - } - } else { - modeValue = static_cast(it->second); - } - - if (!modeValue) { - LOG(AgcMeanLuminance, Warning) - << "Skipping unknown constraint mode '" << modeName << "'"; - continue; - } - - if (!modeDict.isDictionary()) { - LOG(AgcMeanLuminance, Error) - << "Invalid constraint mode '" << modeName << "'"; - return -EINVAL; - } - - int ret = parseConstraint(modeDict, *modeValue); - if (ret) - return ret; - } - } - - /* - * If the tuning data file contains no constraints then we use the - * default constraint that the IPU3/RkISP1 Agc algorithms were adhering - * to anyway before centralisation; this constraint forces the top 2% of - * the histogram to be at least 0.5. - */ - if (constraintModes_.empty()) { - AgcConstraint constraint = { - AgcConstraint::Bound::Lower, - 0.98, - 1.0, - Pwl({ { { 0.0, 0.5 } } }) - }; - - constraintModes_[controls::AeConstraintModeNormal].push_back(std::move(constraint)); - } - - return 0; -} - -int AgcMeanLuminance::parseExposureModes(const ValueNode &tuningData) -{ - static constexpr std::tuple compat[] = { - { "ExposureNormal", "Normal", controls::AeExposureModeNormal }, - { "ExposureShort", "Short", controls::AeExposureModeShort }, - { "ExposureLong", "Long", controls::AeExposureModeLong }, - { "ExposureCustom", "Custom", controls::AeExposureModeCustom }, - }; - - const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()]; - if (exposureModes.isDictionary()) { - for (const auto &[modeName, modeValues] : exposureModes.asDict()) { - std::optional modeValue; - - auto it = AeExposureModeNameValueMap.find(modeName); - if (it == AeExposureModeNameValueMap.end()) { - for (const auto &[oldName, newName, v] : compat) { - if (modeName != oldName) - continue; - - modeValue = v; - - LOG(AgcMeanLuminance, Warning) - << "Exposure mode '" << oldName << "' has been renamed to '" - << newName << "', please fix the tuning file"; - break; - } - } else { - modeValue = static_cast(it->second); - } - - if (!modeValue) { - LOG(AgcMeanLuminance, Warning) - << "Skipping unknown exposure mode '" << modeName << "'"; - continue; - } - - if (!modeValues.isDictionary()) { - LOG(AgcMeanLuminance, Error) - << "Invalid exposure mode '" << modeName << "'"; - return -EINVAL; - } - - std::vector exposureTimes = - modeValues["exposureTime"].get>().value_or(utils::defopt); - std::vector gains = - modeValues["gain"].get>().value_or(utils::defopt); - - if (exposureTimes.size() != gains.size()) { - LOG(AgcMeanLuminance, Error) - << "Exposure time and gain array sizes unequal"; - return -EINVAL; - } - - if (exposureTimes.empty()) { - LOG(AgcMeanLuminance, Error) - << "Exposure time and gain arrays are empty"; - return -EINVAL; - } - - std::vector stages; - for (size_t i = 0; i < exposureTimes.size(); i++) { - stages.push_back({ - .exposureTime = std::chrono::microseconds(exposureTimes[i]), - .gain = gains[i], - }); - } - - exposureModeHelpers_.try_emplace(*modeValue, std::move(stages)); - } - } - - /* - * If we don't have any exposure modes in the tuning data we create an - * ExposureModeHelper using an empty vector of stages. This will result - * in the ExposureModeHelper simply driving the exposure time as high as - * possible before touching gain. - */ - if (exposureModeHelpers_.empty()) - exposureModeHelpers_.try_emplace(controls::AeExposureModeNormal, - std::vector{}); - - return 0; -} - /** * \brief Configure the exposure mode helpers * \param[in] lineDuration The sensor line length @@ -408,76 +190,6 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration, frameCount_ = 0; } -/** - * \brief Parse tuning data for AeConstraintMode and AeExposureMode controls - * \param[in] tuningData the ValueNode representing the tuning data - * - * This function parses tuning data to build the list of allowed values for the - * AeConstraintMode and AeExposureMode controls. Those tuning data must provide - * the data in a specific format; the Agc algorithm's tuning data should contain - * a dictionary called AeConstraintMode containing per-mode setting dictionaries - * with the key being a value from \ref controls::AeConstraintModeNameValueMap. - * The yTarget can either be provided as single value or as array in which case - * it is interpreted as a PWL mapping lux levels to yTarget values. Each mode - * dict may contain either a "lower" or "upper" key or both, for example: - * - * \code{.unparsed} - * algorithms: - * - Agc: - * AeConstraintMode: - * Normal: - * lower: - * qLo: 0.98 - * qHi: 1.0 - * yTarget: 0.5 - * Highlight: - * lower: - * qLo: 0.98 - * qHi: 1.0 - * yTarget: 0.5 - * upper: - * qLo: 0.98 - * qHi: 1.0 - * yTarget: [ 100, 0.8, 20000, 0.5 ] - * - * \endcode - * - * For the AeExposureMode control the data should contain a dictionary called - * AeExposureMode containing per-mode setting dictionaries with the key being a - * value from \ref controls::AeExposureModeNameValueMap. Each mode dict should - * contain an array of exposure times with the key "exposureTime" and an array - * of gain values with the key "gain", in this format: - * - * \code{.unparsed} - * algorithms: - * - Agc: - * AeExposureMode: - * Normal: - * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] - * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] - * Short: - * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] - * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] - * - * \endcode - * - * \return 0 on success or a negative error code - */ -int AgcMeanLuminance::parseTuningData(const ValueNode &tuningData) -{ - int ret; - - ret = parseRelativeLuminanceTarget(tuningData); - if (ret) - return ret; - - ret = parseConstraintModes(tuningData); - if (ret) - return ret; - - return parseExposureModes(tuningData); -} - /** * \brief Set the ExposureModeHelper limits for this class * \param[in] minExposureTime Minimum exposure time to allow @@ -502,12 +214,12 @@ void AgcMeanLuminance::setLimits(utils::Duration minExposureTime, /** * \fn AgcMeanLuminance::constraintModes() - * \brief Get the constraint modes that have been parsed from tuning data + * \brief Get the available constraint modes */ /** * \fn AgcMeanLuminance::exposureModeHelpers() - * \brief Get the ExposureModeHelpers that have been parsed from tuning data + * \brief Get the available ExposureModeHelpers */ /** diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h index f799618ffc..c32bf44a2e 100644 --- a/src/ipa/libipa/agc_mean_luminance.h +++ b/src/ipa/libipa/agc_mean_luminance.h @@ -8,13 +8,10 @@ #pragma once #include -#include #include #include -#include "libcamera/internal/value_node.h" - #include "exposure_mode_helper.h" #include "histogram.h" #include "pwl.h" @@ -26,9 +23,6 @@ namespace ipa { class AgcMeanLuminance { public: - AgcMeanLuminance(); - ~AgcMeanLuminance(); - struct AgcConstraint { enum class Bound { Lower = 0, @@ -40,6 +34,15 @@ public: Pwl yTarget; }; + struct Configuration { + Pwl relativeLuminanceTarget; + std::map> constraintModes; + std::map exposureModeHelpers; + }; + + AgcMeanLuminance(Configuration &&config); + ~AgcMeanLuminance(); + struct Traits { virtual ~Traits() = default; virtual double estimateLuminance(double gain) const = 0; @@ -80,20 +83,17 @@ public: double effectiveYTarget(double lux, double exposureCompensation) const; private: - int parseRelativeLuminanceTarget(const ValueNode &tuningData); - int parseConstraint(const ValueNode &modeDict, int32_t id); - int parseConstraintModes(const ValueNode &tuningData); - int parseExposureModes(const ValueNode &tuningData); double estimateInitialGain(const Traits &traits, double yTarget) const; double constraintClampGain(const Params ¶ms, double gain) const; utils::Duration filterExposure(utils::Duration exposureValue); utils::Duration filteredExposure_; mutable bool luxWarningEnabled_; - Pwl relativeLuminanceTarget_; uint64_t frameCount_; std::vector additionalConstraints_; + + Pwl relativeLuminanceTarget_; std::map> constraintModes_; std::map exposureModeHelpers_; }; From patchwork Mon Nov 9 09:49:40 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28803 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 189FBC335B for ; Mon, 9 Nov 2026 09:50:19 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A52DB6936B; Mon, 9 Nov 2026 10:50:07 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="m1t+FGUr"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id B82446918C for ; Mon, 9 Nov 2026 10:49:59 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 2382B802; Mon, 9 Nov 2026 10:47:38 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217658; bh=MoIA5uzgc9bwwTwK/n3n4LY7ODuBkpJhykB9Srlh1nU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=m1t+FGUrZF8GAgCH6XFeLu0Cy2FvCw3ce6xQG6vVN4UePLM3EMXZyz1Y4k1R6OUqA ydxD6kOt6BMsbugxAhI84CsuX9SfEINTzNNY+uEe8IR1YEYHmNexz4TVN5Gp0KWpfA uOv+4MRzp93VnOUVp2+o2VyuvCEgcDmAPHU0cRAs= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [PATCH v7 05/19] ipa: libipa: agc: Split tuning data fallback handling Date: Mon, 9 Nov 2026 10:49:40 +0100 Message-ID: <20261109094954.341480-6-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a separate function whose job is to add the fallback values for the various configuration parameters that were not specified in the tuning file. This allows some simplification of the code by making use of early returns, so do that as well. And additionally, pass only the relevant object to the "sub" parsers instead of the whole thing. This is needed in order to be able to determine what exactly the tuning file contains, before adding the fallback values. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 245 +++++++++++++++++++++-------------------- 1 file changed, 125 insertions(+), 120 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 38bf90879d..6127859d0f 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -237,13 +237,10 @@ namespace { */ static constexpr double kDefaultRelativeLuminanceTarget = 0.16; -int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &target) { - auto &target = tuningData["relativeLuminanceTarget"]; - if (!target) { - config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } }; + if (!target) return 0; - } auto pwl = target.get(); if (!pwl) { @@ -293,7 +290,7 @@ int parseConstraint(AgcMeanLuminance::Configuration &config, const ValueNode &mo return 0; } -int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &constraintModes) { static constexpr std::tuple compat[] = { { "ConstraintNormal", "Normal", controls::AeConstraintModeNormal }, @@ -302,67 +299,50 @@ int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNod { "ConstraintCustom", "Custom", controls::AeConstraintModeCustom }, }; - const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()]; - if (constraintModes.isDictionary()) { - for (const auto &[modeName, modeDict] : constraintModes.asDict()) { - std::optional modeValue; - - auto it = controls::AeConstraintModeNameValueMap.find(modeName); - if (it == controls::AeConstraintModeNameValueMap.end()) { - for (const auto &[oldName, newName, v] : compat) { - if (modeName != oldName) - continue; - - modeValue = v; - - LOG(Agc, Warning) - << "Constraint mode '" << oldName << "' has been renamed to '" - << newName << "', please fix the tuning file"; - break; - } - } else { - modeValue = static_cast(it->second); - } + if (!constraintModes.isDictionary()) + return 0; - if (!modeValue) { - LOG(Agc, Warning) - << "Skipping unknown constraint mode '" << modeName << "'"; - continue; - } + for (const auto &[modeName, modeDict] : constraintModes.asDict()) { + std::optional modeValue; + + auto it = controls::AeConstraintModeNameValueMap.find(modeName); + if (it == controls::AeConstraintModeNameValueMap.end()) { + for (const auto &[oldName, newName, v] : compat) { + if (modeName != oldName) + continue; - if (!modeDict.isDictionary()) { - LOG(Agc, Error) - << "Invalid constraint mode '" << modeName << "'"; - return -EINVAL; + modeValue = v; + + LOG(Agc, Warning) + << "Constraint mode '" << oldName << "' has been renamed to '" + << newName << "', please fix the tuning file"; + break; } + } else { + modeValue = static_cast(it->second); + } - int ret = parseConstraint(config, modeDict, *modeValue); - if (ret) - return ret; + if (!modeValue) { + LOG(Agc, Warning) + << "Skipping unknown constraint mode '" << modeName << "'"; + continue; } - } - /* - * If the tuning data file contains no constraints then we use the - * default constraint that the IPU3/RkISP1 Agc algorithms were adhering - * to anyway before centralisation; this constraint forces the top 2% of - * the histogram to be at least 0.5. - */ - if (config.constraintModes.empty()) { - AgcMeanLuminance::AgcConstraint constraint = { - AgcMeanLuminance::AgcConstraint::Bound::Lower, - 0.98, - 1.0, - Pwl({ { { 0.0, 0.5 } } }), - }; + if (!modeDict.isDictionary()) { + LOG(Agc, Error) + << "Invalid constraint mode '" << modeName << "'"; + return -EINVAL; + } - config.constraintModes[controls::AeConstraintModeNormal].push_back(std::move(constraint)); + int ret = parseConstraint(config, modeDict, *modeValue); + if (ret) + return ret; } return 0; } -int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &exposureModes) { static constexpr std::tuple compat[] = { { "ExposureNormal", "Normal", controls::AeExposureModeNormal }, @@ -371,79 +351,68 @@ int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode { "ExposureCustom", "Custom", controls::AeExposureModeCustom }, }; - const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()]; - if (exposureModes.isDictionary()) { - for (const auto &[modeName, modeValues] : exposureModes.asDict()) { - std::optional modeValue; - - auto it = controls::AeExposureModeNameValueMap.find(modeName); - if (it == controls::AeExposureModeNameValueMap.end()) { - for (const auto &[oldName, newName, v] : compat) { - if (modeName != oldName) - continue; - - modeValue = v; - - LOG(Agc, Warning) - << "Exposure mode '" << oldName << "' has been renamed to '" - << newName << "', please fix the tuning file"; - break; - } - } else { - modeValue = static_cast(it->second); - } + if (!exposureModes.isDictionary()) + return 0; + + for (const auto &[modeName, modeValues] : exposureModes.asDict()) { + std::optional modeValue; + + auto it = controls::AeExposureModeNameValueMap.find(modeName); + if (it == controls::AeExposureModeNameValueMap.end()) { + for (const auto &[oldName, newName, v] : compat) { + if (modeName != oldName) + continue; + + modeValue = v; - if (!modeValue) { LOG(Agc, Warning) - << "Skipping unknown exposure mode '" << modeName << "'"; - continue; + << "Exposure mode '" << oldName << "' has been renamed to '" + << newName << "', please fix the tuning file"; + break; } + } else { + modeValue = static_cast(it->second); + } - if (!modeValues.isDictionary()) { - LOG(Agc, Error) - << "Invalid exposure mode '" << modeName << "'"; - return -EINVAL; - } + if (!modeValue) { + LOG(Agc, Warning) + << "Skipping unknown exposure mode '" << modeName << "'"; + continue; + } - std::vector exposureTimes = - modeValues["exposureTime"].get>().value_or(utils::defopt); - std::vector gains = - modeValues["gain"].get>().value_or(utils::defopt); + if (!modeValues.isDictionary()) { + LOG(Agc, Error) + << "Invalid exposure mode '" << modeName << "'"; + return -EINVAL; + } - if (exposureTimes.size() != gains.size()) { - LOG(Agc, Error) - << "Exposure time and gain array sizes unequal"; - return -EINVAL; - } + std::vector exposureTimes = + modeValues["exposureTime"].get>().value_or(utils::defopt); + std::vector gains = + modeValues["gain"].get>().value_or(utils::defopt); - if (exposureTimes.empty()) { - LOG(Agc, Error) - << "Exposure time and gain arrays are empty"; - return -EINVAL; - } + if (exposureTimes.size() != gains.size()) { + LOG(Agc, Error) + << "Exposure time and gain array sizes unequal"; + return -EINVAL; + } - std::vector stages; - for (size_t i = 0; i < exposureTimes.size(); i++) { - stages.push_back({ - .exposureTime = std::chrono::microseconds(exposureTimes[i]), - .gain = gains[i], - }); - } + if (exposureTimes.empty()) { + LOG(Agc, Error) + << "Exposure time and gain arrays are empty"; + return -EINVAL; + } - config.exposureModeHelpers.try_emplace(*modeValue, std::move(stages)); + std::vector stages; + for (size_t i = 0; i < exposureTimes.size(); i++) { + stages.push_back({ + .exposureTime = std::chrono::microseconds(exposureTimes[i]), + .gain = gains[i], + }); } - } - /* - * If we don't have any exposure modes in the tuning data we create an - * ExposureModeHelper using an empty vector of stages. This will result - * in the ExposureModeHelper simply driving the exposure time as high as - * possible before touching gain. - */ - if (config.exposureModeHelpers.empty()) - config.exposureModeHelpers.try_emplace( - controls::AeExposureModeNormal, - std::vector{}); + config.exposureModeHelpers.try_emplace(*modeValue, std::move(stages)); + } return 0; } @@ -452,15 +421,49 @@ int parseTuningData(AgcMeanLuminance::Configuration &config, const ValueNode &tu { int ret; - ret = parseRelativeLuminanceTarget(config, tuningData); + ret = parseRelativeLuminanceTarget(config, tuningData["relativeLuminanceTarget"]); if (ret) return ret; - ret = parseConstraintModes(config, tuningData); + ret = parseConstraintModes(config, tuningData[controls::AeConstraintMode.name()]); if (ret) return ret; - return parseExposureModes(config, tuningData); + return parseExposureModes(config, tuningData[controls::AeExposureMode.name()]); +} + +void addFallbacks(AgcMeanLuminance::Configuration &config) +{ + if (config.relativeLuminanceTarget.empty()) + config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } }; + + /* + * If the tuning data file contains no constraints then we use the + * default constraint that the IPU3/RkISP1 Agc algorithms were adhering + * to anyway before centralisation; this constraint forces the top 2% of + * the histogram to be at least 0.5. + */ + if (config.constraintModes.empty()) { + AgcMeanLuminance::AgcConstraint constraint = { + AgcMeanLuminance::AgcConstraint::Bound::Lower, + 0.98, + 1.0, + Pwl({ { { 0.0, 0.5 } } }), + }; + + config.constraintModes[controls::AeConstraintModeNormal].push_back(std::move(constraint)); + } + + /* + * If we don't have any exposure modes in the tuning data we create an + * ExposureModeHelper using an empty vector of stages. This will result + * in the ExposureModeHelper simply driving the exposure time as high as + * possible before touching gain. + */ + if (config.exposureModeHelpers.empty()) + config.exposureModeHelpers.try_emplace( + controls::AeExposureModeNormal, + std::vector{}); } } /* namespace */ @@ -608,6 +611,8 @@ int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor, if (ret) return ret; + addFallbacks(agcConfig); + impl_.emplace(std::move(agcConfig)); } else { impl_.emplace(); From patchwork Mon Nov 9 09:49:41 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28804 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id D1A1CC3304 for ; Mon, 9 Nov 2026 09:50:20 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 829D26A07F; Mon, 9 Nov 2026 10:50:09 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="O2tlZFb/"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id EFCDB691D9 for ; Mon, 9 Nov 2026 10:49:59 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 60029DDE; Mon, 9 Nov 2026 10:47:38 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217658; bh=DNI5ugSEQl1ErhMit0kAi6gX4LDhr2gA3pQmEY/weVA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=O2tlZFb/yn8jn9edVtfd7pt2Vw6PGOnVY8ICNIO8R3M+FmIihmNHCybqYpOKXxFJn B6SNgy8jAdceN+hl5S+iSiBqGZSStiGClrdG4Omjhq7UgnG5YI6O2gEyf7aYLgy3w8 KzDHFLyFrXCjZppLG8NDeVmcmkbJzh/eer9AM+Ag= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [PATCH v7 06/19] ipa: libipa: exposure_mode_helper: Consider last stage as hard-limit Date: Mon, 9 Nov 2026 10:49:41 +0100 Message-ID: <20261109094954.341480-7-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Currently `splitExposure()` will raise the exposure and analogue gain above what is specified in the last stage, if the desired exposure could not be reached. This behaviour is different from that of the raspberry pi ipa, and also could be surprising to the user / tuning file creator. So make sure that the exposure time and analogue gain is limited by the last stage, in addition to the runtime controlled upper bound. With this change, at least one stage is required, so an assertions are added. And the general agc algorithm is adjusted to provide stages for the fallback mode. Note that the `stageGain` initialization has to be adjusted to retrieve the quantization gain, so that at the end of the loop * stageExposureTime * stageGain * quantGain * quantGain2 are all up to date for the last stage. Closes: https://gitlab.freedesktop.org/camera/libcamera/-/work_items/362 Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 7 ++++++- src/ipa/libipa/exposure_mode_helper.cpp | 27 ++++++++++++------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 6127859d0f..c1345787f7 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -463,7 +463,12 @@ void addFallbacks(AgcMeanLuminance::Configuration &config) if (config.exposureModeHelpers.empty()) config.exposureModeHelpers.try_emplace( controls::AeExposureModeNormal, - std::vector{}); + std::vector{ + ExposureModeHelper::Stage{ + .exposureTime = utils::Duration::max(), + .gain = std::numeric_limits::max(), + }, + }); } } /* namespace */ diff --git a/src/ipa/libipa/exposure_mode_helper.cpp b/src/ipa/libipa/exposure_mode_helper.cpp index 4ec31f54c3..5c4242519b 100644 --- a/src/ipa/libipa/exposure_mode_helper.cpp +++ b/src/ipa/libipa/exposure_mode_helper.cpp @@ -84,6 +84,7 @@ ExposureModeHelper::ExposureModeHelper(std::vector &&stages) : stages_(std::move(stages)), lineDuration_(1us), minExposureTime_(0us), maxExposureTime_(0us), minGain_(0), maxGain_(0), sensorHelper_(nullptr) { + ASSERT(!stages_.empty()); } /** @@ -203,8 +204,8 @@ double ExposureModeHelper::clampGain(double gain, double *quantizationGain) cons * value. * * If no combination of exposure time and gain limits is found that meets the - * required exposure, the helper falls-back to simply maximising the exposure - * time first, followed by analogue gain, followed by digital gain. + * required exposure, then the exposure and gain of the last stage will be used, + * and the digital gain will be increased as necessary. * * During the calculations the gain missed due to quantization is recorded and * returned as quantization gain. The quantization gain is not included in the @@ -219,6 +220,7 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const { ASSERT(maxExposureTime_); ASSERT(maxGain_); + ASSERT(!stages_.empty()); utils::Duration exposureTime; double gain; @@ -240,13 +242,13 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const exposure / (exposureTime * gain * quantGain) }; } - double stageGain = clampGain(1.0); - double lastStageGain = stageGain; + utils::Duration stageExposureTime = {}; + double lastStageGain = clampGain(1.0); + double stageGain = {}; for (const auto &stage : stages_) { - utils::Duration stageExposureTime = clampExposureTime(stage.exposureTime, - &quantGain); - stageGain = clampGain(stage.gain); + stageExposureTime = clampExposureTime(stage.exposureTime, &quantGain); + stageGain = clampGain(stage.gain, &quantGain2); /* * We perform the clamping on both exposure time and gain in @@ -280,14 +282,11 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const } /* - * From here on all we can do is max out the exposure time, followed by - * the analogue gain. If we still haven't achieved the target we send - * the rest of the exposure time to digital gain. If we were given no - * stages to use then the default stageGain of 1.0 is used so that - * exposure time is maxed before gain is touched at all. + * If no stages could satisfy the desired exposure, then use the last + * stage's exposure time and gain, which is the maximum allowed. */ - exposureTime = clampExposureTime(exposure / stageGain, &quantGain); - gain = clampGain(exposure / exposureTime, &quantGain2); + exposureTime = stageExposureTime; + gain = stageGain; quantGain *= quantGain2; return { exposureTime, gain, quantGain, From patchwork Mon Nov 9 09:49:42 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28805 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 35B1AC335B for ; Mon, 9 Nov 2026 09:50:22 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id B06406A7DE; Mon, 9 Nov 2026 10:50:10 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="UIb0wGTW"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id AD48F691D1 for ; Mon, 9 Nov 2026 10:50:00 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 9C40CDA8; Mon, 9 Nov 2026 10:47:38 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217658; bh=iJUQfixfHV/cF8XYiJ2HfR2SmkCzwcb6BzWPEK9weTw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=UIb0wGTWx4quo3gKSdx9WZMAy5wbvliYs/cJsvReuUXxS+eb9dGBUtEbCRfEnew3Q 89F/xuWof61c53e9Y0tD1FsoAtTw23Vxapx7HzhxFVpgth+B9kjd3ZrO1NLodIyybk 3WOVaoiDvQQMO3HL9OfBBvVWJMaPPyWL5ecLG+Uc= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham , Stefan Klug Subject: [PATCH v7 07/19] ipa: libipa: agc: Do not report auto controls if not available Date: Mon, 9 Nov 2026 10:49:42 +0100 Message-ID: <20261109094954.341480-8-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" If the auto mode is disabled for the session, do not report controls in metadata that are related to controlling the auto algorithm. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index c1345787f7..cd05db3ddc 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -1181,9 +1181,11 @@ void AgcAlgorithm::fillMetadata(const agc::Session &session, ? controls::AnalogueGainModeAuto : controls::AnalogueGainModeManual); - metadata.set(controls::AeExposureMode, frameContext.exposureMode); - metadata.set(controls::AeConstraintMode, frameContext.constraintMode); - metadata.set(controls::ExposureValue, frameContext.exposureValue); + if (session.autoAllowed) { + metadata.set(controls::AeExposureMode, frameContext.exposureMode); + metadata.set(controls::AeConstraintMode, frameContext.constraintMode); + metadata.set(controls::ExposureValue, frameContext.exposureValue); + } } } /* namespace ipa */ From patchwork Mon Nov 9 09:49:43 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28806 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 7F173C3304 for ; Mon, 9 Nov 2026 09:50:23 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 44AFC691D1; Mon, 9 Nov 2026 10:50:12 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="urG/Do1+"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id C3FF5690F0 for ; Mon, 9 Nov 2026 10:50:00 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id E582E802; Mon, 9 Nov 2026 10:47:38 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217659; bh=/u88aMQDW3mQh14RULhvkOj64RAAT+FVderk8DmDipE=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=urG/Do1+p933JQkCTBuy+BH5gUxfuyib2Lt+c6M+eA5jBUgLbHGhnVC8iL0tC1lMD KY3b3gGOSoAp8q5LzK1GoeZaYDgSh8bo449iBl+3PFkjUyeKt4nqtmGD0LwEDAgR41 q1/P8ViuN3DThlw7RFzI7pBpxYXtvHzFJUdLoVLQ= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug , Jacopo Mondi Subject: [PATCH v7 08/19] ipa: libipa: agc: Keep frame duration limits ordered Date: Mon, 9 Nov 2026 10:49:43 +0100 Message-ID: <20261109094954.341480-9-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" The frame duration limits from controls are already clamped to the session limits, but that does not prevent a situation where min > max. Use the just computed `state.minFrameDuration` as the lower bound when clamping the max value in order to avoid this possibility. Signed-off-by: Barnabás Pőcze Reviewed-by: Stefan Klug Reviewed-by: Jacopo Mondi --- src/ipa/libipa/agc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index cd05db3ddc..cabf7de464 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -947,7 +947,7 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s state.maxFrameDuration = std::clamp( std::chrono::microseconds((*frameDurationLimits).back()), - session.minFrameDuration, session.maxFrameDuration); + state.minFrameDuration, session.maxFrameDuration); } frameContext.minFrameDuration = state.minFrameDuration; frameContext.maxFrameDuration = state.maxFrameDuration; From patchwork Mon Nov 9 09:49:44 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28807 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 9461EC335B for ; Mon, 9 Nov 2026 09:50:24 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 20FC86A7AF; Mon, 9 Nov 2026 10:50:13 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="hAf6bXDj"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id DE098691D3 for ; Mon, 9 Nov 2026 10:50:00 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 3430F981; Mon, 9 Nov 2026 10:47:39 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217659; bh=SiZgH/217fQw3LW1o2zN8HH8/Ypfj6vibGkRHJiL9mc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=hAf6bXDjPGOAhxySoDdnJXDBPh2IBMaX50V0ds6nYcBcbItWq3pHtGXbOY8uTaOyC /P9Dl3Xc9pTZ82V0r3AihSoYdUloLV2VwQ50ELu0ZEKU6QuKt+xEHAn2wltKhX2+Wd ivd73S1GDUTNo8DucP5FPcudJQGpJHICfV1CN4So= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug , Jacopo Mondi Subject: [PATCH v7 09/19] ipa: libipa: agc: Retrieve `FrameDurationLimits` earlier Date: Mon, 9 Nov 2026 10:49:44 +0100 Message-ID: <20261109094954.341480-10-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" The frame duration limits will be used to clamp the manually provided exposure time. Handle them earlier so that they are available. Signed-off-by: Barnabás Pőcze Reviewed-by: Stefan Klug Reviewed-by: Jacopo Mondi --- src/ipa/libipa/agc.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index cabf7de464..94206103c2 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -896,6 +896,20 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s } } + const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits); + if (frameDurationLimits) { + /* Limit the control value to the limits in ControlInfo */ + state.minFrameDuration = std::clamp( + std::chrono::microseconds((*frameDurationLimits).front()), + session.minFrameDuration, session.maxFrameDuration); + + state.maxFrameDuration = std::clamp( + std::chrono::microseconds((*frameDurationLimits).back()), + state.minFrameDuration, session.maxFrameDuration); + } + frameContext.minFrameDuration = state.minFrameDuration; + frameContext.maxFrameDuration = state.maxFrameDuration; + const auto &exposure = controls.get(controls::ExposureTime); if (exposure && !state.autoExposureEnabled) { state.manual.exposure = *exposure * 1.0us / session.lineDuration; @@ -937,20 +951,6 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s if (exposureValue) state.exposureValue = *exposureValue; frameContext.exposureValue = state.exposureValue; - - const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits); - if (frameDurationLimits) { - /* Limit the control value to the limits in ControlInfo */ - state.minFrameDuration = std::clamp( - std::chrono::microseconds((*frameDurationLimits).front()), - session.minFrameDuration, session.maxFrameDuration); - - state.maxFrameDuration = std::clamp( - std::chrono::microseconds((*frameDurationLimits).back()), - state.minFrameDuration, session.maxFrameDuration); - } - frameContext.minFrameDuration = state.minFrameDuration; - frameContext.maxFrameDuration = state.maxFrameDuration; } /** From patchwork Mon Nov 9 09:49:45 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28808 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 8B2DBC335C for ; Mon, 9 Nov 2026 09:50:25 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id EAFA06A7DA; Mon, 9 Nov 2026 10:50:13 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="eHrpmaPf"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 06DA0691D7 for ; Mon, 9 Nov 2026 10:50:01 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 771CFDDE; Mon, 9 Nov 2026 10:47:39 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217659; bh=CAfv+HbJYpuDmxtf9eklAuLvUdPaCY08Eib1MxAPKX8=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=eHrpmaPfXN91gEZ786fVe44WTA/+DZCTK313J1jVD1sm7fm+WHVBr311pjgYSSibd y9Bpl97oVCKOfgKSgFsyGXlHaaHm2kFCbNG393krRdW138c8DUx+J/0s/zKcCM8tdg uyvf29Y2YVu7SSh0WM7lCY8zmo+diuLeHfVxLOLc= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham Subject: [PATCH v7 10/19] ipa: libipa: agc: Report `FrameDurationLimits` Date: Mon, 9 Nov 2026 10:49:45 +0100 Message-ID: <20261109094954.341480-11-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" `FrameDurationLimits` can be reported in metadata, to signal the exact limits that were used when processing the frame. This was missing from the agc algorithm, so add it. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham --- src/ipa/libipa/agc.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 94206103c2..3c6b9d6236 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -1174,6 +1174,9 @@ void AgcAlgorithm::fillMetadata(const agc::Session &session, metadata.set(controls::ExposureTime, utils::Duration(session.lineDuration * frameContext.exposure).get()); metadata.set(controls::FrameDuration, frameContext.frameDuration.get()); + metadata.set(controls::FrameDurationLimits, + { static_cast(frameContext.minFrameDuration.get()), + static_cast(frameContext.maxFrameDuration.get()) }); metadata.set(controls::ExposureTimeMode, frameContext.autoExposureEnabled ? controls::ExposureTimeModeAuto : controls::ExposureTimeModeManual); From patchwork Mon Nov 9 09:49:46 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28809 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 604F2C3304 for ; Mon, 9 Nov 2026 09:50:26 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id D80986A7D6; Mon, 9 Nov 2026 10:50:14 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="PQEHsN+T"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 49CB2691DF for ; Mon, 9 Nov 2026 10:50:01 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id BA7D1802; Mon, 9 Nov 2026 10:47:39 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217659; bh=bwpQSN6QVuCFvTZOvueJmI9vijWASF5knDZslH5ZTwo=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=PQEHsN+Tb2fqdsR0JInHAfwqxgf3en5uaHpY5XSBr2RrbiFp5JVB+u5C4YixP/ONG PXQI2X0O6jSMdwZ9WqcrBUYc32yTjO5h9ScH6yFvJy+UL6UDyRn0Kv3NjzhY1dRWkC 166qoqcTBFzLcPAynjhaH0e1FEKORdJwa/AscjH8= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham Subject: [PATCH v7 11/19] ipa: libipa: agc: Calculate vblank and frame duration sooner Date: Mon, 9 Nov 2026 10:49:46 +0100 Message-ID: <20261109094954.341480-12-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Calculating the vblank and frame duration is problematic in `process()` because at the moment it is calculated for an already finished frame based on the new suggested exposure time. Instead, move the calculation to `prepare()` where the frame's exposure and gain are finalized. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham --- src/ipa/ipu3/algorithms/agc.cpp | 2 +- src/ipa/libipa/agc.cpp | 36 ++++++++++++++--------------- src/ipa/libipa/agc.h | 6 ++--- src/ipa/mali-c55/algorithms/agc.cpp | 2 +- src/ipa/rkisp1/algorithms/agc.cpp | 2 +- src/ipa/softisp/algorithms/agc.cpp | 2 +- 6 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/ipa/ipu3/algorithms/agc.cpp b/src/ipa/ipu3/algorithms/agc.cpp index ccf65da317..afe80dc062 100644 --- a/src/ipa/ipu3/algorithms/agc.cpp +++ b/src/ipa/ipu3/algorithms/agc.cpp @@ -111,7 +111,7 @@ void Agc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame, IPAFrameContext &frameContext, [[maybe_unused]] ipu3_uapi_params *params) { - agc_.prepare(context.activeState.agc, frameContext.agc); + agc_.prepare(context.configuration.agc, context.activeState.agc, frameContext.agc); } Histogram Agc::parseStatistics(const ipu3_uapi_stats_3a *stats, diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 3c6b9d6236..d87c62b4ba 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -955,6 +955,7 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s /** * \brief Prepare a frame + * \param[in] session The agc session configuration * \param[in] state The agc active state * \param[in] frameContext The agc frame context * @@ -964,11 +965,10 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s * \ref agc::FrameContext::gain "frameContext.gain" will be finalized * and may be used by the caller (see agc::prepareControls()). * - * \todo Finalize \ref agc::FrameContext::vblank "frameContext.vblank" as well - * * \sa Algorithm::prepare() */ -void AgcAlgorithm::prepare(agc::ActiveState &state, agc::FrameContext &frameContext) +void AgcAlgorithm::prepare(const agc::Session &session, agc::ActiveState &state, + agc::FrameContext &frameContext) { uint32_t activeAutoExposure = state.automatic.exposure; double activeAutoGain = state.automatic.gain; @@ -999,6 +999,8 @@ void AgcAlgorithm::prepare(agc::ActiveState &state, agc::FrameContext &frameCont } frameContext.yTarget = state.automatic.yTarget; + + processFrameDuration(session, frameContext); } /** @@ -1029,7 +1031,6 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, ControlList &metadata) { if (!params) { - processFrameDuration(session, frameContext, frameContext.minFrameDuration); fillMetadata(session, frameContext, metadata); return; } @@ -1133,13 +1134,6 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, << "quantization-gain: " << state.automatic.quantizationGain << ", " << "digital-gain: " << state.automatic.digitalGain; - /* - * Expand the target frame duration so that we do not run faster than - * the minimum frame duration when we have short exposures. - */ - processFrameDuration(session, frameContext, - std::max(frameContext.minFrameDuration, newExposureTime)); - fillMetadata(session, frameContext, metadata); } @@ -1147,22 +1141,26 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, * \brief Process frame duration and compute vblank * \param[in] session The session parameters * \param[in] frameContext The current frame context - * \param[in] frameDuration The target frame duration * - * Compute and populate vblank from the target frame duration. + * Compute and populate vblank for \a frameContext. */ void AgcAlgorithm::processFrameDuration(const agc::Session &session, - agc::FrameContext &frameContext, - utils::Duration frameDuration) + agc::FrameContext &frameContext) { const utils::Duration &lineDuration = session.lineDuration; - frameContext.vblank = - (frameDuration / lineDuration) - session.sensor.outputSize.height; + /* + * Expand the target frame duration so that we do not run faster than + * the minimum frame duration when we have short exposures. + */ + const auto frameLength = std::max( + frameContext.minFrameDuration / lineDuration, + frameContext.exposure); + + frameContext.vblank = frameLength - session.sensor.outputSize.height; /* Update frame duration accounting for line length quantization. */ - frameContext.frameDuration = - (session.sensor.outputSize.height + frameContext.vblank) * lineDuration; + frameContext.frameDuration = frameLength * lineDuration; } void AgcAlgorithm::fillMetadata(const agc::Session &session, diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h index a5700d2625..4914c9cee2 100644 --- a/src/ipa/libipa/agc.h +++ b/src/ipa/libipa/agc.h @@ -146,7 +146,8 @@ public: void queueRequest(const agc::Session &session, agc::ActiveState &state, agc::FrameContext &frameContext, const ControlList &controls); - void prepare(agc::ActiveState &state, agc::FrameContext &frameContext); + void prepare(const agc::Session &session, agc::ActiveState &state, + agc::FrameContext &frameContext); void process(const agc::Session &session, agc::ActiveState &state, agc::FrameContext &frameContext, std::optional &¶ms, @@ -154,8 +155,7 @@ public: private: void processFrameDuration(const agc::Session &session, - agc::FrameContext &frameContext, - utils::Duration frameDuration); + agc::FrameContext &frameContext); void fillMetadata(const agc::Session &session, const agc::FrameContext &frameContext, ControlList &metadata); diff --git a/src/ipa/mali-c55/algorithms/agc.cpp b/src/ipa/mali-c55/algorithms/agc.cpp index 2c747da891..5684b4df95 100644 --- a/src/ipa/mali-c55/algorithms/agc.cpp +++ b/src/ipa/mali-c55/algorithms/agc.cpp @@ -207,7 +207,7 @@ void Agc::fillWeightsArrayBuffer(MaliC55Params *params, const enum MaliC55Blocks void Agc::prepare(IPAContext &context, const uint32_t frame, IPAFrameContext &frameContext, MaliC55Params *params) { - agc_.prepare(context.activeState.agc, frameContext.agc); + agc_.prepare(context.configuration.agc, context.activeState.agc, frameContext.agc); if (frame > 0) return; diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp index 1c323ede87..d82bb2ece2 100644 --- a/src/ipa/rkisp1/algorithms/agc.cpp +++ b/src/ipa/rkisp1/algorithms/agc.cpp @@ -209,7 +209,7 @@ void Agc::queueRequest(IPAContext &context, void Agc::prepare(IPAContext &context, const uint32_t frame, IPAFrameContext &frameContext, RkISP1Params *params) { - agc_.prepare(context.activeState.agc, frameContext.agc); + agc_.prepare(context.configuration.agc, context.activeState.agc, frameContext.agc); if (context.configuration.compress.supported) { frameContext.compress.enable = true; diff --git a/src/ipa/softisp/algorithms/agc.cpp b/src/ipa/softisp/algorithms/agc.cpp index ea234bc2ae..7d2a53b385 100644 --- a/src/ipa/softisp/algorithms/agc.cpp +++ b/src/ipa/softisp/algorithms/agc.cpp @@ -74,7 +74,7 @@ void Agc::queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t fram void Agc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame, IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params) { - agc_.prepare(context.activeState.agc, frameContext.agc); + agc_.prepare(context.configuration.agc, context.activeState.agc, frameContext.agc); } void Agc::process(IPAContext &context, From patchwork Mon Nov 9 09:49:47 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28810 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 0A1D4C335D for ; Mon, 9 Nov 2026 09:50:27 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id B04D36A7EC; Mon, 9 Nov 2026 10:50:16 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="kV/hAqrL"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 8F45C690F5 for ; Mon, 9 Nov 2026 10:50:01 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 08EA5981; Mon, 9 Nov 2026 10:47:40 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217660; bh=SSbAMlwRTtJo8E5BxSCyISlQ5ePMFAHOExdvpy4PxSQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=kV/hAqrLE/czuUpGchuwtsCawAgXbUJcWVeBVzCQX9BI7Tsl66cgL1C+x4lazIdoH tcmFGjsaumtHplOPDlAALn6/TuU8VCNHZrLRFbbZlFxYb3g9zTCqPZoybNptbFRO9V BdhN5b+VS34jeCkY+joQmyxxyqR9bq1OxGfhoVnw= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Stefan Klug Subject: [PATCH v7 12/19] ipa: libipa: agc: Better respect frame duration limits Date: Mon, 9 Nov 2026 10:49:47 +0100 Message-ID: <20261109094954.341480-13-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Only frame durations that are multiples of the line duration are possible, and currently it is possible that a smaller frame duration is selected even if a frame duration satisfying the requested limits is available. This is caused by the float -> integer truncation during the calculation. For (an unrealistic) example, if the exposure time is 24ms, the min frame duration is 33ms, and the line duration is 2ms, then 32ms will be used as the frame duration even if e.g. 34 would be allowed by the max frame duration. So round the result to ensure that if not prevented by the max frame duration then a value that satisfies the limits is selected. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index d87c62b4ba..3583b65025 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -1154,7 +1154,8 @@ void AgcAlgorithm::processFrameDuration(const agc::Session &session, * the minimum frame duration when we have short exposures. */ const auto frameLength = std::max( - frameContext.minFrameDuration / lineDuration, + std::min(std::ceil(frameContext.minFrameDuration / lineDuration), + frameContext.maxFrameDuration / lineDuration), frameContext.exposure); frameContext.vblank = frameLength - session.sensor.outputSize.height; From patchwork Mon Nov 9 09:49:48 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28811 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id A04E7C335B for ; Mon, 9 Nov 2026 09:50:27 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id B889E691FD; Mon, 9 Nov 2026 10:50:17 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="nu365Ia6"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D8364690F3 for ; Mon, 9 Nov 2026 10:50:01 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 4C819C23; Mon, 9 Nov 2026 10:47:40 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217660; bh=xEaK6NvWOcRx7FembtA4sCpdejyQycvMn3nGvZ33wFU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=nu365Ia64MGPbWyRLLn4fdsHU+X8yPapcnSbFcQfSeldhcggZoIAGyVv2446Zzkbe icPBZ8o7gPIOPRzMeu04DSb66UedwkRmjtLNk3qnSVIMuUzMOMFShUcO68ODIE2ROt x5R44NdsDJ3EseIRqkn2aWC0D+e4mz1+DwRtlR+Q= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham , Stefan Klug Subject: [PATCH v7 13/19] libcamera: pipeline: Set vblank on more platforms Date: Mon, 9 Nov 2026 10:49:48 +0100 Message-ID: <20261109094954.341480-14-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" The common `AgcAlgorithm` provides the desired vblank value for the given frame. So adjust `agc::prepareControls()` to accept the full frame context intead of individual sensor parameters, and make it set the desired vblank value in the control list. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham Reviewed-by: Stefan Klug --- src/ipa/ipu3/ipu3.cpp | 3 +-- src/ipa/libipa/agc.cpp | 11 +++++------ src/ipa/libipa/agc.h | 9 +++++---- src/ipa/mali-c55/mali-c55.cpp | 3 +-- src/ipa/rkisp1/rkisp1.cpp | 4 +--- src/ipa/softisp/softisp.cpp | 3 +-- src/libcamera/pipeline/ipu3/ipu3.cpp | 1 + src/libcamera/pipeline/mali-c55/mali-c55.cpp | 1 + src/libcamera/pipeline/simple/simple.cpp | 1 + 9 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp index 30d52cfcf9..e4333539af 100644 --- a/src/ipa/ipu3/ipu3.cpp +++ b/src/ipa/ipu3/ipu3.cpp @@ -551,8 +551,7 @@ void IPAIPU3::setControls(unsigned int frame) IPAFrameContext &frameContext = context_.frameContexts.get(frame); ControlList ctrls(context_.sensorControls); - agc::prepareControls(ctrls, context_.camHelper.get(), - frameContext.agc.exposure, frameContext.agc.gain); + agc::prepareControls(ctrls, context_.camHelper.get(), frameContext.agc); ControlList lensCtrls(lensCtrls_); lensCtrls.set(V4L2_CID_FOCUS_ABSOLUTE, diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 3583b65025..53c85dea04 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -60,15 +60,14 @@ namespace agc { /** * \fn prepareControls(ControlList &controls, const CameraSensorHelper *sensor, - * uint32_t exposure, double gain) + * const FrameContext &frameContext) * \param[out] controls The controls list to populate * \param[in] sensor The CameraSensorHelper - * \param[in] exposure The exposure (in lines) - * \param[in] gain The analogue gain + * \param[in] frameContext The agc frame context * - * This function sets \a V4L2_CID_EXPOSURE and \a V4L2_CID_ANALOGUE_GAIN - * in \a controls. The gain is mapped to the gain code if \a sensor is provided, - * otherwise the gain value will be used directly. + * This function sets \a V4L2_CID_EXPOSURE, \a V4L2_CID_ANALOGUE_GAIN, and + * \a V4L2_CID_VBLANK in \a controls. The gain is mapped to the gain code + * if \a sensor is provided, otherwise the gain value will be used directly. */ /** diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h index 4914c9cee2..3ca36aea64 100644 --- a/src/ipa/libipa/agc.h +++ b/src/ipa/libipa/agc.h @@ -107,13 +107,14 @@ extractControls(const ControlList &controls, const CameraSensorHelper *sensor) inline void prepareControls(ControlList &controls, const CameraSensorHelper *sensor, - uint32_t exposure, double gain) + const FrameContext &frameContext) { - controls.set(V4L2_CID_EXPOSURE, static_cast(exposure)); + controls.set(V4L2_CID_EXPOSURE, static_cast(frameContext.exposure)); controls.set(V4L2_CID_ANALOGUE_GAIN, static_cast(sensor - ? sensor->gainCode(gain) - : static_cast(gain))); + ? sensor->gainCode(frameContext.gain) + : static_cast(frameContext.gain))); + controls.set(V4L2_CID_VBLANK, static_cast(frameContext.vblank)); } } /* namespace agc */ diff --git a/src/ipa/mali-c55/mali-c55.cpp b/src/ipa/mali-c55/mali-c55.cpp index 9a2918cf2c..9541417c96 100644 --- a/src/ipa/mali-c55/mali-c55.cpp +++ b/src/ipa/mali-c55/mali-c55.cpp @@ -134,8 +134,7 @@ int IPAMaliC55::init(const IPASettings &settings, const IPAConfigInfo &ipaConfig void IPAMaliC55::setControls(const IPAFrameContext &frameContext) { ControlList ctrls(context_.sensorControls); - agc::prepareControls(ctrls, context_.camHelper.get(), - frameContext.agc.exposure, frameContext.agc.gain); + agc::prepareControls(ctrls, context_.camHelper.get(), frameContext.agc); setSensorControls.emit(ctrls); } diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp index 79ab7338c1..cefaf40aee 100644 --- a/src/ipa/rkisp1/rkisp1.cpp +++ b/src/ipa/rkisp1/rkisp1.cpp @@ -367,9 +367,7 @@ void IPARkISP1::setControls(unsigned int frame) << ", gain " << frameContext.agc.gain << ", vblank " << vblank; ControlList ctrls(context_.sensorControls); - agc::prepareControls(ctrls, context_.camHelper.get(), - exposure, frameContext.agc.gain); - ctrls.set(V4L2_CID_VBLANK, static_cast(vblank)); + agc::prepareControls(ctrls, context_.camHelper.get(), frameContext.agc); setSensorControls.emit(frame, ctrls); } diff --git a/src/ipa/softisp/softisp.cpp b/src/ipa/softisp/softisp.cpp index 00249a3004..e35f077378 100644 --- a/src/ipa/softisp/softisp.cpp +++ b/src/ipa/softisp/softisp.cpp @@ -254,8 +254,7 @@ void IPASoftIsp::processStats(const uint32_t frame, metadataReady.emit(frame, metadata); ControlList ctrls(context_.sensorControls); - agc::prepareControls(ctrls, context_.camHelper.get(), - frameContext.agc.exposure, frameContext.agc.gain); + agc::prepareControls(ctrls, context_.camHelper.get(), frameContext.agc); setSensorControls.emit(ctrls); } diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp index 14cab9e555..46af776ee9 100644 --- a/src/libcamera/pipeline/ipu3/ipu3.cpp +++ b/src/libcamera/pipeline/ipu3/ipu3.cpp @@ -1084,6 +1084,7 @@ int PipelineHandlerIPU3::registerCameras() std::unordered_map params = { { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + { V4L2_CID_VBLANK, { delays.vblankDelay, true } }, }; data->delayedCtrls_ = diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp index 73a03373c8..8225472ce8 100644 --- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp +++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp @@ -1909,6 +1909,7 @@ bool PipelineHandlerMaliC55::registerMemoryInputCamera(MediaLink *link) std::unordered_map params = { { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + { V4L2_CID_VBLANK, { delays.vblankDelay, true } }, }; data->delayedCtrls_ = diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp index 35e0664998..2f7f4c893f 100644 --- a/src/libcamera/pipeline/simple/simple.cpp +++ b/src/libcamera/pipeline/simple/simple.cpp @@ -572,6 +572,7 @@ SimpleCameraData::SimpleCameraData(SimplePipelineHandler *pipe, std::unordered_map params = { { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + { V4L2_CID_VBLANK, { delays.vblankDelay, true } }, }; delayedCtrls_ = std::make_unique(sensor_->device(), params); From patchwork Mon Nov 9 09:49:49 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28813 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 1F615C335C for ; Mon, 9 Nov 2026 09:50:29 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 5121E6A7F1; Mon, 9 Nov 2026 10:50:19 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="vYFVi17/"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 265D7691EF for ; Mon, 9 Nov 2026 10:50:02 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 95FBCE91; Mon, 9 Nov 2026 10:47:40 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217660; bh=TSeLbVaWef0i5xKvnqNwwYgr1CGN+sT8UBozqzFx0Mk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=vYFVi17/beDp6HP8IYIOLPYiFz6ntEvvUPmldzzorAF4y6jMemrgqtR+T3Ig3/HxT yNHBxP2fAtUg/Nv4dtyhWIV4lpcJNw8jKk9/kTgH/vpi228KkkeohJHf9pOp4Ub/CA B8lz+FIHdKq+LDbp1wA0NZjdRxVwZmz5PlHjq9ms= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham Subject: [PATCH v7 14/19] ipa: libipa: agc: Rework frame duration limit calculation Date: Mon, 9 Nov 2026 10:49:49 +0100 Message-ID: <20261109094954.341480-15-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" The frame duration is calculated as `frameHeight * lineDuration`, but it was effectively repeating the line duration calculation, but with microsecond precision. Instead, simply reuse the already calculated line duration, and use the already calculated min/max values in the provided `IPACameraSensorInfo`. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Kieran Bingham --- src/ipa/libipa/agc.cpp | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 53c85dea04..e56a60ef29 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -693,25 +693,6 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, << "line-duration: " << session.lineDuration << ", " << "sensor-output: " << session.sensor.outputSize; - /* - * Compute the frame duration limits. - * - * The frame length is computed assuming a fixed line length combined - * with the vertical frame sizes. - */ - - const ControlInfo &v4l2VBlank = config.sensorControls.find(V4L2_CID_VBLANK)->second; - std::array frameHeights{ - v4l2VBlank.min().get() + config.sensorInfo.outputSize.height, - v4l2VBlank.max().get() + config.sensorInfo.outputSize.height, - v4l2VBlank.def().get() + config.sensorInfo.outputSize.height, - }; - - std::array frameDurations; - for (unsigned int i = 0; i < frameHeights.size(); ++i) { - uint64_t frameSize = static_cast(lineLength) * frameHeights[i]; - frameDurations[i] = frameSize * 1000000U / config.sensorInfo.pixelRate; - } /* * When the AGC computes the new exposure values for a frame, it needs @@ -725,8 +706,8 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, session.minAnalogueGain = minGain; session.maxAnalogueGain = maxGain; session.defAnalogueGain = defGain; - session.minFrameDuration = std::chrono::microseconds(frameDurations[0]); - session.maxFrameDuration = std::chrono::microseconds(frameDurations[1]); + session.minFrameDuration = config.sensorInfo.minFrameLength * session.lineDuration; + session.maxFrameDuration = config.sensorInfo.maxFrameLength * session.lineDuration; /* Configure the default exposure and gain. */ state = {}; @@ -765,8 +746,12 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, static_cast(defExposure * lineDurationUs), }; config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{ - frameDurations[0], frameDurations[1], - std::span{ { frameDurations[0], frameDurations[1] } }, + static_cast(session.minFrameDuration.get()), + static_cast(session.maxFrameDuration.get()), + std::span{ { + static_cast(state.minFrameDuration.get()), + static_cast(state.maxFrameDuration.get()), + } }, }; const auto add = [&](const ControlId &cid, const auto &automatic, const auto &manual) { From patchwork Mon Nov 9 09:49:50 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28812 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 58C90C335E for ; Mon, 9 Nov 2026 09:50:28 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 94D146A7EF; Mon, 9 Nov 2026 10:50:18 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="azSZAQZd"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 6F1AC69301 for ; Mon, 9 Nov 2026 10:50:02 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id DCC4E981; Mon, 9 Nov 2026 10:47:40 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217661; bh=VEkKqX0/iBosfajhCL5ipW6nWIYFDEzsugiWZJ7HKxw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=azSZAQZdII5KxvyY9/0iF/enVNDWmWsJsnfHRFGvJJI2PPXMgAuXamztOteXwE5BS QPJ/Ux/+v+VK1+ed6pi5iIOx7XfKJQJBts9e9fZUO+dG3lFIqniC8pg8ZUq7zU9Vam n29PheBGbWPSnBc4ZoZHjWTnDElcx5IbXXcr7QfM= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Kieran Bingham Subject: [PATCH v7 15/19] ipa: camera_sensor_helper: Introduce exposure margin Date: Mon, 9 Nov 2026 10:49:50 +0100 Message-ID: <20261109094954.341480-16-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Jacopo Mondi There is a margin between the maximum achievable shutter time and the frame length. The value is sensor-dependent and should be used to adjust the maximum achievable exposure time whenever the frame length changes. Introduce in the CameraSensorHelper the notion of exposureMargin_ as the minimum difference in number of lines between the current frame length and the maximum exposure time. This feature is already implemented in the RPi CamHelper class hierarchy with the name of frameIntegrationDiff. Populate the CameraSensorHelper instances with an exposureMargin_. The value of the exposure margin comes from the mainline driver version of each sensor, and it has been compared with the frameIntegrationDiff_ value in the rpi camera helpers. Signed-off-by: Jacopo Mondi Signed-off-by: Barnabás Pőcze Reviewed-by: Kieran Bingham Reviewed-by: Stefan Klug --- src/ipa/libipa/camera_sensor_helper.cpp | 68 +++++++++++++++++++++++++ src/ipa/libipa/camera_sensor_helper.h | 2 + 2 files changed, 70 insertions(+) diff --git a/src/ipa/libipa/camera_sensor_helper.cpp b/src/ipa/libipa/camera_sensor_helper.cpp index 359040ec74..608937d45e 100644 --- a/src/ipa/libipa/camera_sensor_helper.cpp +++ b/src/ipa/libipa/camera_sensor_helper.cpp @@ -76,6 +76,16 @@ namespace ipa { * \return The black level of the sensor, or std::nullopt if not known */ +/** + * \fn CameraSensorHelper::exposureMargin() + * \brief Fetch the exposure margin of the sensor + * + * This function returns the exposure margin of the sensor, in lines, which + * is the minimum difference between the integration time and frame length. + * + * \return The exposure margin of the sensor, or std::nullopt if not known + */ + /** * \brief Compute gain code from the analogue gain absolute value * \param[in] gain The real gain to pass @@ -229,6 +239,12 @@ double CameraSensorHelper::quantizeGain(double _gain, double *quantizationGain) * sensor specific. Use this variable to store the values at init time. */ +/** + * \var CameraSensorHelper::exposureMargin_ + * \brief The smallest margin between the integration time and the frame length + * in lines + */ + /** * \class CameraSensorHelperFactoryBase * \brief Base class for camera sensor helper factories @@ -387,6 +403,7 @@ public: { /* Power-on default value: 168 at 12bits. */ blackLevel_ = 2688; + exposureMargin_ = 4; } uint32_t gainCode(double gain) const override @@ -473,6 +490,11 @@ REGISTER_CAMERA_SENSOR_HELPER("ar0144", CameraSensorHelperAr0144) class CameraSensorHelperAr0521 : public CameraSensorHelper { public: + CameraSensorHelperAr0521() + { + exposureMargin_ = 4; + } + uint32_t gainCode(double gain) const override { gain = std::clamp(gain, 1.0, 15.5); @@ -503,6 +525,7 @@ public: /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 100, 0, 0, 1024 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("gc05a2", CameraSensorHelperGc05a2) @@ -515,6 +538,7 @@ public: /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 100, 0, 0, 1024 }; + exposureMargin_ = 16; } }; REGISTER_CAMERA_SENSOR_HELPER("gc08a3", CameraSensorHelperGc08a3) @@ -525,6 +549,7 @@ public: CameraSensorHelperHm1246() { gain_ = AnalogueGainLinear{ 1, 16, 0, 16 }; + exposureMargin_ = 2; } }; REGISTER_CAMERA_SENSOR_HELPER("hm1246", CameraSensorHelperHm1246) @@ -537,6 +562,7 @@ public: /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 512, -1, 512 }; + exposureMargin_ = 10; } }; REGISTER_CAMERA_SENSOR_HELPER("imx214", CameraSensorHelperImx214) @@ -549,6 +575,7 @@ public: /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 256, -1, 256 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("imx219", CameraSensorHelperImx219) @@ -561,6 +588,7 @@ public: /* From datasheet: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 512, -1, 512 }; + exposureMargin_ = 10; } }; REGISTER_CAMERA_SENSOR_HELPER("imx258", CameraSensorHelperImx258) @@ -573,6 +601,7 @@ public: /* From datasheet: 0x32 at 10bits. */ blackLevel_ = 3200; gain_ = AnalogueGainLinear{ 0, 2048, -1, 2048 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("imx283", CameraSensorHelperImx283) @@ -585,6 +614,7 @@ public: /* From datasheet: 0xf0 at 12bits. */ blackLevel_ = 3840; gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; + exposureMargin_ = 2; } }; REGISTER_CAMERA_SENSOR_HELPER("imx290", CameraSensorHelperImx290) @@ -595,6 +625,11 @@ public: CameraSensorHelperImx296() { gain_ = AnalogueGainExp{ 1.0, expGainDb(0.1) }; + /* + * The driver doesn't apply any margin. Use the value + * in RPi's CamHelper. + */ + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("imx296", CameraSensorHelperImx296) @@ -612,6 +647,7 @@ public: /* From datasheet: 0x32 at 10bits. */ blackLevel_ = 3200; gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; + exposureMargin_ = 10; } }; REGISTER_CAMERA_SENSOR_HELPER("imx335", CameraSensorHelperImx335) @@ -624,6 +660,7 @@ public: /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 1024, -1, 1024 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("imx355", CameraSensorHelperImx355) @@ -634,6 +671,7 @@ public: CameraSensorHelperImx415() { gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("imx415", CameraSensorHelperImx415) @@ -652,6 +690,7 @@ public: blackLevel_ = 4096; /* gain = 1024/(1024 - code); code = [0, 800]; max gain = x4.57 */ gain_ = AnalogueGainLinear{ 0, 1024, -1, 1024 }; + exposureMargin_ = 18; } }; REGISTER_CAMERA_SENSOR_HELPER("imx471", CameraSensorHelperImx471) @@ -662,6 +701,7 @@ public: CameraSensorHelperImx477() { gain_ = AnalogueGainLinear{ 0, 1024, -1, 1024 }; + exposureMargin_ = 22; } }; REGISTER_CAMERA_SENSOR_HELPER("imx477", CameraSensorHelperImx477) @@ -674,6 +714,7 @@ public: /* From datasheet: 0xc8 at 12bits. */ blackLevel_ = 3200; gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("imx662", CameraSensorHelperImx662) @@ -686,6 +727,7 @@ public: /* From datasheet: 0x32 at 10bits. */ blackLevel_ = 3200; gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("imx678", CameraSensorHelperImx678) @@ -697,6 +739,7 @@ public: { blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 1024, -1, 1024 }; + exposureMargin_ = 48; } }; REGISTER_CAMERA_SENSOR_HELPER("imx708", CameraSensorHelperImx708) @@ -709,6 +752,7 @@ public: /* From dark frame measurement: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 1, 0, 0, 256 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov01a10", CameraSensorHelperOv01a10) @@ -721,6 +765,7 @@ public: /* From Linux kernel driver: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 6; } }; REGISTER_CAMERA_SENSOR_HELPER("ov08d10", CameraSensorHelperOv08d10) @@ -732,6 +777,7 @@ public: { blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov08x40", CameraSensorHelperOv08x40) @@ -744,6 +790,7 @@ public: blackLevel_ = 3600; /* gain is fixed to 1x, this is just a place holder. */ gain_ = AnalogueGainLinear{ 0, 2048, -1, 2048 }; + exposureMargin_ = 6; } }; REGISTER_CAMERA_SENSOR_HELPER("mira220", CameraSensorHelperMira220) @@ -758,6 +805,7 @@ public: * This has been validated with some empirical testing only. */ gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("ov2685", CameraSensorHelperOv2685) @@ -770,6 +818,7 @@ public: /* From Linux kernel driver: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov2740", CameraSensorHelperOv2740) @@ -782,6 +831,7 @@ public: /* From datasheet: 0x40 at 12bits. */ blackLevel_ = 1024; gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("ov4689", CameraSensorHelperOv4689) @@ -794,6 +844,14 @@ public: /* From datasheet: 0x10 at 10bits. */ blackLevel_ = 1024; gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; + /* + * Very convoluted in the driver that however applies a margin + * of 4 lines when setting vts. + * + * cap_vts = cap_shutter + 4; + * ret = ov5640_set_vts(sensor, cap_vts); + */ + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5640", CameraSensorHelperOv5640) @@ -804,6 +862,7 @@ public: CameraSensorHelperOv5647() { gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5647", CameraSensorHelperOv5647) @@ -814,6 +873,7 @@ public: CameraSensorHelperOv5670() { gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5670", CameraSensorHelperOv5670) @@ -826,6 +886,7 @@ public: /* From Linux kernel driver: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5675", CameraSensorHelperOv5675) @@ -837,6 +898,7 @@ public: { blackLevel_ = 1024; gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5693", CameraSensorHelperOv5693) @@ -847,6 +909,7 @@ public: CameraSensorHelperOv64a40() { gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 32; } }; REGISTER_CAMERA_SENSOR_HELPER("ov64a40", CameraSensorHelperOv64a40) @@ -863,6 +926,7 @@ public: * See: https://patchwork.linuxtv.org/project/linux-media/patch/20221106171129.166892-2-nicholas@rothemail.net/#142267 */ gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 4; } }; REGISTER_CAMERA_SENSOR_HELPER("ov8858", CameraSensorHelperOv8858) @@ -873,6 +937,7 @@ public: CameraSensorHelperOv8865() { gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov8865", CameraSensorHelperOv8865) @@ -883,6 +948,7 @@ public: CameraSensorHelperOv13858() { gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; + exposureMargin_ = 8; } }; REGISTER_CAMERA_SENSOR_HELPER("ov13858", CameraSensorHelperOv13858) @@ -895,6 +961,7 @@ public: /* From datasheet: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 32, -1, 32 }; + exposureMargin_ = 64; } }; REGISTER_CAMERA_SENSOR_HELPER("vd55g1", CameraSensorHelperVd55g1) @@ -907,6 +974,7 @@ public: /* From datasheet: 0x40 at 10bits. */ blackLevel_ = 4096; gain_ = AnalogueGainLinear{ 0, 32, -1, 32 }; + exposureMargin_ = 75; } }; REGISTER_CAMERA_SENSOR_HELPER("vd56g3", CameraSensorHelperVd56g3) diff --git a/src/ipa/libipa/camera_sensor_helper.h b/src/ipa/libipa/camera_sensor_helper.h index bd3d0beec7..a9e55c832d 100644 --- a/src/ipa/libipa/camera_sensor_helper.h +++ b/src/ipa/libipa/camera_sensor_helper.h @@ -27,6 +27,7 @@ public: virtual ~CameraSensorHelper() = default; std::optional blackLevel() const { return blackLevel_; } + std::optional exposureMargin() const { return exposureMargin_; } virtual uint32_t gainCode(double gain) const; virtual double gain(uint32_t gainCode) const; double quantizeGain(double gain, double *quantizationGain) const; @@ -46,6 +47,7 @@ protected: std::optional blackLevel_; std::variant gain_; + std::optional exposureMargin_; private: LIBCAMERA_DISABLE_COPY_AND_MOVE(CameraSensorHelper) From patchwork Mon Nov 9 09:49:51 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28815 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 87339C3304 for ; Mon, 9 Nov 2026 09:50:30 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 6F2456A7E9; Mon, 9 Nov 2026 10:50:23 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="A0eEZjv7"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id B0CCB6933A for ; Mon, 9 Nov 2026 10:50:02 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 2C252DA8; Mon, 9 Nov 2026 10:47:41 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217661; bh=EXL3D1XHN0/ocszjGnLe13HlZqTsxX7SWgBcbqfHIUM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=A0eEZjv7O/IQ5LEuun8BygQQ1wI6txhGRYIdspULm5+MlQhpi+5Lma2TwkcIVkEk4 Lj1l05NpwTFelkkm/Dt9aVtqjdRX2gs+E+k8bCuB2Mtb+LxYawtpYEMRWMJL9o8SKe Mus/FWgjKADPq9GxpN5mgP1sRGh50T7klP5iqZUI= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Kieran Bingham , Jacopo Mondi Subject: [PATCH v7 16/19] ipa: libipa: agc: Move control info map setup Date: Mon, 9 Nov 2026 10:49:51 +0100 Message-ID: <20261109094954.341480-17-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Move the setup of common controls to the end of `configure()`, so that it happens after everything has been configured and calculated. Signed-off-by: Barnabás Pőcze Reviewed-by: Kieran Bingham Reviewed-by: Jacopo Mondi --- src/ipa/libipa/agc.cpp | 82 +++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index e56a60ef29..64e2ca787f 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -733,47 +733,6 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, config.ctrlMap.erase(&controls::AeConstraintMode); config.ctrlMap.erase(&controls::AeExposureMode); - /* \todo Move this to the `Camera` class. */ - config.ctrlMap[&controls::AeEnable] = ControlInfo{ - false, session.autoAllowed, session.autoAllowed - }; - config.ctrlMap[&controls::AnalogueGain] = ControlInfo{ - minGain, maxGain, defGain - }; - config.ctrlMap[&controls::ExposureTime] = ControlInfo{ - static_cast(minExposure * lineDurationUs), - static_cast(maxExposure * lineDurationUs), - static_cast(defExposure * lineDurationUs), - }; - config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{ - static_cast(session.minFrameDuration.get()), - static_cast(session.maxFrameDuration.get()), - std::span{ { - static_cast(state.minFrameDuration.get()), - static_cast(state.maxFrameDuration.get()), - } }, - }; - - const auto add = [&](const ControlId &cid, const auto &automatic, const auto &manual) { - std::array values; - size_t count = 0; - - if (session.autoAllowed) - values[count++] = ControlValue(automatic); - - values[count++] = ControlValue(manual); - - config.ctrlMap[&cid] = ControlInfo{ - { values.data(), count }, - ControlValue(session.autoAllowed ? automatic : manual), - }; - }; - - add(controls::ExposureTimeMode, - controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual); - add(controls::AnalogueGainMode, - controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual); - std::visit(utils::overloaded{ [&](AgcMSV &) { /* No constraint/exposure mode support. */ @@ -820,6 +779,47 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, }, }, impl_); + /* \todo Move this to the `Camera` class. */ + config.ctrlMap[&controls::AeEnable] = ControlInfo{ + false, session.autoAllowed, session.autoAllowed + }; + config.ctrlMap[&controls::AnalogueGain] = ControlInfo{ + minGain, maxGain, defGain + }; + config.ctrlMap[&controls::ExposureTime] = ControlInfo{ + static_cast(minExposure * lineDurationUs), + static_cast(maxExposure * lineDurationUs), + static_cast(defExposure * lineDurationUs), + }; + config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{ + static_cast(session.minFrameDuration.get()), + static_cast(session.maxFrameDuration.get()), + std::span{ { + static_cast(state.minFrameDuration.get()), + static_cast(state.maxFrameDuration.get()), + } }, + }; + + const auto add = [&](const ControlId &cid, const auto &automatic, const auto &manual) { + std::array values; + size_t count = 0; + + if (session.autoAllowed) + values[count++] = ControlValue(automatic); + + values[count++] = ControlValue(manual); + + config.ctrlMap[&cid] = ControlInfo{ + { values.data(), count }, + ControlValue(session.autoAllowed ? automatic : manual), + }; + }; + + add(controls::ExposureTimeMode, + controls::ExposureTimeModeAuto, controls::ExposureTimeModeManual); + add(controls::AnalogueGainMode, + controls::AnalogueGainModeAuto, controls::AnalogueGainModeManual); + return 0; } From patchwork Mon Nov 9 09:49:52 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28814 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id D8E9BC335F for ; Mon, 9 Nov 2026 09:50:29 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id D9FCA6936C; Mon, 9 Nov 2026 10:50:21 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="EKJqkOBM"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 004F76936C for ; Mon, 9 Nov 2026 10:50:02 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 6DE9E15CD; Mon, 9 Nov 2026 10:47:41 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217661; bh=FzwY+EWFjODoywo7djTuO/BKZLwIholw39WiPIsJbik=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=EKJqkOBM5UoHddz8Lkfnk26ow0RosV4zPms8mJlUTwlZdWT9zr544AX8O0F0mYvQj 0DdFwdrj3UWXdAbEsS4wur6jNOFXsvYX+/+xKvazNcr3CLkW/iA4Eq9FgcFWCXGVn5 z15ajxbudmgT6RKy9geUU80hrp0jFhkSafJrNvds= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [PATCH v7 17/19] ipa: libipa: agc: Do not run faster than 30 fps by default Date: Mon, 9 Nov 2026 10:49:52 +0100 Message-ID: <20261109094954.341480-18-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" In auto mode, by default the camera should not run faster than 30fps unless the user explicitly adjusts `FrameDurationLimits`. This matches the behaviour of the raspberry pi agc algorithm. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 64e2ca787f..94d3ffa42d 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -779,6 +779,13 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, }, }, impl_); + if (session.autoAllowed) { + /* By default do not run faster than 30fps. */ + state.minFrameDuration = std::clamp( + utils::Duration(1.0s / 30), + session.minFrameDuration, session.maxFrameDuration); + } + /* \todo Move this to the `Camera` class. */ config.ctrlMap[&controls::AeEnable] = ControlInfo{ false, session.autoAllowed, session.autoAllowed From patchwork Mon Nov 9 09:49:53 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28816 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 4310EC3360 for ; Mon, 9 Nov 2026 09:50:31 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 3B2656A7F3; Mon, 9 Nov 2026 10:50:24 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="DzCoGIDM"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 37D13691E5 for ; Mon, 9 Nov 2026 10:50:03 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id B1E801894; Mon, 9 Nov 2026 10:47:41 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217661; bh=Gk+cuWi7udWZg1mZkE2/thGCQ9RTeWVnijIDzFHNzl0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=DzCoGIDMyeo9SOgdlXlh71XOnDrt9/k3gVp5Y4/0bd8VAQWI1mz6iEIVzQmnjdoPQ YC6YYoDxo1Uyb0LjqbBctPIFFxUjXuUb4czVqIomaKrAp7/S21Kgz0xKBLx1nqut9g IzWBJ4cvJsY6+3SH8pPzavUPoXdHZWu8V69pfR1c= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [PATCH v7 18/19] ipa: libipa: agc: Do not run slower than 15fps by default Date: Mon, 9 Nov 2026 10:49:53 +0100 Message-ID: <20261109094954.341480-19-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" If the tuning file defines no exposure modes, then the "Maximize" mode will be implicitly instantiated and used. But in this case, do not let the camera run slower than 15fps, by default, unless the user explicitly requests it using `FrameDurationLimits`. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 12 ++++++++++++ src/ipa/libipa/agc.h | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 94d3ffa42d..017e4d42fe 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -615,6 +615,8 @@ int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor, if (ret) return ret; + tuning_.hasExposureModes = !agcConfig.exposureModeHelpers.empty(); + addFallbacks(agcConfig); impl_.emplace(std::move(agcConfig)); @@ -784,6 +786,16 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, state.minFrameDuration = std::clamp( utils::Duration(1.0s / 30), session.minFrameDuration, session.maxFrameDuration); + + if (!tuning_.hasExposureModes) { + /* + * By default do not run slower than 15fps + * if no exposure modes are defined. + */ + state.maxFrameDuration = std::clamp( + utils::Duration(1.0s / 15), + state.minFrameDuration, session.maxFrameDuration); + } } /* \todo Move this to the `Camera` class. */ diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h index 3ca36aea64..d8057720d0 100644 --- a/src/ipa/libipa/agc.h +++ b/src/ipa/libipa/agc.h @@ -163,6 +163,10 @@ private: std::variant impl_; CameraSensorHelper *sensor_ = nullptr; + + struct { + bool hasExposureModes = false; + } tuning_; }; } /* namespace ipa */ From patchwork Mon Nov 9 09:49:54 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28817 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id EF923C3361 for ; Mon, 9 Nov 2026 09:50:31 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 162EA6A7F5; Mon, 9 Nov 2026 10:50:25 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="NJ6VCV48"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 7CDFE691FD for ; Mon, 9 Nov 2026 10:50:03 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id EED6218C7; Mon, 9 Nov 2026 10:47:41 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1794217662; bh=N+70XR63z0tgzcxyQi9FQfWFgyVi22Lr60x8Dk3iHmE=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=NJ6VCV48gGmayLc6lMahuPSc7FOSjaZlFj5KuCNyFCunVOb3/nu/Hgj6ekAu25jDJ 5x8akdGurrqTXE0EhCP/II35O4vn0WUI7V7tuiYqCX26m+umr5iWfnJ5lJWhQPZqST p/6Pdr5cvrZlGerwqwZ3vyxZjfV3Fb6OD+E4++Kg= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Stefan Klug Subject: [PATCH v7 19/19] ipa: libipa: agc: Take exposure margin into account Date: Mon, 9 Nov 2026 10:49:54 +0100 Message-ID: <20261109094954.341480-20-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> References: <20261109094954.341480-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" The sensor exposure time depends on the vertical blanking amount and the exposure margin. Instead of relying on the maximum exposure available at onfiguration, use the frame duration limits (~ vblank) and exposure margin to dynamically calculate the max available exposure time. Signed-off-by: Barnabás Pőcze Reviewed-by: Jacopo Mondi Reviewed-by: Stefan Klug --- src/ipa/libipa/agc.cpp | 100 ++++++++++++++++++++++++++++------------- src/ipa/libipa/agc.h | 3 +- 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 017e4d42fe..952f9f4b70 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -74,12 +74,12 @@ namespace agc { * \struct Session * \brief Session configuration for AgcAlgorithm * + * \var Session::minExposure + * \brief Minimum exposure (in lines) for the streaming session + * * \var Session::minExposureTime * \brief Minimum exposure time for the streaming session * - * \var Session::maxExposureTime - * \brief Maximum exposure time for the streaming session - * * \var Session::minAnalogueGain * \brief Minimum analogue gain for the streaming session * @@ -104,6 +104,11 @@ namespace agc { * \var Session::sensor.outputSize * \brief Configured output size of the sensor * + * \var Session::sensor.exposureMargin + * \brief Exposure margin of the sensor + * + * \sa CameraSensorHelper::exposureMargin() + * * \var Session::autoAllowed * \copybrief AgcAlgorithm::ConfigurationParams::autoAllowed * \sa AgcAlgorithm::ConfigurationParams::autoAllowed @@ -470,6 +475,16 @@ void addFallbacks(AgcMeanLuminance::Configuration &config) }); } +[[nodiscard]] +uint32_t clampExposure(const agc::Session &session, const agc::FrameContext &frameContext, + uint32_t exposure) +{ + const uint32_t maxFrame = frameContext.maxFrameDuration / session.lineDuration; + const uint32_t maxExposure = maxFrame - session.sensor.exposureMargin; + + return std::clamp(exposure, session.minExposure, maxExposure); +} + } /* namespace */ /** @@ -667,7 +682,12 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, session.lineDuration = lineLength * 1.0s / config.sensorInfo.pixelRate; session.sensor.outputSize = config.sensorInfo.outputSize; - const double lineDurationUs = session.lineDuration.get(); + auto exposureMargin = sensor_ ? sensor_->exposureMargin() : std::nullopt; + session.sensor.exposureMargin = exposureMargin.value_or(4); + if (!exposureMargin) + LOG(Agc, Warning) + << "Sensor exposure margin not available, using " + << session.sensor.exposureMargin; /* * Compute exposure time limits from the V4L2_CID_EXPOSURE control @@ -676,7 +696,6 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, const ControlInfo &v4l2Exposure = config.sensorControls.find(V4L2_CID_EXPOSURE)->second; int32_t minExposure = v4l2Exposure.min().get(); - int32_t maxExposure = v4l2Exposure.max().get(); int32_t defExposure = v4l2Exposure.def().get(); /* Compute the analogue gain limits. */ @@ -689,28 +708,46 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, float maxGain = extractGain(v4l2Gain.max()); float defGain = extractGain(v4l2Gain.def()); - LOG(Agc, Debug) - << "exposure: [" << minExposure << ',' << maxExposure << "], " - << "gain: [" << minGain << ',' << maxGain << "], " - << "line-duration: " << session.lineDuration << ", " - << "sensor-output: " << session.sensor.outputSize; - - /* * When the AGC computes the new exposure values for a frame, it needs * to know the limits for exposure time and analogue gain. As it depends * on the sensor, update it with the controls. - * - * \todo take VBLANK into account for maximum exposure time */ + session.minExposure = minExposure; session.minExposureTime = minExposure * session.lineDuration; - session.maxExposureTime = maxExposure * session.lineDuration; session.minAnalogueGain = minGain; session.maxAnalogueGain = maxGain; session.defAnalogueGain = defGain; session.minFrameDuration = config.sensorInfo.minFrameLength * session.lineDuration; session.maxFrameDuration = config.sensorInfo.maxFrameLength * session.lineDuration; + /* + * The assumption is that the minimum exposure time is independent of vblank, + * only the maximum exposure time is affected by it. + */ + const uint32_t maxExposure = config.sensorInfo.maxFrameLength - session.sensor.exposureMargin; + const utils::Duration maxExposureTime = maxExposure * session.lineDuration; + + LOG(Agc, Debug) + << "exposure: [" << session.minExposure << ',' << maxExposure << "], " + << "exposure-time: [" << session.minExposureTime << ',' << maxExposureTime << "], " + << "gain: [" << session.minAnalogueGain << ',' << session.maxAnalogueGain << "], " + << "line-length: " << lineLength << ", " + << "line-duration: " << session.lineDuration << ", " + << "frame-height: [" << config.sensorInfo.minFrameLength << ',' << config.sensorInfo.maxFrameLength << "], " + << "sensor-output: " << session.sensor.outputSize << ", " + << "sensor-exposure-margin: " << session.sensor.exposureMargin; + + /* + * This is a sanity check. If this fails, something is very likely off. + * Equality could theoretically be allowed, but it is not so that + * `(frameDuration / lineDuration) - exposureMargin` is no less + * than the minimum exposure time even if the division is slightly + * inaccurate. + */ + if (session.minExposure + session.sensor.exposureMargin >= config.sensorInfo.minFrameLength) + return -EINVAL; + /* Configure the default exposure and gain. */ state = {}; state.automatic.gain = session.minAnalogueGain; @@ -806,9 +843,10 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, minGain, maxGain, defGain }; config.ctrlMap[&controls::ExposureTime] = ControlInfo{ - static_cast(minExposure * lineDurationUs), - static_cast(maxExposure * lineDurationUs), - static_cast(defExposure * lineDurationUs), + static_cast(session.minExposureTime.get()), + static_cast(maxExposureTime.get()), + static_cast(state.automatic.exposure * + session.lineDuration.get()), }; config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{ static_cast(session.minFrameDuration.get()), @@ -931,7 +969,7 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s frameContext.autoGainEnabled = state.autoGainEnabled; if (!frameContext.autoExposureEnabled) - frameContext.exposure = state.manual.exposure; + frameContext.exposure = clampExposure(session, frameContext, state.manual.exposure); if (!frameContext.autoGainEnabled) frameContext.gain = state.manual.gain; @@ -1045,21 +1083,19 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, /* * Set the AGC limits using the fixed exposure time and/or gain in * manual mode, or the sensor limits in auto mode. + * + * \todo This uses the configuration from an already completed frame. */ - utils::Duration minExposureTime; - utils::Duration maxExposureTime; + utils::Duration minExposureTime = session.minExposureTime; + utils::Duration maxExposureTime = frameContext.maxFrameDuration - + session.sensor.exposureMargin * lineDuration; double minAnalogueGain; double maxAnalogueGain; - /* \todo This uses the configuration from an already completed frame. */ - - if (frameContext.autoExposureEnabled) { - minExposureTime = session.minExposureTime; - maxExposureTime = std::clamp(frameContext.maxFrameDuration, - session.minExposureTime, - session.maxExposureTime); - } else { - minExposureTime = lineDuration * frameContext.exposure; + if (!frameContext.autoExposureEnabled) { + minExposureTime = std::clamp( + frameContext.exposure * lineDuration, + minExposureTime, maxExposureTime); maxExposureTime = minExposureTime; } @@ -1129,6 +1165,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, }, }, impl_); + state.automatic.exposure = clampExposure(session, frameContext, state.automatic.exposure); + const utils::Duration newExposureTime = state.automatic.exposure * lineDuration; LOG(Agc, Debug) @@ -1159,7 +1197,7 @@ void AgcAlgorithm::processFrameDuration(const agc::Session &session, const auto frameLength = std::max( std::min(std::ceil(frameContext.minFrameDuration / lineDuration), frameContext.maxFrameDuration / lineDuration), - frameContext.exposure); + frameContext.exposure + session.sensor.exposureMargin); frameContext.vblank = frameLength - session.sensor.outputSize.height; diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h index d8057720d0..9b4bcf0bed 100644 --- a/src/ipa/libipa/agc.h +++ b/src/ipa/libipa/agc.h @@ -36,8 +36,8 @@ class Histogram; namespace agc { struct Session { + uint32_t minExposure; utils::Duration minExposureTime; - utils::Duration maxExposureTime; double minAnalogueGain; double maxAnalogueGain; double defAnalogueGain; @@ -47,6 +47,7 @@ struct Session { struct { Size outputSize; + uint32_t exposureMargin; } sensor; bool autoAllowed;