From patchwork Thu Aug 27 10:41:08 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: 28119 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 589ADC3343 for ; Thu, 27 Aug 2026 10:41:26 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 813606847D; Thu, 27 Aug 2026 12:41:25 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="h9yC/isa"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D1B7068466 for ; Thu, 27 Aug 2026 12:41:12 +0200 (CEST) Received: from pb-laptop.local (185.221.143.32.nat.pool.zt.hu [185.221.143.32]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 4C821C1 for ; Thu, 27 Aug 2026 12:39:46 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1787827186; bh=v4nIKINNParjUS0ziW6POnkDO2DXydmZd62CQ6m5jTI=; h=From:To:Subject:Date:In-Reply-To:References:From; b=h9yC/isaWNdXEI4obHVayYb4MuZxlMccEuuXK3e9ym9OaHfVsRB+jhBMoTf9Uk8J3 jrRnIbX5JPJ0zBBeyaGuMLtprSyTdRzSU9OKOosKK2n7GmiX4uW874btkimg7VSBkt 4SoW5lC6sKnVHzl0LU7mEAO+jniwAPdMYHEIdAmg= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Subject: [RFC PATCH v1 8/8] ipa: libipa: agc: Take exposure margin into account Date: Thu, 27 Aug 2026 12:41:08 +0200 Message-ID: <20260827104108.1432632-9-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260827104108.1432632-1-barnabas.pocze@ideasonboard.com> References: <20260827104108.1432632-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 Tested-by: Jacopo Mondi --- src/ipa/libipa/agc.cpp | 93 ++++++++++++++++++++++++++++++++---------- src/ipa/libipa/agc.h | 3 +- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 6783055a5e..6ef1f938bd 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -70,12 +70,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 * @@ -100,6 +100,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 @@ -221,6 +226,31 @@ namespace agc { } /* namespace agc */ +namespace { + +[[nodiscard]] +uint32_t clampExposure(const agc::Session &session, const agc::ActiveState &state, + uint32_t exposure) +{ + return std::max( + std::min( + exposure, + (state.maxFrameDuration / session.lineDuration) - session.sensor.exposureMargin + ), + session.minExposure + ); +} + +[[nodiscard]] +uint32_t clampExposure(const agc::Session &session, const agc::ActiveState &state, + utils::Duration exposureTime) +{ + return clampExposure(session, state, exposureTime / session.lineDuration); +} + +} /* namespace */ + + /** * \class AgcAlgorithm * \brief libIPA LSC algorithm algorithm @@ -360,6 +390,14 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, session.lineDuration = lineLength * 1.0s / config.sensorInfo.pixelRate; session.sensor.outputSize = config.sensorInfo.outputSize; + 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; + } + const double lineDurationUs = session.lineDuration.get(); /* @@ -369,7 +407,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. */ @@ -382,12 +419,6 @@ 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; - /* * Compute the frame duration limits. * @@ -408,17 +439,32 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, * 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 = frameHeights.min * session.lineDuration; session.maxFrameDuration = frameHeights.max * session.lineDuration; + const uint32_t maxExposure = frameHeights.max - session.sensor.exposureMargin; + const utils::Duration maxExposureTime = maxExposure * session.lineDuration; + + ASSERT(frameHeights.max > session.sensor.exposureMargin); + ASSERT(session.minExposure + session.sensor.exposureMargin <= frameHeights.min); + ASSERT(static_cast(session.minExposure) < maxExposure); + + 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: [" << frameHeights.min << ',' << frameHeights.max << "], " + << "sensor-output: " << session.sensor.outputSize << ", " + << "sensor-exposure-margin: " << session.sensor.exposureMargin; + /* Configure the default exposure and gain. */ state = {}; state.automatic.gain = session.minAnalogueGain; @@ -431,7 +477,9 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, state.autoGainEnabled = session.autoAllowed; state.exposureValue = 0; state.minFrameDuration = session.minFrameDuration; - state.maxFrameDuration = session.maxFrameDuration; + state.maxFrameDuration = std::clamp( + utils::Duration(1.0s / 5), /* Try to achieve at least 5 fps by default. */ + session.minFrameDuration, session.maxFrameDuration); /* * The IPA control maps keep their states, so the removal is necessary. @@ -451,9 +499,9 @@ 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 * lineDurationUs), }; config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{ static_cast(session.minFrameDuration.get()), @@ -606,7 +654,7 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s const auto &exposure = controls.get(controls::ExposureTime); if (exposure && !state.autoExposureEnabled) { - state.manual.exposure = *exposure * 1.0us / session.lineDuration; + state.manual.exposure = clampExposure(session, state, *exposure * 1.0us); LOG(Agc, Debug) << "Set exposure to " << state.manual.exposure; } @@ -700,7 +748,7 @@ void AgcAlgorithm::prepare(const agc::Session& session, agc::ActiveState &state, */ const auto frameDuration = std::max( frameContext.minFrameDuration / session.lineDuration, - frameContext.exposure); + frameContext.exposure + session.sensor.exposureMargin); frameContext.vblank = frameDuration - session.sensor.outputSize.height; @@ -755,9 +803,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, if (state.autoExposureEnabled) { minExposureTime = session.minExposureTime; - maxExposureTime = std::clamp(state.maxFrameDuration, - session.minExposureTime, - session.maxExposureTime); + maxExposureTime = + state.maxFrameDuration - session.sensor.exposureMargin * session.lineDuration; } else { minExposureTime = lineDuration * state.manual.exposure; maxExposureTime = minExposureTime; @@ -829,6 +876,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, }, }, impl_); + state.automatic.exposure = clampExposure(session, state, state.automatic.exposure); + const utils::Duration newExposureTime = state.automatic.exposure * lineDuration; LOG(Agc, Debug) diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h index 388693d21d..bfb60150db 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;